mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-11 03:35:55 +00:00
1190 lines
51 KiB
Swift
1190 lines
51 KiB
Swift
//
|
|
// AppearanceSettings.swift
|
|
// SimpleX (iOS)
|
|
//
|
|
// Created by Evgeny on 03/08/2022.
|
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SimpleXChat
|
|
import Yams
|
|
|
|
let colorModesLocalized: [LocalizedStringKey] = ["System", "Light", "Dark"]
|
|
let colorModesNames: [DefaultThemeMode?] = [nil, DefaultThemeMode.light, DefaultThemeMode.dark]
|
|
|
|
let darkThemesLocalized: [LocalizedStringKey] = ["Dark", "SimpleX", "Black"]
|
|
let darkThemesNames: [String] = [DefaultTheme.DARK.themeName, DefaultTheme.SIMPLEX.themeName, DefaultTheme.BLACK.themeName]
|
|
|
|
let darkThemesWithoutBlackLocalized: [LocalizedStringKey] = ["Dark", "SimpleX"]
|
|
let darkThemesWithoutBlackNames: [String] = [DefaultTheme.DARK.themeName, DefaultTheme.SIMPLEX.themeName]
|
|
|
|
let appSettingsURL = URL(string: UIApplication.openSettingsURLString)!
|
|
|
|
struct AppearanceSettings: View {
|
|
@EnvironmentObject var m: ChatModel
|
|
@Environment(\.colorScheme) var colorScheme
|
|
@EnvironmentObject var sceneDelegate: SceneDelegate
|
|
@EnvironmentObject var theme: AppTheme
|
|
@State private var iconLightTapped = false
|
|
@State private var iconDarkTapped = false
|
|
@State private var colorMode: DefaultThemeMode? = {
|
|
if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME { nil as DefaultThemeMode? } else { CurrentColors.base.mode }
|
|
}()
|
|
@State private var darkModeTheme: String = UserDefaults.standard.string(forKey: DEFAULT_SYSTEM_DARK_THEME) ?? DefaultTheme.DARK.themeName
|
|
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner
|
|
@AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var chatItemRoundness = defaultChatItemRoundness
|
|
@AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var chatItemTail = true
|
|
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
|
|
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
|
|
|
@State var themeUserDestination: (Int64, ThemeModeOverrides?)? = {
|
|
if let currentUser = ChatModel.shared.currentUser, let uiThemes = currentUser.uiThemes, uiThemes.preferredMode(!CurrentColors.colors.isLight) != nil {
|
|
(currentUser.userId, uiThemes)
|
|
} else {
|
|
nil
|
|
}
|
|
}()
|
|
|
|
@State var perUserTheme: ThemeModeOverride = {
|
|
ChatModel.shared.currentUser?.uiThemes?.preferredMode(!CurrentColors.colors.isLight) ?? ThemeModeOverride(mode: CurrentColors.base.mode)
|
|
}()
|
|
|
|
@State var showImageImporter: Bool = false
|
|
@State var customizeThemeIsOpen: Bool = false
|
|
|
|
var body: some View {
|
|
VStack{
|
|
List {
|
|
Section(String("Language")) {
|
|
HStack {
|
|
Text(currentLanguage)
|
|
Spacer()
|
|
Button("Change") {
|
|
UIApplication.shared.open(appSettingsURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
Section("Chat list") {
|
|
Toggle("Reachable chat toolbar", isOn: $oneHandUI)
|
|
Picker("Toolbar opacity", selection: $toolbarMaterial) {
|
|
ForEach(ToolbarMaterial.allCases, id: \.rawValue) { tm in
|
|
Text(tm.text).tag(tm.rawValue)
|
|
}
|
|
}
|
|
.frame(height: 36)
|
|
}
|
|
|
|
Section {
|
|
ThemeDestinationPicker(themeUserDestination: $themeUserDestination, themeUserDest: themeUserDestination?.0, customizeThemeIsOpen: $customizeThemeIsOpen)
|
|
|
|
WallpaperPresetSelector(
|
|
selectedWallpaper: theme.wallpaper.type,
|
|
currentColors: currentColors,
|
|
onChooseType: onChooseType
|
|
)
|
|
.padding(.bottom, 10)
|
|
.listRowInsets(.init())
|
|
.listRowBackground(Color.clear)
|
|
.modifier(WallpaperImporter(showImageImporter: $showImageImporter, onChooseImage: { image in
|
|
if let filename = saveWallpaperFile(image: image) {
|
|
if themeUserDestination == nil, case let WallpaperType.image(filename, _, _) = theme.wallpaper.type {
|
|
removeWallpaperFile(fileName: filename)
|
|
} else if let type = perUserTheme.type, case let WallpaperType.image(filename, _, _) = type {
|
|
removeWallpaperFile(fileName: filename)
|
|
}
|
|
onTypeChange(WallpaperType.image(filename, 1, WallpaperScaleType.fill))
|
|
}
|
|
}))
|
|
|
|
if case let WallpaperType.image(filename, _, _) = theme.wallpaper.type, (themeUserDestination == nil || perUserTheme.wallpaper?.imageFile != nil) {
|
|
Button {
|
|
if themeUserDestination == nil {
|
|
let defaultActiveTheme = ThemeManager.defaultActiveTheme(themeOverridesDefault.get())
|
|
ThemeManager.saveAndApplyWallpaper(theme.base, nil, themeOverridesDefault)
|
|
ThemeManager.removeTheme(defaultActiveTheme?.themeId)
|
|
removeWallpaperFile(fileName: filename)
|
|
} else {
|
|
removeUserThemeModeOverrides($themeUserDestination, $perUserTheme)
|
|
}
|
|
saveThemeToDatabase(themeUserDestination)
|
|
} label: {
|
|
Text("Remove image")
|
|
.foregroundColor(theme.colors.primary)
|
|
}
|
|
.listRowBackground(Color.clear)
|
|
}
|
|
|
|
Picker("Color mode", selection: $colorMode) {
|
|
ForEach(Array(colorModesNames.enumerated()), id: \.element) { index, mode in
|
|
Text(colorModesLocalized[index])
|
|
}
|
|
}
|
|
.frame(height: 36)
|
|
Picker("Dark mode colors", selection: $darkModeTheme) {
|
|
if theme.base == .BLACK || themeOverridesDefault.get().contains(where: { $0.base == .BLACK }) {
|
|
ForEach(Array(darkThemesNames.enumerated()), id: \.element) { index, darkTheme in
|
|
Text(darkThemesLocalized[index])
|
|
}
|
|
} else {
|
|
ForEach(Array(darkThemesWithoutBlackNames.enumerated()), id: \.element) { index, darkTheme in
|
|
Text(darkThemesLocalized[index])
|
|
}
|
|
}
|
|
}
|
|
.frame(height: 36)
|
|
|
|
NavigationLink {
|
|
let userId = themeUserDestination?.0
|
|
if let userId {
|
|
UserWallpaperEditorSheet(userId: userId)
|
|
.onAppear {
|
|
customizeThemeIsOpen = true
|
|
}
|
|
} else {
|
|
CustomizeThemeView(onChooseType: onChooseType)
|
|
.navigationTitle("Customize theme")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
.onAppear {
|
|
customizeThemeIsOpen = true
|
|
}
|
|
}
|
|
} label: {
|
|
Text("Customize theme")
|
|
}
|
|
} header: {
|
|
Text("Themes")
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
.onChange(of: profileImageCornerRadius) { cornerRadius in
|
|
profileImageCornerRadiusGroupDefault.set(cornerRadius)
|
|
saveThemeToDatabase(nil)
|
|
}
|
|
.onChange(of: colorMode) { mode in
|
|
guard let mode else {
|
|
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
|
|
return
|
|
}
|
|
if case DefaultThemeMode.light = mode {
|
|
ThemeManager.applyTheme(DefaultTheme.LIGHT.themeName)
|
|
} else if case DefaultThemeMode.dark = mode {
|
|
ThemeManager.applyTheme(systemDarkThemeDefault.get())
|
|
}
|
|
}
|
|
.onChange(of: darkModeTheme) { darkTheme in
|
|
ThemeManager.changeDarkTheme(darkTheme)
|
|
if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME {
|
|
ThemeManager.applyTheme(currentThemeDefault.get())
|
|
} else if currentThemeDefault.get() != DefaultTheme.LIGHT.themeName {
|
|
ThemeManager.applyTheme(systemDarkThemeDefault.get())
|
|
}
|
|
}
|
|
|
|
Section(header: Text("Message shape").foregroundColor(theme.colors.secondary)) {
|
|
HStack {
|
|
Text("Corner")
|
|
Slider(value: $chatItemRoundness, in: 0...1, step: 0.05)
|
|
}
|
|
Toggle("Tail", isOn: $chatItemTail)
|
|
}
|
|
|
|
Section(header: Text("Profile images").foregroundColor(theme.colors.secondary)) {
|
|
HStack(spacing: 16) {
|
|
if let img = m.currentUser?.image, img != "" {
|
|
ProfileImage(imageStr: img, size: 60)
|
|
} else {
|
|
clipProfileImage(Image(colorScheme == .light ? "icon-dark" : "icon-light"), size: 60, radius: profileImageCornerRadius)
|
|
}
|
|
|
|
Slider(
|
|
value: $profileImageCornerRadius,
|
|
in: 0...50,
|
|
step: 2.5
|
|
)
|
|
}
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
|
|
Section(header: Text("App icon").foregroundColor(theme.colors.secondary)) {
|
|
HStack {
|
|
updateAppIcon(image: "icon-light", icon: nil, tapped: $iconLightTapped)
|
|
Spacer().frame(width: 16)
|
|
updateAppIcon(image: "icon-dark", icon: "DarkAppIcon", tapped: $iconDarkTapped)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.onAppear {
|
|
customizeThemeIsOpen = false
|
|
}
|
|
}
|
|
|
|
private func updateThemeUserDestination() {
|
|
if let dest = themeUserDestination {
|
|
var (userId, themes) = dest
|
|
themes = themes ?? ThemeModeOverrides()
|
|
if case DefaultThemeMode.light = perUserTheme.mode {
|
|
themes?.light = perUserTheme
|
|
} else {
|
|
themes?.dark = perUserTheme
|
|
}
|
|
themeUserDestination = (userId, themes)
|
|
}
|
|
}
|
|
|
|
private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool {
|
|
if themeUserDestination == nil {
|
|
ThemeManager.saveAndApplyWallpaper(theme.base, type, themeOverridesDefault)
|
|
} else {
|
|
var wallpaperFiles = Set([perUserTheme.wallpaper?.imageFile])
|
|
_ = ThemeManager.copyFromSameThemeOverrides(type, nil, $perUserTheme)
|
|
wallpaperFiles.remove(perUserTheme.wallpaper?.imageFile)
|
|
wallpaperFiles.forEach(removeWallpaperFile)
|
|
updateThemeUserDestination()
|
|
}
|
|
saveThemeToDatabase(themeUserDestination)
|
|
return true
|
|
}
|
|
|
|
private func onTypeChange(_ type: WallpaperType?) {
|
|
if themeUserDestination == nil {
|
|
ThemeManager.saveAndApplyWallpaper(theme.base, type, themeOverridesDefault)
|
|
} else {
|
|
ThemeManager.applyWallpaper(type, $perUserTheme)
|
|
updateThemeUserDestination()
|
|
}
|
|
saveThemeToDatabase(themeUserDestination)
|
|
}
|
|
|
|
private func currentColors(_ type: WallpaperType?) -> ThemeManager.ActiveTheme {
|
|
// If applying for :
|
|
// - all themes: no overrides needed
|
|
// - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected
|
|
let perUserOverride: ThemeModeOverrides? = themeUserDestination == nil
|
|
? nil
|
|
: theme.wallpaper.type.sameType(type)
|
|
? m.currentUser?.uiThemes
|
|
: nil
|
|
return ThemeManager.currentColors(type, nil, perUserOverride, themeOverridesDefault.get())
|
|
}
|
|
|
|
private func onChooseType(_ type: WallpaperType?) {
|
|
// don't have image in parent or already selected wallpaper with custom image
|
|
if let type, case WallpaperType.image = type {
|
|
if case WallpaperType.image = theme.wallpaper.type, themeUserDestination?.1 != nil {
|
|
showImageImporter = true
|
|
} else if currentColors(type).wallpaper.type.image == nil {
|
|
showImageImporter = true
|
|
} else if currentColors(type).wallpaper.type.image != nil, case WallpaperType.image = theme.wallpaper.type, themeUserDestination == nil {
|
|
showImageImporter = true
|
|
} else if themeUserDestination == nil {
|
|
onTypeChange(currentColors(type).wallpaper.type)
|
|
} else {
|
|
_ = onTypeCopyFromSameTheme(currentColors(type).wallpaper.type)
|
|
}
|
|
} else if (themeUserDestination != nil && themeUserDestination?.1?.preferredMode(!CurrentColors.colors.isLight)?.type != type) || theme.wallpaper.type != type {
|
|
_ = onTypeCopyFromSameTheme(type)
|
|
} else {
|
|
onTypeChange(type)
|
|
}
|
|
}
|
|
|
|
private var currentLanguage: String {
|
|
let lang = Bundle.main.preferredLocalizations.first ?? "en"
|
|
return Locale.current.localizedString(forIdentifier: lang)?.localizedCapitalized ?? lang
|
|
}
|
|
|
|
private func updateAppIcon(image: String, icon: String?, tapped: Binding<Bool>) -> some View {
|
|
Image(image)
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 60, height: 60)
|
|
.onTapGesture {
|
|
UIApplication.shared.setAlternateIconName(icon) { err in
|
|
if let err = err {
|
|
logger.error("\(err.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
._onButtonGesture { tapped.wrappedValue = $0 } perform: {}
|
|
.overlay(tapped.wrappedValue ? Color.secondary : Color.clear)
|
|
.cornerRadius(13.5)
|
|
}
|
|
}
|
|
|
|
enum ToolbarMaterial: String, CaseIterable {
|
|
case bar
|
|
case ultraThin
|
|
case thin
|
|
case regular
|
|
case thick
|
|
case ultraThick
|
|
|
|
static func material(_ s: String) -> Material {
|
|
ToolbarMaterial(rawValue: s)?.material ?? Material.bar
|
|
}
|
|
|
|
static let defaultMaterial: String = ToolbarMaterial.regular.rawValue
|
|
|
|
var material: Material {
|
|
switch self {
|
|
case .bar: .bar
|
|
case .ultraThin: .ultraThin
|
|
case .thin: .thin
|
|
case .regular: .regular
|
|
case .thick: .thick
|
|
case .ultraThick: .ultraThick
|
|
}
|
|
}
|
|
|
|
var text: String {
|
|
switch self {
|
|
case .bar: "System"
|
|
case .ultraThin: "Ultra thin"
|
|
case .thin: "Thin"
|
|
case .regular: "Regular"
|
|
case .thick: "Thick"
|
|
case .ultraThick: "Ultra thick"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ChatThemePreview: View {
|
|
@EnvironmentObject var theme: AppTheme
|
|
var base: DefaultTheme
|
|
var wallpaperType: WallpaperType?
|
|
var backgroundColor: Color?
|
|
var tintColor: Color?
|
|
var withMessages: Bool = true
|
|
|
|
var body: some View {
|
|
let themeBackgroundColor = theme.colors.background
|
|
let backgroundColor = backgroundColor ?? wallpaperType?.defaultBackgroundColor(theme.base, theme.colors.background)
|
|
let tintColor = tintColor ?? wallpaperType?.defaultTintColor(theme.base)
|
|
let view = VStack {
|
|
if withMessages {
|
|
let alice = ChatItem.getSample(1, CIDirection.directRcv, Date.now, NSLocalizedString("Good afternoon!", comment: "message preview"))
|
|
let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir))
|
|
HStack {
|
|
ChatItemView(chat: Chat.sampleData, chatItem: alice)
|
|
.modifier(ChatItemClipped(alice, tailVisible: true))
|
|
Spacer()
|
|
}
|
|
HStack {
|
|
Spacer()
|
|
ChatItemView(chat: Chat.sampleData, chatItem: bob)
|
|
.modifier(ChatItemClipped(bob, tailVisible: true))
|
|
.frame(alignment: .trailing)
|
|
}
|
|
} else {
|
|
Rectangle().fill(.clear)
|
|
}
|
|
}
|
|
.padding(.vertical, 10)
|
|
.padding(.horizontal, 16)
|
|
.frame(maxWidth: .infinity)
|
|
|
|
if let wallpaperType, let wallpaperImage = wallpaperType.image, let backgroundColor, let tintColor {
|
|
view.modifier(ChatViewBackground(image: wallpaperImage, imageType: wallpaperType, background: backgroundColor, tint: tintColor))
|
|
} else {
|
|
view.background(themeBackgroundColor)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct WallpaperPresetSelector: View {
|
|
@EnvironmentObject var theme: AppTheme
|
|
var selectedWallpaper: WallpaperType?
|
|
var activeBackgroundColor: Color? = nil
|
|
var activeTintColor: Color? = nil
|
|
var currentColors: (WallpaperType?) -> ThemeManager.ActiveTheme
|
|
var onChooseType: (WallpaperType?) -> Void
|
|
let width: Double = 80
|
|
let height: Double = 80
|
|
let backgrounds = PresetWallpaper.allCases
|
|
|
|
private let cornerRadius: Double = 22.5
|
|
|
|
var baseTheme: DefaultTheme { theme.base }
|
|
|
|
var body: some View {
|
|
VStack {
|
|
ChatThemePreview(
|
|
base: theme.base,
|
|
wallpaperType: selectedWallpaper,
|
|
backgroundColor: activeBackgroundColor ?? theme.wallpaper.background,
|
|
tintColor: activeTintColor ?? theme.wallpaper.tint
|
|
)
|
|
.environmentObject(currentColors(selectedWallpaper).toAppTheme())
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack {
|
|
BackgroundItem(nil)
|
|
ForEach(backgrounds, id: \.self) { background in
|
|
BackgroundItem(background)
|
|
}
|
|
OwnBackgroundItem(selectedWallpaper)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func plus() -> some View {
|
|
Image(systemName: "plus")
|
|
.tint(theme.colors.primary)
|
|
.frame(width: 25, height: 25)
|
|
}
|
|
|
|
func BackgroundItem(_ background: PresetWallpaper?) -> some View {
|
|
let checked = (background == nil && (selectedWallpaper == nil || selectedWallpaper?.isEmpty == true)) || selectedWallpaper?.samePreset(other: background) == true
|
|
let type = background?.toType(baseTheme, checked ? selectedWallpaper?.scale : nil)
|
|
let overrides = currentColors(type).toAppTheme()
|
|
return ZStack {
|
|
if let type {
|
|
ChatThemePreview(
|
|
base: baseTheme,
|
|
wallpaperType: type,
|
|
backgroundColor: checked ? activeBackgroundColor ?? overrides.wallpaper.background : overrides.wallpaper.background,
|
|
tintColor: checked ? activeTintColor ?? overrides.wallpaper.tint : overrides.wallpaper.tint,
|
|
withMessages: false
|
|
)
|
|
.environmentObject(overrides)
|
|
} else {
|
|
Rectangle().fill(overrides.colors.background)
|
|
}
|
|
}
|
|
.frame(width: CGFloat(width), height: CGFloat(height))
|
|
.clipShape(RoundedRectangle(cornerRadius: width / 100 * cornerRadius))
|
|
.overlay(RoundedRectangle(cornerRadius: width / 100 * cornerRadius)
|
|
.strokeBorder(checked ? theme.colors.primary.opacity(0.8) : theme.colors.onBackground.opacity(isInDarkTheme() ? 0.2 : 0.1), lineWidth: 1)
|
|
)
|
|
.onTapGesture {
|
|
onChooseType(background?.toType(baseTheme))
|
|
}
|
|
}
|
|
|
|
func OwnBackgroundItem(_ type: WallpaperType?) -> some View {
|
|
let overrides = currentColors(WallpaperType.image("", nil, nil))
|
|
let appWallpaper = overrides.wallpaper
|
|
let backgroundColor = appWallpaper.background
|
|
let tintColor = appWallpaper.tint
|
|
let wallpaperImage = appWallpaper.type.image
|
|
let checked = if let type, case WallpaperType.image = type, wallpaperImage != nil { true } else { false }
|
|
let borderColor = if let type, case WallpaperType.image = type { theme.colors.primary.opacity(0.8) } else { theme.colors.onBackground.opacity(0.1) }
|
|
return ZStack {
|
|
if checked || wallpaperImage != nil {
|
|
ChatThemePreview(
|
|
base: baseTheme,
|
|
wallpaperType: checked ? type : appWallpaper.type,
|
|
backgroundColor: checked ? activeBackgroundColor ?? backgroundColor : backgroundColor,
|
|
tintColor: checked ? activeTintColor ?? tintColor : tintColor,
|
|
withMessages: false
|
|
)
|
|
.environmentObject(currentColors(type).toAppTheme())
|
|
} else {
|
|
plus()
|
|
}
|
|
}
|
|
.frame(width: width, height: height)
|
|
.clipShape(RoundedRectangle(cornerRadius: width / 100 * cornerRadius))
|
|
.overlay(RoundedRectangle(cornerRadius: width / 100 * cornerRadius)
|
|
.strokeBorder(borderColor, lineWidth: 1)
|
|
)
|
|
.onTapGesture {
|
|
onChooseType(WallpaperType.image("", nil, nil))
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CustomizeThemeView: View {
|
|
@EnvironmentObject var theme: AppTheme
|
|
var onChooseType: (WallpaperType?) -> Void
|
|
@State private var showFileImporter = false
|
|
|
|
var body: some View {
|
|
List {
|
|
let wallpaperImage = theme.wallpaper.type.image
|
|
let wallpaperType = theme.wallpaper.type
|
|
let baseTheme = theme.base
|
|
|
|
let editColor: (ThemeColor) -> Binding<Color> = { name in
|
|
editColorBinding(
|
|
name: name,
|
|
wallpaperType: wallpaperType,
|
|
wallpaperImage: wallpaperImage,
|
|
theme: theme,
|
|
onColorChange: { color in
|
|
updateBackendTask.cancel()
|
|
updateBackendTask = Task {
|
|
if (try? await Task.sleep(nanoseconds: 200_000000)) != nil {
|
|
ThemeManager.saveAndApplyThemeColor(baseTheme, name, color)
|
|
saveThemeToDatabase(nil)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
WallpaperPresetSelector(
|
|
selectedWallpaper: wallpaperType,
|
|
currentColors: { type in
|
|
ThemeManager.currentColors(type, nil, nil, themeOverridesDefault.get())
|
|
},
|
|
onChooseType: onChooseType
|
|
)
|
|
.listRowInsets(.init())
|
|
.listRowBackground(Color.clear)
|
|
|
|
if case let WallpaperType.image(filename, _, _) = theme.wallpaper.type {
|
|
Button {
|
|
let defaultActiveTheme = ThemeManager.defaultActiveTheme(themeOverridesDefault.get())
|
|
ThemeManager.saveAndApplyWallpaper(baseTheme, nil, themeOverridesDefault)
|
|
ThemeManager.removeTheme(defaultActiveTheme?.themeId)
|
|
removeWallpaperFile(fileName: filename)
|
|
saveThemeToDatabase(nil)
|
|
} label: {
|
|
Text("Remove image")
|
|
.foregroundColor(theme.colors.primary)
|
|
}
|
|
.listRowBackground(Color.clear)
|
|
}
|
|
|
|
Section {
|
|
WallpaperSetupView(
|
|
wallpaperType: wallpaperType,
|
|
base: baseTheme,
|
|
initialWallpaper: theme.wallpaper,
|
|
editColor: { name in
|
|
editColor(name)
|
|
},
|
|
onTypeChange: { type in
|
|
ThemeManager.saveAndApplyWallpaper(baseTheme, type, themeOverridesDefault)
|
|
updateBackendTask.cancel()
|
|
updateBackendTask = Task {
|
|
if (try? await Task.sleep(nanoseconds: 200_000000)) != nil {
|
|
saveThemeToDatabase(nil)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
} header: {
|
|
Text("Chat colors")
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
|
|
CustomizeThemeColorsSection(editColor: editColor)
|
|
|
|
let currentOverrides = ThemeManager.defaultActiveTheme(themeOverridesDefault.get())
|
|
let canResetColors = theme.base.hasChangedAnyColor(currentOverrides)
|
|
if canResetColors {
|
|
Button {
|
|
ThemeManager.resetAllThemeColors()
|
|
saveThemeToDatabase(nil)
|
|
} label: {
|
|
Text("Reset colors").font(.callout).foregroundColor(theme.colors.primary)
|
|
}
|
|
}
|
|
|
|
ImportExportThemeSection(showFileImporter: $showFileImporter, perChat: nil, perUser: nil)
|
|
}
|
|
.modifier(
|
|
ThemeImporter(isPresented: $showFileImporter) { theme in
|
|
ThemeManager.saveAndApplyThemeOverrides(theme)
|
|
saveThemeToDatabase(nil)
|
|
}
|
|
)
|
|
/// When changing app theme, user overrides are hidden. User overrides will be returned back after closing Appearance screen, see ThemeDestinationPicker()
|
|
.interactiveDismissDisabled(true)
|
|
}
|
|
}
|
|
|
|
struct ImportExportThemeSection: View {
|
|
@EnvironmentObject var theme: AppTheme
|
|
@Binding var showFileImporter: Bool
|
|
var perChat: ThemeModeOverride?
|
|
var perUser: ThemeModeOverrides?
|
|
|
|
var body: some View {
|
|
Section {
|
|
Button {
|
|
let overrides = ThemeManager.currentThemeOverridesForExport(nil, perChat, perUser)
|
|
do {
|
|
let encoded = try encodeThemeOverrides(overrides)
|
|
var lines = encoded.split(separator: "\n")
|
|
// Removing theme id without using custom serializer or data class
|
|
lines.remove(at: 0)
|
|
let theme = lines.joined(separator: "\n")
|
|
let tempUrl = getTempFilesDirectory().appendingPathComponent("simplex.theme")
|
|
try? FileManager.default.removeItem(at: tempUrl)
|
|
if FileManager.default.createFile(atPath: tempUrl.path, contents: theme.data(using: .utf8)) {
|
|
showShareSheet(items: [tempUrl])
|
|
}
|
|
} catch {
|
|
AlertManager.shared.showAlertMsg(title: "Error", message: "Error exporting theme: \(error.localizedDescription)")
|
|
}
|
|
} label: {
|
|
Text("Export theme").foregroundColor(theme.colors.primary)
|
|
}
|
|
Button {
|
|
showFileImporter = true
|
|
} label: {
|
|
Text("Import theme").foregroundColor(theme.colors.primary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ThemeImporter: ViewModifier {
|
|
@Binding var isPresented: Bool
|
|
var save: (ThemeOverrides) -> Void
|
|
|
|
func body(content: Content) -> some View {
|
|
content.fileImporter(
|
|
isPresented: $isPresented,
|
|
allowedContentTypes: [.data/*.plainText*/],
|
|
allowsMultipleSelection: false
|
|
) { result in
|
|
if case let .success(files) = result, let fileURL = files.first {
|
|
do {
|
|
var fileSize: Int? = nil
|
|
if fileURL.startAccessingSecurityScopedResource() {
|
|
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
|
|
fileSize = resourceValues.fileSize
|
|
}
|
|
if let fileSize = fileSize,
|
|
// Same as Android/desktop
|
|
fileSize <= 5_500_000 {
|
|
if let string = try? String(contentsOf: fileURL, encoding: .utf8), let theme: ThemeOverrides = decodeYAML("themeId: \(UUID().uuidString)\n" + string) {
|
|
save(theme)
|
|
logger.error("Saved theme from file")
|
|
} else {
|
|
logger.error("Error decoding theme file")
|
|
}
|
|
fileURL.stopAccessingSecurityScopedResource()
|
|
} else {
|
|
fileURL.stopAccessingSecurityScopedResource()
|
|
let prettyMaxFileSize = ByteCountFormatter.string(fromByteCount: 5_500_000, countStyle: .binary)
|
|
AlertManager.shared.showAlertMsg(
|
|
title: "Large file!",
|
|
message: "Currently maximum supported file size is \(prettyMaxFileSize)."
|
|
)
|
|
}
|
|
} catch {
|
|
logger.error("Appearance fileImporter error \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct UserWallpaperEditorSheet: View {
|
|
@Environment(\.dismiss) var dismiss
|
|
@EnvironmentObject var theme: AppTheme
|
|
@State var userId: Int64
|
|
@State private var globalThemeUsed: Bool = false
|
|
|
|
@State private var themes = ChatModel.shared.currentUser?.uiThemes ?? ThemeModeOverrides()
|
|
|
|
var body: some View {
|
|
let preferred = themes.preferredMode(!theme.colors.isLight)
|
|
let initialTheme = preferred ?? ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
|
UserWallpaperEditor(
|
|
initialTheme: initialTheme,
|
|
themeModeOverride: initialTheme,
|
|
applyToMode: themes.light == themes.dark ? nil : initialTheme.mode,
|
|
globalThemeUsed: $globalThemeUsed,
|
|
save: { applyToMode, newTheme in
|
|
updateBackendTask.cancel()
|
|
updateBackendTask = Task {
|
|
let themes = ChatModel.shared.currentUser?.uiThemes ?? ThemeModeOverrides()
|
|
let initialTheme = themes.preferredMode(!theme.colors.isLight) ?? ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
|
|
|
await save(
|
|
applyToMode,
|
|
newTheme,
|
|
themes,
|
|
userId,
|
|
realtimeUpdate:
|
|
initialTheme.wallpaper?.preset != newTheme?.wallpaper?.preset ||
|
|
initialTheme.wallpaper?.imageFile != newTheme?.wallpaper?.imageFile ||
|
|
initialTheme.wallpaper?.scale != newTheme?.wallpaper?.scale ||
|
|
initialTheme.wallpaper?.scaleType != newTheme?.wallpaper?.scaleType
|
|
)
|
|
}
|
|
}
|
|
)
|
|
.navigationTitle("Profile theme")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
.onAppear {
|
|
globalThemeUsed = preferred == nil
|
|
}
|
|
.onChange(of: theme.base.mode) { _ in
|
|
globalThemeUsed = (ChatModel.shared.currentUser?.uiThemes ?? ThemeModeOverrides()).preferredMode(!theme.colors.isLight) == nil
|
|
}
|
|
.onChange(of: ChatModel.shared.currentUser?.userId) { _ in
|
|
dismiss()
|
|
}
|
|
}
|
|
|
|
private func save(
|
|
_ applyToMode: DefaultThemeMode?,
|
|
_ newTheme: ThemeModeOverride?,
|
|
_ themes: ThemeModeOverrides?,
|
|
_ userId: Int64,
|
|
realtimeUpdate: Bool
|
|
) async {
|
|
let unchangedThemes: ThemeModeOverrides = themes ?? ThemeModeOverrides()
|
|
var wallpaperFiles = Set([unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile])
|
|
var changedThemes: ThemeModeOverrides? = unchangedThemes
|
|
let light: ThemeModeOverride? = if let newTheme {
|
|
ThemeModeOverride(mode: DefaultThemeMode.light, colors: newTheme.colors, wallpaper: newTheme.wallpaper?.withFilledWallpaperPath())
|
|
} else {
|
|
nil
|
|
}
|
|
let dark: ThemeModeOverride? = if let newTheme {
|
|
ThemeModeOverride(mode: DefaultThemeMode.dark, colors: newTheme.colors, wallpaper: newTheme.wallpaper?.withFilledWallpaperPath())
|
|
} else {
|
|
nil
|
|
}
|
|
|
|
if let applyToMode {
|
|
switch applyToMode {
|
|
case DefaultThemeMode.light:
|
|
changedThemes?.light = light
|
|
case DefaultThemeMode.dark:
|
|
changedThemes?.dark = dark
|
|
}
|
|
} else {
|
|
changedThemes?.light = light
|
|
changedThemes?.dark = dark
|
|
}
|
|
if changedThemes?.light != nil || changedThemes?.dark != nil {
|
|
let light = changedThemes?.light
|
|
let dark = changedThemes?.dark
|
|
let currentMode = CurrentColors.base.mode
|
|
// same image file for both modes, copy image to make them as different files
|
|
if var light, var dark, let lightWallpaper = light.wallpaper, let darkWallpaper = dark.wallpaper, let lightImageFile = lightWallpaper.imageFile, let darkImageFile = darkWallpaper.imageFile, lightWallpaper.imageFile == darkWallpaper.imageFile {
|
|
let imageFile = if currentMode == DefaultThemeMode.light {
|
|
darkImageFile
|
|
} else {
|
|
lightImageFile
|
|
}
|
|
let filePath = saveWallpaperFile(url: getWallpaperFilePath(imageFile))
|
|
if currentMode == DefaultThemeMode.light {
|
|
dark.wallpaper?.imageFile = filePath
|
|
changedThemes = ThemeModeOverrides(light: changedThemes?.light, dark: dark)
|
|
} else {
|
|
light.wallpaper?.imageFile = filePath
|
|
changedThemes = ThemeModeOverrides(light: light, dark: changedThemes?.dark)
|
|
}
|
|
}
|
|
} else {
|
|
changedThemes = nil
|
|
}
|
|
wallpaperFiles.remove(changedThemes?.light?.wallpaper?.imageFile)
|
|
wallpaperFiles.remove(changedThemes?.dark?.wallpaper?.imageFile)
|
|
wallpaperFiles.forEach(removeWallpaperFile)
|
|
|
|
let oldThemes = ChatModel.shared.currentUser?.uiThemes
|
|
let changedThemesConstant = changedThemes
|
|
if realtimeUpdate {
|
|
await MainActor.run {
|
|
ChatModel.shared.updateCurrentUserUiThemes(uiThemes: changedThemesConstant)
|
|
}
|
|
}
|
|
do {
|
|
try await Task.sleep(nanoseconds: 200_000000)
|
|
} catch {
|
|
return
|
|
}
|
|
if !realtimeUpdate {
|
|
await MainActor.run {
|
|
ChatModel.shared.updateCurrentUserUiThemes(uiThemes: changedThemesConstant)
|
|
}
|
|
}
|
|
|
|
if await !apiSetUserUIThemes(userId: userId, themes: changedThemesConstant) {
|
|
await MainActor.run {
|
|
// If failed to apply for some reason return the old themes
|
|
ChatModel.shared.updateCurrentUserUiThemes(uiThemes: oldThemes)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ThemeDestinationPicker: View {
|
|
@EnvironmentObject var m: ChatModel
|
|
@EnvironmentObject var theme: AppTheme
|
|
@Binding var themeUserDestination: (Int64, ThemeModeOverrides?)?
|
|
@State var themeUserDest: Int64?
|
|
@Binding var customizeThemeIsOpen: Bool
|
|
|
|
var body: some View {
|
|
let values = [(nil, NSLocalizedString("All profiles", comment: "profile dropdown"))] + m.users.filter { $0.user.activeUser }.map { ($0.user.userId, $0.user.chatViewName)}
|
|
|
|
if values.contains(where: { (userId, text) in userId == themeUserDestination?.0 }) {
|
|
Picker("Apply to", selection: $themeUserDest) {
|
|
ForEach(values, id: \.0) { (_, text) in
|
|
Text(text)
|
|
}
|
|
}
|
|
.frame(height: 36)
|
|
.onChange(of: themeUserDest) { userId in
|
|
themeUserDest = userId
|
|
if let userId {
|
|
themeUserDestination = (userId, m.users.first { $0.user.userId == userId }?.user.uiThemes)
|
|
} else {
|
|
themeUserDestination = nil
|
|
}
|
|
if let userId, userId != m.currentUser?.userId {
|
|
changeActiveUser(userId, viewPwd: nil)
|
|
}
|
|
}
|
|
.onChange(of: themeUserDestination == nil) { isNil in
|
|
if isNil {
|
|
// Easiest way to hide per-user customization.
|
|
// Otherwise, it would be needed to make global variable and to use it everywhere for making a decision to include these overrides into active theme constructing or not
|
|
m.currentUser?.uiThemes = nil
|
|
} else {
|
|
m.updateCurrentUserUiThemes(uiThemes: m.users.first(where: { $0.user.userId == m.currentUser?.userId })?.user.uiThemes)
|
|
}
|
|
}
|
|
.onDisappear {
|
|
// Skip when Appearance screen is not hidden yet
|
|
if customizeThemeIsOpen { return }
|
|
// Restore user overrides from stored list of users
|
|
m.updateCurrentUserUiThemes(uiThemes: m.users.first(where: { $0.user.userId == m.currentUser?.userId })?.user.uiThemes)
|
|
themeUserDestination = if let currentUser = m.currentUser, let uiThemes = currentUser.uiThemes {
|
|
(currentUser.userId, uiThemes)
|
|
} else {
|
|
nil
|
|
}
|
|
}
|
|
} else {
|
|
EmptyView()
|
|
.onAppear {
|
|
themeUserDestination = nil
|
|
themeUserDest = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CustomizeThemeColorsSection: View {
|
|
@EnvironmentObject var theme: AppTheme
|
|
var editColor: (ThemeColor) -> Binding<Color>
|
|
|
|
var body: some View {
|
|
Section {
|
|
picker(.primary, editColor)
|
|
picker(.primaryVariant, editColor)
|
|
picker(.secondary, editColor)
|
|
picker(.secondaryVariant, editColor)
|
|
picker(.background, editColor)
|
|
picker(.surface, editColor)
|
|
//picker(.title, editColor)
|
|
picker(.primaryVariant2, editColor)
|
|
} header: {
|
|
Text("Interface colors")
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
func editColorBinding(name: ThemeColor, wallpaperType: WallpaperType?, wallpaperImage: Image?, theme: AppTheme, onColorChange: @escaping (Color?) -> Void) -> Binding<Color> {
|
|
Binding(get: {
|
|
let baseTheme = theme.base
|
|
let wallpaperBackgroundColor = theme.wallpaper.background ?? wallpaperType?.defaultBackgroundColor(baseTheme, theme.colors.background) ?? Color.clear
|
|
let wallpaperTintColor = theme.wallpaper.tint ?? wallpaperType?.defaultTintColor(baseTheme) ?? Color.clear
|
|
return switch name {
|
|
case ThemeColor.wallpaperBackground: wallpaperBackgroundColor
|
|
case ThemeColor.wallpaperTint: wallpaperTintColor
|
|
case ThemeColor.primary: theme.colors.primary
|
|
case ThemeColor.primaryVariant: theme.colors.primaryVariant
|
|
case ThemeColor.secondary: theme.colors.secondary
|
|
case ThemeColor.secondaryVariant: theme.colors.secondaryVariant
|
|
case ThemeColor.background: theme.colors.background
|
|
case ThemeColor.surface: theme.colors.surface
|
|
case ThemeColor.title: theme.appColors.title
|
|
case ThemeColor.primaryVariant2: theme.appColors.primaryVariant2
|
|
case ThemeColor.sentMessage: theme.appColors.sentMessage
|
|
case ThemeColor.sentQuote: theme.appColors.sentQuote
|
|
case ThemeColor.receivedMessage: theme.appColors.receivedMessage
|
|
case ThemeColor.receivedQuote: theme.appColors.receivedQuote
|
|
}
|
|
}, set: onColorChange)
|
|
}
|
|
|
|
struct WallpaperSetupView: View {
|
|
var wallpaperType: WallpaperType?
|
|
var base: DefaultTheme
|
|
var initialWallpaper: AppWallpaper?
|
|
var editColor: (ThemeColor) -> Binding<Color>
|
|
var onTypeChange: (WallpaperType?) -> Void
|
|
|
|
var body: some View {
|
|
if let wallpaperType, case let WallpaperType.image(_, _, scaleType) = wallpaperType {
|
|
let wallpaperScaleType = if let scaleType {
|
|
scaleType
|
|
} else if let initialWallpaper, case let WallpaperType.image(_, _, scaleType) = initialWallpaper.type, let scaleType {
|
|
scaleType
|
|
} else {
|
|
WallpaperScaleType.fill
|
|
}
|
|
WallpaperScaleTypeChooser(wallpaperScaleType: Binding.constant(wallpaperScaleType), wallpaperType: wallpaperType, onTypeChange: onTypeChange)
|
|
}
|
|
|
|
|
|
if let wallpaperType, wallpaperType.isPreset {
|
|
WallpaperScaleChooser(wallpaperScale: Binding.constant(initialWallpaper?.type.scale ?? 1), wallpaperType: wallpaperType, onTypeChange: onTypeChange)
|
|
} else if let wallpaperType, case let WallpaperType.image(_, _, scaleType) = wallpaperType, scaleType == WallpaperScaleType.repeat {
|
|
WallpaperScaleChooser(wallpaperScale: Binding.constant(initialWallpaper?.type.scale ?? 1), wallpaperType: wallpaperType, onTypeChange: onTypeChange)
|
|
}
|
|
|
|
if wallpaperType?.isPreset == true || wallpaperType?.isImage == true {
|
|
picker(.wallpaperBackground, editColor)
|
|
picker(.wallpaperTint, editColor)
|
|
}
|
|
|
|
picker(.sentMessage, editColor)
|
|
picker(.sentQuote, editColor)
|
|
picker(.receivedMessage, editColor)
|
|
picker(.receivedQuote, editColor)
|
|
|
|
}
|
|
|
|
private struct WallpaperScaleChooser: View {
|
|
@Binding var wallpaperScale: Float
|
|
var wallpaperType: WallpaperType?
|
|
var onTypeChange: (WallpaperType?) -> Void
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Text("\(wallpaperScale)".prefix(4))
|
|
.frame(width: 40, height: 36, alignment: .leading)
|
|
Slider(
|
|
value: Binding(get: { wallpaperScale }, set: { scale in
|
|
if let wallpaperType, case let WallpaperType.preset(filename, _) = wallpaperType {
|
|
onTypeChange(WallpaperType.preset(filename, Float("\(scale)".prefix(9))))
|
|
} else if let wallpaperType, case let WallpaperType.image(filename, _, scaleType) = wallpaperType {
|
|
onTypeChange(WallpaperType.image(filename, Float("\(scale)".prefix(9)), scaleType))
|
|
}
|
|
}),
|
|
in: 0.5...2,
|
|
step: 0.0000001
|
|
)
|
|
.frame(height: 36)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct WallpaperScaleTypeChooser: View {
|
|
@Binding var wallpaperScaleType: WallpaperScaleType
|
|
var wallpaperType: WallpaperType?
|
|
var onTypeChange: (WallpaperType?) -> Void
|
|
|
|
var body: some View {
|
|
Picker("Scale", selection: Binding(get: { wallpaperScaleType }, set: { scaleType in
|
|
if let wallpaperType, case let WallpaperType.image(filename, scale, _) = wallpaperType {
|
|
onTypeChange(WallpaperType.image(filename, scale, scaleType))
|
|
}
|
|
})) {
|
|
ForEach(Array(WallpaperScaleType.allCases), id: \.self) { type in
|
|
Text(type.text)
|
|
}
|
|
}
|
|
.frame(height: 36)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct picker: View {
|
|
var name: ThemeColor
|
|
@State var color: Color
|
|
var editColor: (ThemeColor) -> Binding<Color>
|
|
// Prevent a race between setting a color here and applying externally changed color to the binding
|
|
@State private var lastColorUpdate: Date = .now
|
|
|
|
init(_ name: ThemeColor, _ editColor: @escaping (ThemeColor) -> Binding<Color>) {
|
|
self.name = name
|
|
self.color = editColor(name).wrappedValue
|
|
self.editColor = editColor
|
|
}
|
|
|
|
var body: some View {
|
|
ColorPickerView(name: name, selection: $color)
|
|
.onChange(of: color) { newColor in
|
|
let editedColor = editColor(name)
|
|
if editedColor.wrappedValue != newColor {
|
|
editedColor.wrappedValue = newColor
|
|
lastColorUpdate = .now
|
|
}
|
|
}
|
|
.onChange(of: editColor(name).wrappedValue) { newValue in
|
|
// Allows to update underlying color in the picker when color changed externally, for example, by reseting colors of a theme or changing the theme
|
|
if lastColorUpdate < Date.now - 1 && newValue != color {
|
|
color = newValue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ColorPickerView: View {
|
|
var name: ThemeColor
|
|
@State var selection: Binding<Color>
|
|
|
|
var body: some View {
|
|
let supportsOpacity = switch name {
|
|
case .wallpaperTint: true
|
|
case .sentMessage: true
|
|
case .sentQuote: true
|
|
case .receivedMessage: true
|
|
case .receivedQuote: true
|
|
default: UIColor(selection.wrappedValue).cgColor.alpha < 1
|
|
}
|
|
ColorPicker(name.text, selection: selection, supportsOpacity: supportsOpacity)
|
|
}
|
|
}
|
|
|
|
struct WallpaperImporter: ViewModifier {
|
|
@Binding var showImageImporter: Bool
|
|
var onChooseImage: (UIImage) -> Void
|
|
|
|
func body(content: Content) -> some View {
|
|
content.sheet(isPresented: $showImageImporter) {
|
|
// LALAL TODO: limit by 5 mb
|
|
LibraryMediaListPicker(addMedia: { onChooseImage($0.uiImage) }, selectionLimit: 1, filter: .images, finishedPreprocessing: { }) { itemsSelected in
|
|
await MainActor.run {
|
|
showImageImporter = false
|
|
}
|
|
}
|
|
}
|
|
// content.fileImporter(
|
|
// isPresented: $showImageImporter,
|
|
// allowedContentTypes: [.image],
|
|
// allowsMultipleSelection: false
|
|
// ) { result in
|
|
// if case let .success(files) = result, let fileURL = files.first {
|
|
// do {
|
|
// var fileSize: Int? = nil
|
|
// if fileURL.startAccessingSecurityScopedResource() {
|
|
// let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
|
|
// fileSize = resourceValues.fileSize
|
|
// }
|
|
// fileURL.stopAccessingSecurityScopedResource()
|
|
// if let fileSize = fileSize,
|
|
// // Same as Android/desktop
|
|
// fileSize <= 5_000_000,
|
|
// let image = UIImage(contentsOfFile: fileURL.path){
|
|
// onChooseImage(image)
|
|
// } else {
|
|
// let prettyMaxFileSize = ByteCountFormatter.string(fromByteCount: 5_500_000, countStyle: .binary)
|
|
// AlertManager.shared.showAlertMsg(
|
|
// title: "Large file!",
|
|
// message: "Currently maximum supported file size is \(prettyMaxFileSize)."
|
|
// )
|
|
// }
|
|
// } catch {
|
|
// logger.error("Appearance fileImporter error \(error.localizedDescription)")
|
|
// }
|
|
// }
|
|
// }
|
|
}
|
|
}
|
|
|
|
|
|
/// deprecated. Remove in 2025
|
|
func getUIAccentColorDefault() -> CGColor {
|
|
let defs = UserDefaults.standard
|
|
return CGColor(
|
|
red: defs.double(forKey: DEFAULT_ACCENT_COLOR_RED),
|
|
green: defs.double(forKey: DEFAULT_ACCENT_COLOR_GREEN),
|
|
blue: defs.double(forKey: DEFAULT_ACCENT_COLOR_BLUE),
|
|
alpha: 1
|
|
)
|
|
}
|
|
|
|
private var updateBackendTask: Task = Task {}
|
|
|
|
private func saveThemeToDatabase(_ themeUserDestination: (Int64, ThemeModeOverrides?)?) {
|
|
let m = ChatModel.shared
|
|
let oldThemes = m.currentUser?.uiThemes
|
|
if let themeUserDestination {
|
|
DispatchQueue.main.async {
|
|
// Update before save to make it work seamless
|
|
m.updateCurrentUserUiThemes(uiThemes: themeUserDestination.1)
|
|
}
|
|
}
|
|
Task {
|
|
if themeUserDestination == nil {
|
|
do {
|
|
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
|
} catch {
|
|
logger.error("Error saving settings: \(error)")
|
|
}
|
|
} else if let themeUserDestination, await !apiSetUserUIThemes(userId: themeUserDestination.0, themes: themeUserDestination.1) {
|
|
// If failed to apply for some reason return the old themes
|
|
m.updateCurrentUserUiThemes(uiThemes: oldThemes)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func removeUserThemeModeOverrides(_ themeUserDestination: Binding<(Int64, ThemeModeOverrides?)?>, _ perUserTheme: Binding<ThemeModeOverride>) {
|
|
guard let dest = themeUserDestination.wrappedValue else { return }
|
|
perUserTheme.wrappedValue = ThemeModeOverride(mode: CurrentColors.base.mode)
|
|
themeUserDestination.wrappedValue = (dest.0, nil)
|
|
var wallpaperFilesToDelete: [String] = []
|
|
if let type = ChatModel.shared.currentUser?.uiThemes?.light?.type, case let WallpaperType.image(filename, _, _) = type {
|
|
wallpaperFilesToDelete.append(filename)
|
|
}
|
|
if let type = ChatModel.shared.currentUser?.uiThemes?.dark?.type, case let WallpaperType.image(filename, _, _) = type {
|
|
wallpaperFilesToDelete.append(filename)
|
|
}
|
|
wallpaperFilesToDelete.forEach(removeWallpaperFile)
|
|
}
|
|
|
|
private func decodeYAML<T: Decodable>(_ string: String) -> T? {
|
|
do {
|
|
return try YAMLDecoder().decode(T.self, from: string)
|
|
} catch {
|
|
logger.error("Error decoding YAML: \(error)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private func encodeThemeOverrides(_ value: ThemeOverrides) throws -> String {
|
|
let encoder = YAMLEncoder()
|
|
encoder.options = YAMLEncoder.Options(sequenceStyle: .block, mappingStyle: .block, newLineScalarStyle: .doubleQuoted)
|
|
|
|
guard var node = try Yams.compose(yaml: try encoder.encode(value)) else {
|
|
throw RuntimeError("Error while composing a node from object")
|
|
}
|
|
node["base"]?.scalar?.style = .doubleQuoted
|
|
|
|
ThemeColors.CodingKeys.allCases.forEach { key in
|
|
node["colors"]?[key.stringValue]?.scalar?.style = .doubleQuoted
|
|
}
|
|
|
|
ThemeWallpaper.CodingKeys.allCases.forEach { key in
|
|
if case .scale = key {
|
|
// let number be without quotes
|
|
} else {
|
|
node["wallpaper"]?[key.stringValue]?.scalar?.style = .doubleQuoted
|
|
}
|
|
}
|
|
return try Yams.serialize(node: node)
|
|
}
|
|
|
|
/// deprecated. Remove in 2025
|
|
func getUserInterfaceStyleDefault() -> UIUserInterfaceStyle {
|
|
switch UserDefaults.standard.integer(forKey: DEFAULT_USER_INTERFACE_STYLE) {
|
|
case 1: return .light
|
|
case 2: return .dark
|
|
default: return .unspecified
|
|
}
|
|
}
|
|
|
|
struct AppearanceSettings_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
AppearanceSettings()
|
|
}
|
|
}
|