// // ThemeManager.swift // SimpleX (iOS) // // Created by Avently on 03.06.2024. // Copyright © 2024 SimpleX Chat. All rights reserved. // import Foundation import SwiftUI import SimpleXChat class ThemeManager { struct ActiveTheme: Equatable { let name: String let base: DefaultTheme let colors: Colors let appColors: AppColors var wallpaper: AppWallpaper = AppWallpaper(background: nil, tint: nil, type: .empty) func toAppTheme() -> AppTheme { AppTheme(name: name, base: base, colors: colors, appColors: appColors, wallpaper: wallpaper) } } private static func systemDarkThemeColors() -> (Colors, DefaultTheme) { switch systemDarkThemeDefault.get() { case DefaultTheme.DARK.themeName: (DarkColorPalette, DefaultTheme.DARK) case DefaultTheme.SIMPLEX.themeName: (SimplexColorPalette, DefaultTheme.SIMPLEX) case DefaultTheme.BLACK.themeName: (BlackColorPalette, DefaultTheme.BLACK) default: (SimplexColorPalette, DefaultTheme.SIMPLEX) } } private static func nonSystemThemeName() -> String { let themeName = currentThemeDefault.get() return if themeName != DefaultTheme.SYSTEM_THEME_NAME { themeName } else { systemInDarkThemeCurrently ? systemDarkThemeDefault.get() : DefaultTheme.LIGHT.themeName } } static func defaultActiveTheme(_ appSettingsTheme: [ThemeOverrides]) -> ThemeOverrides? { let nonSystemThemeName = nonSystemThemeName() let defaultThemeId = currentThemeIdsDefault.get()[nonSystemThemeName] return appSettingsTheme.getTheme(defaultThemeId) } static func defaultActiveTheme(_ perUserTheme: ThemeModeOverrides?, _ appSettingsTheme: [ThemeOverrides]) -> ThemeModeOverride { let perUserTheme = !CurrentColors.colors.isLight ? perUserTheme?.dark : perUserTheme?.light if let perUserTheme { return perUserTheme } let defaultTheme = defaultActiveTheme(appSettingsTheme) return ThemeModeOverride(mode: CurrentColors.base.mode, colors: defaultTheme?.colors ?? ThemeColors(), wallpaper: defaultTheme?.wallpaper ?? ThemeWallpaper.from(PresetWallpaper.school.toType(CurrentColors.base), nil, nil)) } static func currentColors(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?, _ appSettingsTheme: [ThemeOverrides]) -> ActiveTheme { let themeName = currentThemeDefault.get() let nonSystemThemeName = nonSystemThemeName() let defaultTheme = defaultActiveTheme(appSettingsTheme) let baseTheme = switch nonSystemThemeName { case DefaultTheme.LIGHT.themeName: ActiveTheme(name: DefaultTheme.LIGHT.themeName, base: DefaultTheme.LIGHT, colors: LightColorPalette.clone(), appColors: LightColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.LIGHT))) case DefaultTheme.DARK.themeName: ActiveTheme(name: DefaultTheme.DARK.themeName, base: DefaultTheme.DARK, colors: DarkColorPalette.clone(), appColors: DarkColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.DARK))) case DefaultTheme.SIMPLEX.themeName: ActiveTheme(name: DefaultTheme.SIMPLEX.themeName, base: DefaultTheme.SIMPLEX, colors: SimplexColorPalette.clone(), appColors: SimplexColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.SIMPLEX))) case DefaultTheme.BLACK.themeName: ActiveTheme(name: DefaultTheme.BLACK.themeName, base: DefaultTheme.BLACK, colors: BlackColorPalette.clone(), appColors: BlackColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.BLACK))) default: ActiveTheme(name: DefaultTheme.LIGHT.themeName, base: DefaultTheme.LIGHT, colors: LightColorPalette.clone(), appColors: LightColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.LIGHT))) } let perUserTheme = baseTheme.colors.isLight ? perUserTheme?.light : perUserTheme?.dark let theme = appSettingsTheme.sameTheme(themeOverridesForType ?? perChatTheme?.type ?? perUserTheme?.type ?? defaultTheme?.wallpaper?.toAppWallpaper().type, nonSystemThemeName) ?? defaultTheme if theme == nil && perUserTheme == nil && perChatTheme == nil && themeOverridesForType == nil { return ActiveTheme(name: themeName, base: baseTheme.base, colors: baseTheme.colors, appColors: baseTheme.appColors, wallpaper: baseTheme.wallpaper) } let presetWallpaperTheme: ThemeColors? = if let themeOverridesForType, case let WallpaperType.preset(filename, _) = themeOverridesForType { PresetWallpaper.from(filename)?.colors[baseTheme.base] } else if let wallpaper = perChatTheme?.wallpaper { if let preset = wallpaper.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil } } else if let wallpaper = perUserTheme?.wallpaper { if let preset = wallpaper.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil } } else { if let preset = theme?.wallpaper?.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil } } let themeOrEmpty = theme ?? ThemeOverrides(base: baseTheme.base) let colors = themeOrEmpty.toColors(themeOrEmpty.base, perChatTheme?.colors, perUserTheme?.colors, presetWallpaperTheme) return ActiveTheme( name: themeName, base: baseTheme.base, colors: colors, appColors: themeOrEmpty.toAppColors(themeOrEmpty.base, perChatTheme?.colors, perChatTheme?.type, perUserTheme?.colors, perUserTheme?.type, presetWallpaperTheme), wallpaper: themeOrEmpty.toAppWallpaper(themeOverridesForType, perChatTheme, perUserTheme, colors.background) ) } static func currentThemeOverridesForExport(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?) -> ThemeOverrides { let current = currentColors(themeOverridesForType, perChatTheme, perUserTheme, themeOverridesDefault.get()) let wType = current.wallpaper.type let wBackground = current.wallpaper.background let wTint = current.wallpaper.tint let w: ThemeWallpaper? = if case WallpaperType.empty = wType { nil } else { ThemeWallpaper.from(wType, wBackground?.toReadableHex(), wTint?.toReadableHex()).withFilledWallpaperBase64() } return ThemeOverrides( themeId: "", base: current.base, colors: ThemeColors.from(current.colors, current.appColors), wallpaper: w ) } static func applyTheme(_ theme: String) { currentThemeDefault.set(theme) CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) AppTheme.shared.updateFromCurrentColors() let tint = UIColor(CurrentColors.colors.primary) if SceneDelegate.windowStatic?.tintColor != tint { SceneDelegate.windowStatic?.tintColor = tint } // applyNavigationBarColors(CurrentColors.toAppTheme()) } static func adjustWindowStyle() { let style = switch currentThemeDefault.get() { case DefaultTheme.LIGHT.themeName: UIUserInterfaceStyle.light case DefaultTheme.SYSTEM_THEME_NAME: UIUserInterfaceStyle.unspecified default: UIUserInterfaceStyle.dark } if SceneDelegate.windowStatic?.overrideUserInterfaceStyle != style { SceneDelegate.windowStatic?.overrideUserInterfaceStyle = style } } // static func applyNavigationBarColors(_ theme: AppTheme) { // let baseColors = switch theme.base { // case DefaultTheme.LIGHT: LightColorPaletteApp // case DefaultTheme.DARK: DarkColorPaletteApp // case DefaultTheme.SIMPLEX: SimplexColorPaletteApp // case DefaultTheme.BLACK: BlackColorPaletteApp // } // let isDefaultColor = baseColors.title == theme.appColors.title // // let title = UIColor(theme.appColors.title) // if !isDefaultColor && UINavigationBar.appearance().titleTextAttributes?.first as? UIColor != title { // UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: title] // UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: title] // } else { // UINavigationBar.appearance().titleTextAttributes = nil // UINavigationBar.appearance().largeTitleTextAttributes = nil // } // } static func changeDarkTheme(_ theme: String) { systemDarkThemeDefault.set(theme) CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) AppTheme.shared.updateFromCurrentColors() } static func saveAndApplyThemeColor(_ baseTheme: DefaultTheme, _ name: ThemeColor, _ color: Color? = nil, _ pref: CodableDefault<[ThemeOverrides]>? = nil) { let nonSystemThemeName = baseTheme.themeName let pref = pref ?? themeOverridesDefault let overrides = pref.get() let themeId = currentThemeIdsDefault.get()[nonSystemThemeName] let prevValue = overrides.getTheme(themeId) ?? ThemeOverrides(base: baseTheme) pref.set(overrides.replace(prevValue.withUpdatedColor(name, color?.toReadableHex()))) var themeIds = currentThemeIdsDefault.get() themeIds[nonSystemThemeName] = prevValue.themeId currentThemeIdsDefault.set(themeIds) applyTheme(currentThemeDefault.get()) } static func applyThemeColor(name: ThemeColor, color: Color? = nil, pref: Binding) { pref.wrappedValue = pref.wrappedValue.withUpdatedColor(name, color?.toReadableHex()) } static func saveAndApplyWallpaper(_ baseTheme: DefaultTheme, _ type: WallpaperType?, _ pref: CodableDefault<[ThemeOverrides]>?) { let nonSystemThemeName = baseTheme.themeName let pref = pref ?? themeOverridesDefault let overrides = pref.get() let theme = overrides.sameTheme(type, baseTheme.themeName) var prevValue = theme ?? ThemeOverrides(base: baseTheme) prevValue.wallpaper = if let type { if case WallpaperType.empty = type { nil as ThemeWallpaper? } else { ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint) } } else { nil } pref.set(overrides.replace(prevValue)) var themeIds = currentThemeIdsDefault.get() themeIds[nonSystemThemeName] = prevValue.themeId currentThemeIdsDefault.set(themeIds) applyTheme(currentThemeDefault.get()) } static func copyFromSameThemeOverrides(_ type: WallpaperType?, _ lowerLevelOverride: ThemeModeOverride?, _ pref: Binding) -> Bool { let overrides = themeOverridesDefault.get() let sameWallpaper: ThemeWallpaper? = if let wallpaper = lowerLevelOverride?.wallpaper, lowerLevelOverride?.type?.sameType(type) == true { wallpaper } else { overrides.sameTheme(type, CurrentColors.base.themeName)?.wallpaper } guard let sameWallpaper else { if let type { var w: ThemeWallpaper = ThemeWallpaper.from(type, nil, nil) w.scale = nil w.scaleType = nil w.background = nil w.tint = nil pref.wrappedValue = ThemeModeOverride(mode: CurrentColors.base.mode, wallpaper: w) } else { // Make an empty wallpaper to override any top level ones pref.wrappedValue = ThemeModeOverride(mode: CurrentColors.base.mode, wallpaper: ThemeWallpaper()) } return true } var type = sameWallpaper.toAppWallpaper().type if case let WallpaperType.image(filename, scale, scaleType) = type, sameWallpaper.imageFile == filename { // same image file. Needs to be copied first in order to be able to remove the file once it's not needed anymore without affecting main theme override if let filename = saveWallpaperFile(url: getWallpaperFilePath(filename)) { type = WallpaperType.image(filename, scale, scaleType) } else { logger.error("Error while copying wallpaper from global overrides to chat overrides") return false } } var prevValue = pref.wrappedValue var w = ThemeWallpaper.from(type, nil, nil) w.scale = nil w.scaleType = nil w.background = nil w.tint = nil prevValue.colors = ThemeColors() prevValue.wallpaper = w pref.wrappedValue = prevValue return true } static func applyWallpaper(_ type: WallpaperType?, _ pref: Binding) { var prevValue = pref.wrappedValue prevValue.wallpaper = if let type { ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint) } else { nil } pref.wrappedValue = prevValue } static func saveAndApplyThemeOverrides(_ theme: ThemeOverrides, _ pref: CodableDefault<[ThemeOverrides]>? = nil) { let wallpaper = theme.wallpaper?.importFromString() let nonSystemThemeName = theme.base.themeName let pref: CodableDefault<[ThemeOverrides]> = pref ?? themeOverridesDefault let overrides = pref.get() var prevValue = overrides.getTheme(nil, wallpaper?.toAppWallpaper().type, theme.base) ?? ThemeOverrides(base: theme.base) if let imageFile = prevValue.wallpaper?.imageFile { try? FileManager.default.removeItem(at: getWallpaperFilePath(imageFile)) } prevValue.base = theme.base prevValue.colors = theme.colors prevValue.wallpaper = wallpaper pref.set(overrides.replace(prevValue)) currentThemeDefault.set(nonSystemThemeName) var currentThemeIds = currentThemeIdsDefault.get() currentThemeIds[nonSystemThemeName] = prevValue.themeId currentThemeIdsDefault.set(currentThemeIds) applyTheme(nonSystemThemeName) } static func resetAllThemeColors(_ pref: CodableDefault<[ThemeOverrides]>? = nil) { let nonSystemThemeName = nonSystemThemeName() let pref: CodableDefault<[ThemeOverrides]> = pref ?? themeOverridesDefault let overrides = pref.get() guard let themeId = currentThemeIdsDefault.get()[nonSystemThemeName], var prevValue = overrides.getTheme(themeId) else { return } prevValue.colors = ThemeColors() prevValue.wallpaper?.background = nil prevValue.wallpaper?.tint = nil pref.set(overrides.replace(prevValue)) applyTheme(currentThemeDefault.get()) } static func resetAllThemeColors(_ pref: Binding) { var prevValue = pref.wrappedValue prevValue.colors = ThemeColors() prevValue.wallpaper?.background = nil prevValue.wallpaper?.tint = nil pref.wrappedValue = prevValue } static func removeTheme(_ themeId: String?) { var themes = themeOverridesDefault.get().map { $0 } themes.removeAll(where: { $0.themeId == themeId }) themeOverridesDefault.set(themes) } }