import Foundation import UIKit import Display import SwiftSignalKit import Postbox import TelegramCore import SyncCore import TelegramPresentationData import TelegramUIPreferences import ItemListUI import PresentationDataUtils import TelegramStringFormatting import AccountContext import DeviceLocationManager import Geocoding import WallpaperResources import Sunrise private enum TriggerMode { case system case none case timeBased case brightness } private enum TimeBasedManualField { case from case to } private final class ThemeAutoNightSettingsControllerArguments { let context: AccountContext let updateMode: (TriggerMode) -> Void let updateTimeBasedAutomatic: (Bool) -> Void let openTimeBasedManual: (TimeBasedManualField) -> Void let updateTimeBasedAutomaticLocation: () -> Void let updateAutomaticBrightness: (Double) -> Void let updateTheme: (PresentationThemeReference) -> Void init(context: AccountContext, updateMode: @escaping (TriggerMode) -> Void, updateTimeBasedAutomatic: @escaping (Bool) -> Void, openTimeBasedManual: @escaping (TimeBasedManualField) -> Void, updateTimeBasedAutomaticLocation: @escaping () -> Void, updateAutomaticBrightness: @escaping (Double) -> Void, updateTheme: @escaping (PresentationThemeReference) -> Void) { self.context = context self.updateMode = updateMode self.updateTimeBasedAutomatic = updateTimeBasedAutomatic self.openTimeBasedManual = openTimeBasedManual self.updateTimeBasedAutomaticLocation = updateTimeBasedAutomaticLocation self.updateAutomaticBrightness = updateAutomaticBrightness self.updateTheme = updateTheme } } private enum ThemeAutoNightSettingsControllerSection: Int32 { case mode case settings case theme } private enum ThemeAutoNightSettingsControllerEntry: ItemListNodeEntry { case modeSystem(PresentationTheme, String, Bool) case modeDisabled(PresentationTheme, String, Bool) case modeTimeBased(PresentationTheme, String, Bool) case modeBrightness(PresentationTheme, String, Bool) case settingsHeader(PresentationTheme, String) case timeBasedAutomaticLocation(PresentationTheme, String, Bool) case timeBasedAutomaticLocationValue(PresentationTheme, String, String) case timeBasedManualFrom(PresentationTheme, String, String) case timeBasedManualTo(PresentationTheme, String, String) case brightnessValue(PresentationTheme, Double) case settingInfo(PresentationTheme, String) case themeHeader(PresentationTheme, String) case themeItem(PresentationTheme, PresentationStrings, [PresentationThemeReference], [PresentationThemeReference], PresentationThemeReference, [Int64: PresentationThemeAccentColor], [Int64: TelegramWallpaper]) var section: ItemListSectionId { switch self { case .modeSystem, .modeDisabled, .modeTimeBased, .modeBrightness: return ThemeAutoNightSettingsControllerSection.mode.rawValue case .settingsHeader, .timeBasedAutomaticLocation, .timeBasedAutomaticLocationValue, .timeBasedManualFrom, .timeBasedManualTo, .brightnessValue, .settingInfo: return ThemeAutoNightSettingsControllerSection.settings.rawValue case .themeHeader, .themeItem: return ThemeAutoNightSettingsControllerSection.theme.rawValue } } var stableId: Int32 { switch self { case .modeSystem: return 0 case .modeDisabled: return 1 case .modeTimeBased: return 2 case .modeBrightness: return 3 case .settingsHeader: return 4 case .timeBasedAutomaticLocation: return 5 case .timeBasedAutomaticLocationValue: return 6 case .timeBasedManualFrom: return 7 case .timeBasedManualTo: return 8 case .brightnessValue: return 9 case .settingInfo: return 10 case .themeHeader: return 11 case .themeItem: return 12 } } static func ==(lhs: ThemeAutoNightSettingsControllerEntry, rhs: ThemeAutoNightSettingsControllerEntry) -> Bool { switch lhs { case let .modeSystem(lhsTheme, lhsTitle, lhsValue): if case let .modeSystem(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue { return true } else { return false } case let .modeDisabled(lhsTheme, lhsTitle, lhsValue): if case let .modeDisabled(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue { return true } else { return false } case let .modeTimeBased(lhsTheme, lhsTitle, lhsValue): if case let .modeTimeBased(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue { return true } else { return false } case let .modeBrightness(lhsTheme, lhsTitle, lhsValue): if case let .modeBrightness(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue { return true } else { return false } case let .settingsHeader(lhsTheme, lhsTitle): if case let .settingsHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle { return true } else { return false } case let .timeBasedAutomaticLocation(lhsTheme, lhsTitle, lhsValue): if case let .timeBasedAutomaticLocation(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue { return true } else { return false } case let .timeBasedAutomaticLocationValue(lhsTheme, lhsTitle, lhsValue): if case let .timeBasedAutomaticLocationValue(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue { return true } else { return false } case let .timeBasedManualFrom(lhsTheme, lhsTitle, lhsValue): if case let .timeBasedManualFrom(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue { return true } else { return false } case let .timeBasedManualTo(lhsTheme, lhsTitle, lhsValue): if case let .timeBasedManualTo(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue { return true } else { return false } case let .brightnessValue(lhsTheme, lhsValue): if case let .brightnessValue(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue { return true } else { return false } case let .settingInfo(lhsTheme, lhsValue): if case let .settingInfo(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue { return true } else { return false } case let .themeHeader(lhsTheme, lhsValue): if case let .themeHeader(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue { return true } else { return false } case let .themeItem(lhsTheme, lhsStrings, lhsThemes, lhsAllThemes, lhsCurrentTheme, lhsThemeAccentColors, lhsThemeChatWallpapers): if case let .themeItem(rhsTheme, rhsStrings, rhsThemes, rhsAllThemes, rhsCurrentTheme, rhsThemeAccentColors, rhsThemeChatWallpapers) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsThemes == rhsThemes, lhsAllThemes == rhsAllThemes, lhsCurrentTheme == rhsCurrentTheme, lhsThemeAccentColors == rhsThemeAccentColors, lhsThemeChatWallpapers == rhsThemeChatWallpapers { return true } else { return false } } } static func <(lhs: ThemeAutoNightSettingsControllerEntry, rhs: ThemeAutoNightSettingsControllerEntry) -> Bool { return lhs.stableId < rhs.stableId } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ThemeAutoNightSettingsControllerArguments switch self { case let .modeSystem(theme, title, value): return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateMode(.system) }) case let .modeDisabled(theme, title, value): return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateMode(.none) }) case let .modeTimeBased(theme, title, value): return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateMode(.timeBased) }) case let .modeBrightness(theme, title, value): return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateMode(.brightness) }) case let .settingsHeader(theme, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .timeBasedAutomaticLocation(theme, title, value): return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updateTimeBasedAutomatic(value) }) case let .timeBasedAutomaticLocationValue(theme, title, value): return ItemListDisclosureItem(presentationData: presentationData, icon: nil, title: title, titleColor: .accent, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { arguments.updateTimeBasedAutomaticLocation() }) case let .timeBasedManualFrom(theme, title, value): return ItemListDisclosureItem(presentationData: presentationData, icon: nil, title: title, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openTimeBasedManual(.from) }) case let .timeBasedManualTo(theme, title, value): return ItemListDisclosureItem(presentationData: presentationData, icon: nil, title: title, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openTimeBasedManual(.to) }) case let .brightnessValue(theme, value): return ThemeSettingsBrightnessItem(theme: theme, value: Int32(value * 100.0), sectionId: self.section, updated: { value in arguments.updateAutomaticBrightness(Double(value) / 100.0) }) case let .settingInfo(theme, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .themeHeader(theme, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .themeItem(theme, strings, themes, allThemes, currentTheme, themeSpecificAccentColors, themeSpecificChatWallpapers): return ThemeSettingsThemeItem(context: arguments.context, theme: theme, strings: strings, sectionId: self.section, themes: themes, allThemes: allThemes, displayUnsupported: false, themeSpecificAccentColors: themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, currentTheme: currentTheme, updatedTheme: { theme in arguments.updateTheme(theme) }, contextAction: nil) } } } private func themeAutoNightSettingsControllerEntries(theme: PresentationTheme, strings: PresentationStrings, settings: PresentationThemeSettings, switchSetting: AutomaticThemeSwitchSetting, availableThemes: [PresentationThemeReference], dateTimeFormat: PresentationDateTimeFormat) -> [ThemeAutoNightSettingsControllerEntry] { var entries: [ThemeAutoNightSettingsControllerEntry] = [] let activeTriggerMode: TriggerMode switch switchSetting.trigger { case .system: if #available(iOSApplicationExtension 13.0, iOS 13.0, *) { activeTriggerMode = .system } else { activeTriggerMode = .none } case .explicitNone: activeTriggerMode = .none case .timeBased: activeTriggerMode = .timeBased case .brightness: activeTriggerMode = .brightness } if #available(iOSApplicationExtension 13.0, iOS 13.0, *) { entries.append(.modeSystem(theme, strings.AutoNightTheme_System, activeTriggerMode == .system)) } entries.append(.modeDisabled(theme, strings.AutoNightTheme_Disabled, activeTriggerMode == .none)) entries.append(.modeTimeBased(theme, strings.AutoNightTheme_Scheduled, activeTriggerMode == .timeBased)) entries.append(.modeBrightness(theme, strings.AutoNightTheme_Automatic, activeTriggerMode == .brightness)) switch switchSetting.trigger { case .system, .explicitNone: break case let .timeBased(setting): entries.append(.settingsHeader(theme, strings.AutoNightTheme_ScheduleSection)) var automaticLocation = false if case .automatic = setting { automaticLocation = true } entries.append(.timeBasedAutomaticLocation(theme, strings.AutoNightTheme_UseSunsetSunrise, automaticLocation)) switch setting { case let .automatic(latitude, longitude, localizedName): let calculator = EDSunriseSet(date: Date(), timezone: TimeZone.current, latitude: latitude, longitude: longitude)! let sunset = roundTimeToDay(Int32(calculator.sunset.timeIntervalSince1970)) let sunrise = roundTimeToDay(Int32(calculator.sunrise.timeIntervalSince1970)) entries.append(.timeBasedAutomaticLocationValue(theme, strings.AutoNightTheme_UpdateLocation, localizedName)) if sunset != 0 || sunrise != 0 { entries.append(.settingInfo(theme, strings.AutoNightTheme_LocationHelp(stringForMessageTimestamp(timestamp: sunset, dateTimeFormat: dateTimeFormat, local: false), stringForMessageTimestamp(timestamp: sunrise, dateTimeFormat: dateTimeFormat, local: false)).0)) } case let .manual(fromSeconds, toSeconds): entries.append(.timeBasedManualFrom(theme, strings.AutoNightTheme_ScheduledFrom, stringForMessageTimestamp(timestamp: fromSeconds, dateTimeFormat: dateTimeFormat, local: false))) entries.append(.timeBasedManualTo(theme, strings.AutoNightTheme_ScheduledTo, stringForMessageTimestamp(timestamp: toSeconds, dateTimeFormat: dateTimeFormat, local: false))) } case let .brightness(threshold): entries.append(.settingsHeader(theme, strings.AutoNightTheme_AutomaticSection)) entries.append(.brightnessValue(theme, threshold)) entries.append(.settingInfo(theme, strings.AutoNightTheme_AutomaticHelp("\(Int(threshold * 100.0))").0.replacingOccurrences(of: "%%", with: "%"))) } switch switchSetting.trigger { case .explicitNone: break case .system, .timeBased, .brightness: entries.append(.themeHeader(theme, strings.AutoNightTheme_PreferredTheme)) let generalThemes: [PresentationThemeReference] = availableThemes.filter { reference in if case let .cloud(theme) = reference { return theme.theme.settings == nil } else { return true } } entries.append(.themeItem(theme, strings, generalThemes, availableThemes, switchSetting.theme, settings.themeSpecificAccentColors, settings.themeSpecificChatWallpapers)) } return entries } private func roundTimeToDay(_ timestamp: Int32) -> Int32 { let calendar = Calendar.current let offset = 0 let components = calendar.dateComponents([.hour, .minute, .second], from: Date(timeIntervalSince1970: Double(timestamp + Int32(offset)))) return Int32(components.hour! * 60 * 60 + components.minute! * 60 + components.second!) } private func areSettingsValid(_ settings: AutomaticThemeSwitchSetting) -> Bool { switch settings.trigger { case .system, .explicitNone, .brightness: return true case let .timeBased(setting): switch setting { case let .automatic(latitude, longitude, _): if !latitude.isZero || !longitude.isZero { return true } else { return false } case .manual: return true } } } public func themeAutoNightSettingsController(context: AccountContext) -> ViewController { var presentControllerImpl: ((ViewController) -> Void)? let actionsDisposable = DisposableSet() let updateAutomaticBrightnessDisposable = MetaDisposable() let stagingSettingsPromise = ValuePromise(nil) let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings]) let updateLocationDisposable = MetaDisposable() actionsDisposable.add(updateLocationDisposable) let updateSettings: (@escaping (AutomaticThemeSwitchSetting) -> AutomaticThemeSwitchSetting) -> Void = { f in let _ = (combineLatest(stagingSettingsPromise.get(), sharedData) |> take(1) |> deliverOnMainQueue).start(next: { stagingSettings, sharedData in let settings = (sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings] as? PresentationThemeSettings) ?? PresentationThemeSettings.defaultSettings let updated = f(stagingSettings ?? settings.automaticThemeSwitchSetting) stagingSettingsPromise.set(updated) if areSettingsValid(updated) { let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in var current = current current.automaticThemeSwitchSetting = updated return current }).start() } }) } let forceUpdateLocation: () -> Void = { let locationCoordinates = Signal<(Double, Double), NoError> { subscriber in return context.sharedContext.locationManager!.push(mode: DeviceLocationMode.precise, updated: { coordinate in subscriber.putNext((coordinate.latitude, coordinate.longitude)) subscriber.putCompletion() }) } let geocodedLocation = locationCoordinates |> mapToSignal { coordinates -> Signal<(Double, Double, String), NoError> in return reverseGeocodeLocation(latitude: coordinates.0, longitude: coordinates.1) |> map { locality in return (coordinates.0, coordinates.1, locality?.city ?? "") } } let disposable = (geocodedLocation |> take(1) |> deliverOnMainQueue).start(next: { location in updateSettings { settings in var settings = settings if case let .timeBased(setting) = settings.trigger, case .automatic = setting { settings.trigger = .timeBased(setting: .automatic(latitude: location.0, longitude: location.1, localizedName: location.2)) } return settings } }) updateLocationDisposable.set(disposable) } let arguments = ThemeAutoNightSettingsControllerArguments(context: context, updateMode: { mode in var updateLocation = false updateSettings { settings in var settings = settings switch mode { case .system: settings.trigger = .system case .none: settings.trigger = .explicitNone case .timeBased: if case .timeBased = settings.trigger { } else { settings.trigger = .timeBased(setting: .automatic(latitude: 0.0, longitude: 0.0, localizedName: "")) updateLocation = true } case .brightness: if case .brightness = settings.trigger { } else { settings.trigger = .brightness(threshold: 0.2) } } if updateLocation { forceUpdateLocation() } return settings } }, updateTimeBasedAutomatic: { value in var updateLocation = false updateSettings { settings in var settings = settings if case let .timeBased(setting) = settings.trigger { switch setting { case .automatic: if !value { settings.trigger = .timeBased(setting: .manual(fromSeconds: 22 * 60 * 60, toSeconds: 9 * 60 * 60)) } case .manual: if value { settings.trigger = .timeBased(setting: .automatic(latitude: 0.0, longitude: 0.0, localizedName: "")) updateLocation = true } } } if updateLocation { forceUpdateLocation() } return settings } }, openTimeBasedManual: { field in var currentValue: Int32 switch field { case .from: currentValue = 22 * 60 * 60 case .to: currentValue = 9 * 60 * 60 } updateSettings { settings in let settings = settings switch settings.trigger { case let .timeBased(setting): switch setting { case let .manual(fromSeconds, toSeconds): switch field { case .from: currentValue = fromSeconds case .to: currentValue = toSeconds } default: break } default: break } presentControllerImpl?(ThemeAutoNightTimeSelectionActionSheet(context: context, currentValue: currentValue, applyValue: { value in guard let value = value else { return } updateSettings { settings in var settings = settings switch settings.trigger { case let .timeBased(setting): switch setting { case var .manual(fromSeconds, toSeconds): switch field { case .from: fromSeconds = value case .to: toSeconds = value } settings.trigger = .timeBased(setting: .manual(fromSeconds: fromSeconds, toSeconds: toSeconds)) default: break } default: break } return settings } })) return settings } }, updateTimeBasedAutomaticLocation: { forceUpdateLocation() }, updateAutomaticBrightness: { value in updateAutomaticBrightnessDisposable.set((Signal.complete() |> delay(0.1, queue: Queue.mainQueue())).start(completed: { updateSettings { settings in var settings = settings switch settings.trigger { case .brightness: settings.trigger = .brightness(threshold: max(0.0, min(1.0, value))) default: break } return settings } })) }, updateTheme: { theme in guard let presentationTheme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: theme) else { return } let resolvedWallpaper: Signal if case let .file(file) = presentationTheme.chat.defaultWallpaper, file.id == 0 { resolvedWallpaper = cachedWallpaper(account: context.account, slug: file.slug, settings: file.settings) |> map { wallpaper -> TelegramWallpaper? in return wallpaper?.wallpaper } } else { resolvedWallpaper = .single(nil) } let _ = (resolvedWallpaper |> mapToSignal { resolvedWallpaper -> Signal in var updatedTheme = theme if case let .cloud(info) = theme { updatedTheme = .cloud(PresentationCloudTheme(theme: info.theme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: info.theme.isCreator ? context.account.id : nil)) } updateSettings { settings in var settings = settings settings.theme = updatedTheme return settings } return .complete() }).start() }) let cloudThemes = Promise<[TelegramTheme]>() let updatedCloudThemes = telegramThemes(postbox: context.account.postbox, network: context.account.network, accountManager: context.sharedContext.accountManager) cloudThemes.set(updatedCloudThemes) let signal = combineLatest(context.sharedContext.presentationData |> deliverOnMainQueue, sharedData |> deliverOnMainQueue, cloudThemes.get() |> deliverOnMainQueue, stagingSettingsPromise.get() |> deliverOnMainQueue) |> map { presentationData, sharedData, cloudThemes, stagingSettings -> (ItemListControllerState, (ItemListNodeState, Any)) in let settings = (sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings] as? PresentationThemeSettings) ?? PresentationThemeSettings.defaultSettings let defaultThemes: [PresentationThemeReference] = [.builtin(.night), .builtin(.nightAccent)] let cloudThemes: [PresentationThemeReference] = cloudThemes.map { .cloud(PresentationCloudTheme(theme: $0, resolvedWallpaper: nil, creatorAccountId: $0.isCreator ? context.account.id : nil)) } var availableThemes = defaultThemes availableThemes.append(contentsOf: cloudThemes) let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.AutoNightTheme_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeAutoNightSettingsControllerEntries(theme: presentationData.theme, strings: presentationData.strings, settings: settings, switchSetting: stagingSettings ?? settings.automaticThemeSwitchSetting, availableThemes: availableThemes, dateTimeFormat: presentationData.dateTimeFormat), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } let controller = ItemListController(context: context, state: signal) controller.alwaysSynchronous = true presentControllerImpl = { [weak controller] c in controller?.present(c, in: .window(.root)) } return controller }