co-maps/iphone/Maps/Model/Settings.swift

486 lines
15 KiB
Swift
Raw Permalink Normal View History

2025-11-22 13:58:55 +01:00
import Combine
import AVFoundation
/// The settings
@objc class Settings: NSObject {
// MARK: Properties
// The notification name for changed routing options
static let routingOptionsChangedNotificationName: Notification.Name = Notification.Name(rawValue: "RoutingOptionsChanged")
/// Key for storing if the sync beta alert has been shown in the user defaults
static private let userDefaultsKeyHasShownSyncBetaAlert = "kUDDidShowICloudSynchronizationEnablingAlert"
/// Key for storing the type of action used for the bottom left main interface button in the user defaults
static private let userDefaultsKeyLeftButtonType = "LeftButtonType"
/// Key for storing the map appearance in the user defaults
static private let userDefaultsKeyMapAppearance = "MapAppearance"
/// The current distance unit
static var distanceUnit: DistanceUnit {
get {
if SettingsBridge.measurementUnits() == .imperial {
return .imperial
} else {
return .metric
}
}
set {
if newValue == .imperial {
SettingsBridge.setMeasurementUnits(.imperial)
} else {
SettingsBridge.setMeasurementUnits(.metric)
}
}
}
/// If zoom buttons should be displayed
@objc static var hasZoomButtons: Bool {
get {
return SettingsBridge.zoomButtonsEnabled()
}
set {
SettingsBridge.setZoomButtonsEnabled(newValue)
}
}
/// The type of action used for the bottom left main interface button
static var leftButtonType: LeftButtonType {
get {
if let leftButtonTypeRawValue = UserDefaults.standard.string(forKey: userDefaultsKeyLeftButtonType), let leftButtonType = LeftButtonType(rawValue: leftButtonTypeRawValue) {
return leftButtonType
}
return .help
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: userDefaultsKeyLeftButtonType)
}
}
/// If 3D buildings should be displayed
@objc static var has3dBuildings: Bool {
get {
return SettingsBridge.buildings3dViewEnabled()
}
set {
SettingsBridge.setBuildings3dViewEnabled(newValue)
}
}
/// If automatic map downloads should be enabled
@objc static var hasAutomaticDownload: Bool {
get {
return SettingsBridge.autoDownloadEnabled()
}
set {
SettingsBridge.setAutoDownloadEnabled(newValue)
}
}
/// The current mobile data policy
@objc static var mobileDataPolicy: MobileDataPolicy {
get {
let networkPolicyPermission = NetworkPolicy.shared().permission
if networkPolicyPermission == .always {
return .always
} else if networkPolicyPermission == .never {
return .never
} else {
return .ask
}
}
set {
if newValue == .always {
NetworkPolicy.shared().permission = .always
} else if newValue == .never {
NetworkPolicy.shared().permission = .never
} else {
NetworkPolicy.shared().permission = .ask
}
}
}
/// The current power saving mode
@objc static var powerSavingMode: PowerSavingMode {
get {
return PowerSavingMode(rawValue: SettingsBridge.powerManagement()) ?? .never
}
set {
SettingsBridge.setPowerManagement(newValue.rawValue)
}
}
/// If an increased font size should be used for map labels
@objc static var hasIncreasedFontsize: Bool {
get {
return SettingsBridge.largeFontSize()
}
set {
SettingsBridge.setLargeFontSize(newValue)
}
}
/// If names should be transliterated to Latin
@objc static var shouldTransliterateToLatin: Bool {
get {
return SettingsBridge.transliteration()
}
set {
SettingsBridge.setTransliteration(newValue)
}
}
/// The available languages for the map
static var availableLanguagesForMap: [MapLanguage] {
var languages = SettingsBridge.availableMapLanguages().map { language in
return MapLanguage(id: language.key, localizedName: language.value)
}.sorted()
languages.insert(MapLanguage(id: "default", localizedName: String(localized: "pref_maplanguage_local")), at: 0)
languages.insert(MapLanguage(id: "auto", localizedName: String(localized: "auto")), at: 0)
return languages
}
/// The current language for the map
static var languageForMap: MapLanguage.ID {
get {
return SettingsBridge.mapLanguageCode()
}
set {
SettingsBridge.setMapLanguageCode(newValue)
}
}
/// If the compass should be calibrated
@objc static var shouldCalibrateCompass: Bool {
get {
return SettingsBridge.compassCalibrationEnabled()
}
set {
SettingsBridge.setCompassCalibrationEnabled(newValue)
}
}
/// The current map appearance
@objc static var mapAppearance: Appearance {
get {
let mapAppearanceRawValue = UserDefaults.standard.integer(forKey: userDefaultsKeyMapAppearance)
if mapAppearanceRawValue != 0, let mapAppearance = Appearance(rawValue: mapAppearanceRawValue) {
return mapAppearance
}
return .auto
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: userDefaultsKeyMapAppearance)
ThemeManager.invalidate()
}
}
/// The current appearance
@objc static var appearance: Appearance {
get {
let theme = SettingsBridge.theme()
if theme == MWMTheme.day {
return .light
} else if theme == MWMTheme.night {
return .dark
} else {
return .auto
}
}
set {
if newValue == .light {
SettingsBridge.setTheme(MWMTheme.day)
} else if newValue == .dark {
SettingsBridge.setTheme(MWMTheme.night)
} else {
SettingsBridge.setTheme(MWMTheme.auto)
}
}
}
/// If the bookmarks should be synced via iCloud
@objc static var shouldSync: Bool {
get {
return SettingsBridge.iCLoudSynchronizationEnabled()
}
set {
SettingsBridge.setICLoudSynchronizationEnabled(newValue)
}
}
/// If the sync beta alert has been shown
@objc static var hasShownSyncBetaAlert: Bool {
get {
return UserDefaults.standard.bool(forKey: userDefaultsKeyHasShownSyncBetaAlert)
}
set {
UserDefaults.standard.set(newValue, forKey: userDefaultsKeyHasShownSyncBetaAlert)
}
}
/// The publisher for state changes of the iCloud sync
static var syncStatePublisher: AnyPublisher<SynchronizationManagerState, Never> {
iCloudSynchronizaionManager.shared.notifyObservers()
return iCloudSynchronizaionManager.shared.statePublisher
}
/// If our custom logging is enabled
@objc static var isLogging: Bool {
get {
return SettingsBridge.isFileLoggingEnabled()
}
set {
SettingsBridge.setFileLoggingEnabled(newValue)
}
}
/// The formatter for the size of the log file
@objc static var logSizeFormatter: MeasurementFormatter {
let measurementFormatter = MeasurementFormatter()
measurementFormatter.unitStyle = .medium
measurementFormatter.unitOptions = .naturalScale
return measurementFormatter
}
/// The size of the log file in bytes
@objc static var logSize: Measurement<UnitInformationStorage> {
return Measurement<UnitInformationStorage>(value: Double(SettingsBridge.logFileSize()), unit: .bytes)
}
/// If the perspective view should be used during routing
@objc static var hasPerspectiveViewWhileRouting: Bool {
get {
return SettingsBridge.perspectiveViewEnabled()
}
set {
SettingsBridge.setPerspectiveViewEnabled(newValue)
}
}
/// If auto zoom should be used during routing
@objc static var hasAutoZoomWhileRouting: Bool {
get {
return SettingsBridge.autoZoomEnabled()
}
set {
SettingsBridge.setAutoZoomEnabled(newValue)
}
}
/// If voice guidance should be provided during routing
@objc static var shouldProvideVoiceRouting: Bool {
get {
return MWMTextToSpeech.isTTSEnabled()
}
set {
MWMTextToSpeech.setTTSEnabled(newValue)
}
}
/// The available languages for voice guidance during routing
static var availableLanguagesForVoiceRouting: [VoiceRoutingLanguage] {
return MWMTextToSpeech.availableLanguages().map { language in
return VoiceRoutingLanguage(id: language.key, localizedName: language.value)
}.sorted()
}
/// The current language for voice guidance during routing
@objc static var languageForVoiceRouting: VoiceRoutingLanguage.ID {
get {
return MWMTextToSpeech.selectedLanguage()
}
set {
MWMTextToSpeech.tts().setNotificationsLocale(newValue)
}
}
/// The voice used for voice guidance during routing
@objc static var voiceForVoiceRouting: String? {
if let voice = MWMTextToSpeech.tts().voice() {
return voice.name
}
return nil
}
/// If street names should be announced in the voice guidance during routing
@objc static var shouldAnnounceStreetnamesWhileVoiceRouting: Bool {
get {
return MWMTextToSpeech.isStreetNamesTTSEnabled()
}
set {
MWMTextToSpeech.setStreetNamesTTSEnabled(newValue)
}
}
/// The current announcement of speed traps in the voice guidance during routing
@objc static var announcingSpeedTrapsWhileVoiceRouting: AnnouncingSpeedTrapsWhileVoiceRouting {
get {
return AnnouncingSpeedTrapsWhileVoiceRouting(rawValue: MWMTextToSpeech.speedCameraMode()) ?? .never
}
set {
MWMTextToSpeech.setSpeedCameraMode(newValue.rawValue)
}
}
/// If toll roads should be avoided during routing
@objc static var shouldAvoidTollRoadsWhileRouting: Bool {
get {
return RoutingOptions().avoidToll
}
set {
let routingOptions = RoutingOptions()
routingOptions.avoidToll = newValue
routingOptions.save()
NotificationCenter.default.post(name: routingOptionsChangedNotificationName, object: nil)
}
}
/// If unpaved roads should be avoided during routing
@objc static var shouldAvoidUnpavedRoadsWhileRouting: Bool {
get {
return RoutingOptions().avoidDirty
}
set {
let routingOptions = RoutingOptions()
routingOptions.avoidDirty = newValue
routingOptions.save()
NotificationCenter.default.post(name: routingOptionsChangedNotificationName, object: nil)
}
}
/// If paved roads should be avoided during routing
@objc static var shouldAvoidPavedRoadsWhileRouting: Bool {
get {
return RoutingOptions().avoidPaved
}
set {
let routingOptions = RoutingOptions()
routingOptions.avoidPaved = newValue
routingOptions.save()
NotificationCenter.default.post(name: routingOptionsChangedNotificationName, object: nil)
}
}
/// If ferries should be avoided during routing
@objc static var shouldAvoidFerriesWhileRouting: Bool {
get {
return RoutingOptions().avoidFerry
}
set {
let routingOptions = RoutingOptions()
routingOptions.avoidFerry = newValue
routingOptions.save()
NotificationCenter.default.post(name: routingOptionsChangedNotificationName, object: nil)
}
}
/// If motorways should be avoided during routing
@objc static var shouldAvoidMotorwaysWhileRouting: Bool {
get {
return RoutingOptions().avoidMotorway
}
set {
let routingOptions = RoutingOptions()
routingOptions.avoidMotorway = newValue
routingOptions.save()
NotificationCenter.default.post(name: routingOptionsChangedNotificationName, object: nil)
}
}
/// If steps should be avoided during routing
@objc static var shouldAvoidStepsWhileRouting: Bool {
get {
return RoutingOptions().avoidSteps
}
set {
let routingOptions = RoutingOptions()
routingOptions.avoidSteps = newValue
routingOptions.save()
NotificationCenter.default.post(name: routingOptionsChangedNotificationName, object: nil)
}
}
// MARK: Methods
/// Create a bookmarks backup before enabling the sync beta
/// - Parameter completionHandler: A compeltion handler, which returns if a backup has been created
static func createBookmarksBackupBecauseOfSyncBeta(completionHandler: ((_ hasCreatedBackup: Bool) -> Void)?) {
BookmarksManager.shared().shareAllCategories { status, url in
switch status {
case .success:
let window = (UIApplication.shared.connectedScenes.filter { $0.activationState == .foregroundActive }.first(where: { $0 is UIWindowScene }) as? UIWindowScene)?.keyWindow
if let viewController = window?.rootViewController?.presentedViewController {
let shareController = ActivityViewController.share(for: url, message: String(localized: "share_bookmarks_email_body")) { _, _, _, _ in
completionHandler?(true)
}
shareController.present(inParentViewController: viewController, anchorView: nil)
}
case .emptyCategory:
Toast.show(withText: String(localized: "bookmarks_error_title_share_empty"))
completionHandler?(false)
case .archiveError:
Toast.show(withText: String(localized: "dialog_routing_system_error"))
completionHandler?(false)
case .fileError:
Toast.show(withText: String(localized: "dialog_routing_system_error"))
completionHandler?(false)
}
}
}
/// Play a test audio snippet for voice guidance during routing
@objc static func playVoiceRoutingTest() {
MWMTextToSpeech.playTest()
}
}