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 { 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 { return Measurement(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() } }