ios: wallpapers (#4304)

* ios: wallpapers

* theme selection

* applied theme colors and preset wallpaper

* more places with background

* one more

* accent color

* defaults

* rename

* background

* no change to cell color

* unneeded

* changes

* no global tint

* defaults

* removed unneeded class

* for merging
This commit is contained in:
Stanislav Dmitrenko
2024-06-20 15:51:29 +07:00
committed by GitHub
parent 1573b7af13
commit b8bf5871fb
91 changed files with 2321 additions and 206 deletions
+4 -2
View File
@@ -148,13 +148,15 @@ class AppDelegate: NSObject, UIApplicationDelegate {
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
var window: UIWindow?
static var windowStatic: UIWindow?
var windowScene: UIWindowScene?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
UITableView.appearance().backgroundColor = .clear
guard let windowScene = scene as? UIWindowScene else { return }
self.windowScene = windowScene
window = windowScene.keyWindow
window?.tintColor = UIColor(cgColor: getUIAccentColorDefault())
window?.overrideUserInterfaceStyle = getUserInterfaceStyleDefault()
SceneDelegate.windowStatic = windowScene.keyWindow
ThemeManager.applyTheme(currentThemeDefault.get())
}
}
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "wallpaper_cats@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "wallpaper_flowers@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "wallpaper_hearts@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "wallpaper_kids@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "wallpaper_school@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "wallpaper_travel@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

+20 -1
View File
@@ -14,6 +14,8 @@ struct ContentView: View {
@ObservedObject var alertManager = AlertManager.shared
@ObservedObject var callController = CallController.shared
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
@EnvironmentObject var sceneDelegate: SceneDelegate
var contentAccessAuthenticationExtended: Bool
@@ -51,6 +53,16 @@ struct ContentView: View {
}
var body: some View {
if #available(iOS 16.0, *) {
allViews()
.scrollContentBackground(.hidden)
} else {
// on iOS 15 scroll view background disabled in SceneDelegate
allViews()
}
}
@ViewBuilder func allViews() -> some View {
ZStack {
let showCallArea = chatModel.activeCall != nil && chatModel.activeCall?.callState != .waitCapabilities && chatModel.activeCall?.callState != .invitationAccepted
// contentView() has to be in a single branch, so that enabling authentication doesn't trigger re-rendering and close settings.
@@ -96,6 +108,8 @@ struct ContentView: View {
initializationView()
}
}
//.tint(theme.colors.primary)
.background(theme.colors.background)
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
.sheet(isPresented: $showSettings) {
SettingsView(showSettings: $showSettings)
@@ -138,6 +152,11 @@ struct ContentView: View {
break
}
}
.onChange(of: colorScheme) { scheme in
if sceneDelegate.window?.overrideUserInterfaceStyle == .unspecified {
reactOnDarkThemeChanges(scheme == .dark)
}
}
}
@ViewBuilder private func contentView() -> some View {
@@ -224,7 +243,7 @@ struct ContentView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity )
.background(
Rectangle()
.fill(.background)
.fill(theme.colors.background)
)
}
+1
View File
@@ -39,6 +39,7 @@ struct SimpleXApp: App {
// so that it's computed by the time view renders, and not on event after rendering
ContentView(contentAccessAuthenticationExtended: !authenticationExpired())
.environmentObject(chatModel)
.environmentObject(AppTheme.shared)
.onOpenURL { url in
logger.debug("ContentView.onOpenURL: \(url)")
chatModel.appOpenUrl = url
+37
View File
@@ -0,0 +1,37 @@
//
// Color.swift
// SimpleX (iOS)
//
// Created by Avently on 05.06.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import Foundation
import SwiftUI
let Purple200 = Color(0xFFBB86FC)
let Purple500 = Color(0xFF6200EE)
let Purple700 = Color(0xFF3700B3)
let Teal200 = Color(0xFF03DAC5)
let Gray = Color(0x22222222)
let Indigo = Color(0xFF9966FF)
let SimplexBlue = Color(0, 136, 255, a: 255)
let SimplexGreen = Color(77, 218, 103, a: 255)
let SecretColor = Color(0x40808080)
let LightGray = Color(241, 242, 246, a: 255)
let DarkGray = Color(43, 44, 46, a: 255)
let HighOrLowlight = Color(139, 135, 134, a: 255)
let MessagePreviewDark = Color(179, 175, 174, a: 255)
let MessagePreviewLight = Color(49, 45, 44, a: 255)
let ToolbarLight = Color(220, 220, 220, a: 12)
let ToolbarDark = Color(80, 80, 80, a: 12)
let SettingsSecondaryLight = Color(200, 196, 195, a: 90)
let GroupDark = Color(80, 80, 80, a: 60)
let IncomingCallLight = Color(239, 237, 236, a: 255)
let WarningOrange = Color(255, 127, 0, a: 255)
let WarningYellow = Color(255, 192, 0, a: 255)
let FileLight = Color(183, 190, 199, a: 255)
let FileDark = Color(101, 101, 106, a: 255)
var MenuTextColor: Color { if isInDarkTheme() { AppTheme.shared.colors.onBackground.opacity(0.8) } else { Color.black } }
var NoteFolderIconColor: Color { AppTheme.shared.appColors.primaryVariant2 }
+848
View File
@@ -0,0 +1,848 @@
//
// Theme.swift
// SimpleX (iOS)
//
// Created by Avently on 03.06.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import Foundation
import SimpleXChat
import SwiftUI
enum DefaultTheme: String, Codable {
case LIGHT
case DARK
case SIMPLEX
case BLACK
static let SYSTEM_THEME_NAME: String = "SYSTEM"
var themeName: String { self.rawValue }
var mode: DefaultThemeMode {
self == .LIGHT
? DefaultThemeMode.light
: DefaultThemeMode.dark
}
func hasChangedAnyColor(_ overrides: ThemeOverrides?) -> Bool {
if let overrides {
overrides.colors != ThemeColors() || overrides.wallpaper != nil && (overrides.wallpaper?.background != nil || overrides.wallpaper?.tint != nil)
} else {
false
}
}
}
enum DefaultThemeMode: String, Codable {
case light
case dark
}
class Colors: ObservableObject, NSCopying {
@Published var primary: Color
@Published var primaryVariant: Color
@Published var secondary: Color
@Published var secondaryVariant: Color
@Published var background: Color
@Published var surface: Color
@Published var error: Color
@Published var onBackground: Color
@Published var onSurface: Color
@Published var isLight: Bool
init(primary: Color, primaryVariant: Color, secondary: Color, secondaryVariant: Color, background: Color, surface: Color, error: Color, onBackground: Color, onSurface: Color, isLight: Bool) {
self.primary = primary
self.primaryVariant = primaryVariant
self.secondary = secondary
self.secondaryVariant = secondaryVariant
self.background = background
self.surface = surface
self.error = error
self.onBackground = onBackground
self.onSurface = onSurface
self.isLight = isLight
}
func copy(with zone: NSZone? = nil) -> Any {
Colors(primary: self.primary, primaryVariant: self.primaryVariant, secondary: self.secondary, secondaryVariant: self.secondaryVariant, background: self.background, surface: self.surface, error: self.error, onBackground: self.onBackground, onSurface: self.onSurface, isLight: self.isLight)
}
func clone() -> Colors { copy() as! Colors }
}
class AppColors: ObservableObject, NSCopying {
@Published var title: Color
@Published var primaryVariant2: Color
@Published var sentMessage: Color
@Published var sentQuote: Color
@Published var receivedMessage: Color
@Published var receivedQuote: Color
init(title: Color, primaryVariant2: Color, sentMessage: Color, sentQuote: Color, receivedMessage: Color, receivedQuote: Color) {
self.title = title
self.primaryVariant2 = primaryVariant2
self.sentMessage = sentMessage
self.sentQuote = sentQuote
self.receivedMessage = receivedMessage
self.receivedQuote = receivedQuote
}
func copy(with zone: NSZone? = nil) -> Any {
AppColors(title: self.title, primaryVariant2: self.primaryVariant2, sentMessage: self.sentMessage, sentQuote: self.sentQuote, receivedMessage: self.receivedMessage, receivedQuote: self.receivedQuote)
}
func clone() -> AppColors { copy() as! AppColors }
func copy(
title: Color?,
primaryVariant2: Color?,
sentMessage: Color?,
sentQuote: Color?,
receivedMessage: Color?,
receivedQuote: Color?
) -> AppColors {
AppColors(
title: title ?? self.title,
primaryVariant2: primaryVariant2 ?? self.primaryVariant2,
sentMessage: sentMessage ?? self.sentMessage,
sentQuote: sentQuote ?? self.sentQuote,
receivedMessage: receivedMessage ?? self.receivedMessage,
receivedQuote: receivedQuote ?? self.receivedQuote
)
}
}
class AppWallpaper: ObservableObject, NSCopying {
@Published var background: Color? = nil
@Published var tint: Color? = nil
@Published var type: WallpaperType = WallpaperType.Empty
init(background: Color?, tint: Color?, type: WallpaperType) {
self.background = background
self.tint = tint
self.type = type
}
func copy(with zone: NSZone? = nil) -> Any {
AppWallpaper(background: self.background, tint: self.tint, type: self.type)
}
func clone() -> AppWallpaper { copy() as! AppWallpaper }
func copyWithoutDefault(_ background: Color?, _ tint: Color?, _ type: WallpaperType) -> AppWallpaper {
AppWallpaper(
background: background,
tint: tint,
type: type
)
}
}
enum ThemeColor {
case PRIMARY
case PRIMARY_VARIANT
case SECONDARY
case SECONDARY_VARIANT
case BACKGROUND
case SURFACE
case TITLE
case SENT_MESSAGE
case SENT_QUOTE
case RECEIVED_MESSAGE
case RECEIVED_QUOTE
case PRIMARY_VARIANT2
case WALLPAPER_BACKGROUND
case WALLPAPER_TINT
func fromColors(_ colors: Colors, _ appColors: AppColors, _ appWallpaper: AppWallpaper) -> Color? {
switch (self) {
case .PRIMARY: colors.primary
case .PRIMARY_VARIANT: colors.primaryVariant
case .SECONDARY: colors.secondary
case .SECONDARY_VARIANT: colors.secondaryVariant
case .BACKGROUND: colors.background
case .SURFACE: colors.surface
case .TITLE: appColors.title
case .PRIMARY_VARIANT2: appColors.primaryVariant2
case .SENT_MESSAGE: appColors.sentMessage
case .SENT_QUOTE: appColors.sentQuote
case .RECEIVED_MESSAGE: appColors.receivedMessage
case .RECEIVED_QUOTE: appColors.receivedQuote
case .WALLPAPER_BACKGROUND: appWallpaper.background
case .WALLPAPER_TINT: appWallpaper.tint
}
}
var text: LocalizedStringKey {
switch (self) {
case .PRIMARY: "Accent"
case .PRIMARY_VARIANT: "Additional accent"
case .SECONDARY: "Secondary"
case .SECONDARY_VARIANT: "Additional secondary"
case .BACKGROUND: "Background"
case .SURFACE: "Menus & alerts"
case .TITLE: "Title"
case .PRIMARY_VARIANT2: "Additional accent 2"
case .SENT_MESSAGE: "Sent message"
case .SENT_QUOTE: "Sent reply"
case .RECEIVED_MESSAGE: "Received message"
case .RECEIVED_QUOTE: "Received reply"
case .WALLPAPER_BACKGROUND: "Wallpaper background"
case .WALLPAPER_TINT: "Wallpaper accent"
}
}
}
struct ThemeColors: Codable, Equatable{
var primary: String? = nil
var primaryVariant: String? = nil
var secondary: String? = nil
var secondaryVariant: String? = nil
var background: String? = nil
var surface: String? = nil
var title: String? = nil
var primaryVariant2: String? = nil
var sentMessage: String? = nil
var sentQuote: String? = nil
var receivedMessage: String? = nil
var receivedQuote: String? = nil
enum CodingKeys: String, CodingKey {
case primary = "accent"
case primaryVariant = "accentVariant"
case secondary
case secondaryVariant
case background
case surface = "menus"
case title
case primaryVariant2 = "accentVariant2"
case sentMessage
case sentQuote = "sentReply"
case receivedMessage
case receivedQuote = "receivedReply"
}
static func from(sentMessage: String, sentQuote: String, receivedMessage: String, receivedQuote: String) -> ThemeColors {
var c = ThemeColors()
c.sentMessage = sentMessage
c.sentQuote = sentQuote
c.receivedMessage = receivedMessage
c.receivedQuote = receivedQuote
return c
}
static func from(_ colors: Colors, _ appColors: AppColors) -> ThemeColors {
ThemeColors(
primary: colors.primary.toReadableHex(),
primaryVariant: colors.primaryVariant.toReadableHex(),
secondary: colors.secondary.toReadableHex(),
secondaryVariant: colors.secondaryVariant.toReadableHex(),
background: colors.background.toReadableHex(),
surface: colors.surface.toReadableHex(),
title: appColors.title.toReadableHex(),
primaryVariant2: appColors.primaryVariant2.toReadableHex(),
sentMessage: appColors.sentMessage.toReadableHex(),
sentQuote: appColors.sentQuote.toReadableHex(),
receivedMessage: appColors.receivedMessage.toReadableHex(),
receivedQuote: appColors.receivedQuote.toReadableHex()
)
}
}
public struct ThemeWallpaper: Codable {
public var preset: String?
public var scale: Float?
public var scaleType: WallpaperScaleType?
public var background: String?
public var tint: String?
public var image: String?
public var imageFile: String?
func toAppWallpaper() -> AppWallpaper {
AppWallpaper (
background: background?.colorFromReadableHex(),
tint: tint?.colorFromReadableHex(),
type: WallpaperType.from(self) ?? WallpaperType.Empty
)
}
func withFilledWallpaperBase64() -> ThemeWallpaper {
let aw = toAppWallpaper()
let type = aw.type
let preset: String? = if case let WallpaperType.Preset(filename, _) = type { filename } else { nil }
let scale: Float? = if case let WallpaperType.Preset(_, scale) = type { scale } else { if case let WallpaperType.Image(_, scale, _) = type { scale } else { 1.0 } }
let scaleType: WallpaperScaleType? = if case let WallpaperType.Image(_, _, scaleType) = type { scaleType } else { nil }
let image: String? = if case WallpaperType.Image = type, let image = type.uiImage { resizeImageToStrSize(image, maxDataSize: 5_000_000) } else { nil }
return ThemeWallpaper (
preset: preset,
scale: scale,
scaleType: scaleType,
background: aw.background?.toReadableHex(),
tint: aw.tint?.toReadableHex(),
image: image,
imageFile: nil
)
}
func withFilledWallpaperPath() -> ThemeWallpaper {
let aw = toAppWallpaper()
let type = aw.type
let preset: String? = if case let WallpaperType.Preset(filename, _) = type { filename } else { nil }
let scale: Float? = if scale == nil { nil } else {
if case let WallpaperType.Preset(_, scale) = type {
scale
} else if case let WallpaperType.Image(_, scale, _) = type {
scale
} else {
nil
}
}
let scaleType: WallpaperScaleType? = if scaleType == nil { nil } else if case let WallpaperType.Image(_, _, scaleType) = type { scaleType } else { nil }
let imageFile: String? = if case let WallpaperType.Image(filename, _, _) = type { filename } else { nil }
return ThemeWallpaper (
preset: preset,
scale: scale,
scaleType: scaleType,
background: aw.background?.toReadableHex(),
tint: aw.tint?.toReadableHex(),
image: nil,
imageFile: imageFile
)
}
func importFromString() -> ThemeWallpaper {
self
// LALAL
//if preset == nil, let image {
// Need to save image from string and to save its path
// do {
// let parsed = base64ToBitmap(image)
// let filename = saveWallpaperFile(parsed)
// return copy(image = nil, imageFile = filename)
// } catch let e {
// logger.error("Error while parsing/copying the image: \(e)")
// return ThemeWallpaper()
// }
// } else {
// self
// }
}
static func from(_ type: WallpaperType, _ background: String?, _ tint: String?) -> ThemeWallpaper {
let preset: String? = if case let WallpaperType.Preset(filename, _) = type { filename } else { nil }
let scale: Float? = if case let WallpaperType.Preset(_, scale) = type { scale } else if case let WallpaperType.Image(_, scale, _) = type { scale } else { nil }
let scaleType: WallpaperScaleType? = if case let WallpaperType.Image(_, _, scaleType) = type { scaleType } else { nil }
let imageFile: String? = if case let WallpaperType.Image(filename, _, _) = type { filename } else { nil }
return ThemeWallpaper(
preset: preset,
scale: scale,
scaleType: scaleType,
background: background,
tint: tint,
image: nil,
imageFile: imageFile
)
}
}
public struct ThemeOverrides: Codable {
var themeId: String = UUID().uuidString
var base: DefaultTheme
var colors: ThemeColors = ThemeColors()
var wallpaper: ThemeWallpaper? = nil
func isSame(_ type: WallpaperType?, _ themeName: String) -> Bool {
if base.themeName != themeName {
return false
}
return if let preset = wallpaper?.preset, let type, case let WallpaperType.Preset(filename, _) = type, preset == filename {
true
} else if wallpaper?.imageFile != nil, let type, case WallpaperType.Image = type {
true
} else if wallpaper?.preset == nil && wallpaper?.imageFile == nil && type == nil {
true
} else if wallpaper?.preset == nil && wallpaper?.imageFile == nil, let type, case WallpaperType.Empty = type {
true
} else {
false
}
}
func withUpdatedColor(_ name: ThemeColor, _ color: String?) -> ThemeOverrides {
var c = colors
var w = wallpaper
switch name {
case ThemeColor.PRIMARY: c.primary = color
case ThemeColor.PRIMARY_VARIANT: c.primaryVariant = color
case ThemeColor.SECONDARY: c.secondary = color
case ThemeColor.SECONDARY_VARIANT: c.secondaryVariant = color
case ThemeColor.BACKGROUND: c.background = color
case ThemeColor.SURFACE: c.surface = color
case ThemeColor.TITLE: c.title = color
case ThemeColor.PRIMARY_VARIANT2: c.primaryVariant2 = color
case ThemeColor.SENT_MESSAGE: c.sentMessage = color
case ThemeColor.SENT_QUOTE: c.sentQuote = color
case ThemeColor.RECEIVED_MESSAGE: c.receivedMessage = color
case ThemeColor.RECEIVED_QUOTE: c.receivedQuote = color
case ThemeColor.WALLPAPER_BACKGROUND: w?.background = color
case ThemeColor.WALLPAPER_TINT: w?.tint = color
}
return ThemeOverrides(themeId: themeId, base: base, colors: c, wallpaper: w)
}
func toColors(_ base: DefaultTheme, _ perChatTheme: ThemeColors?, _ perUserTheme: ThemeColors?, _ presetWallpaperTheme: ThemeColors?) -> Colors {
let baseColors = switch base {
case DefaultTheme.LIGHT: LightColorPalette
case DefaultTheme.DARK: DarkColorPalette
case DefaultTheme.SIMPLEX: SimplexColorPalette
case DefaultTheme.BLACK: BlackColorPalette
}
let c = baseColors.clone()
c.primary = perChatTheme?.primary?.colorFromReadableHex() ?? perUserTheme?.primary?.colorFromReadableHex() ?? colors.primary?.colorFromReadableHex() ?? presetWallpaperTheme?.primary?.colorFromReadableHex() ?? baseColors.primary
c.primaryVariant = perChatTheme?.primaryVariant?.colorFromReadableHex() ?? perUserTheme?.primaryVariant?.colorFromReadableHex() ?? colors.primaryVariant?.colorFromReadableHex() ?? presetWallpaperTheme?.primaryVariant?.colorFromReadableHex() ?? baseColors.primaryVariant
c.secondary = perChatTheme?.secondary?.colorFromReadableHex() ?? perUserTheme?.secondary?.colorFromReadableHex() ?? colors.secondary?.colorFromReadableHex() ?? presetWallpaperTheme?.secondary?.colorFromReadableHex() ?? baseColors.secondary
c.secondaryVariant = perChatTheme?.secondaryVariant?.colorFromReadableHex() ?? perUserTheme?.secondaryVariant?.colorFromReadableHex() ?? colors.secondaryVariant?.colorFromReadableHex() ?? presetWallpaperTheme?.secondaryVariant?.colorFromReadableHex() ?? baseColors.secondaryVariant
c.background = perChatTheme?.background?.colorFromReadableHex() ?? perUserTheme?.background?.colorFromReadableHex() ?? colors.background?.colorFromReadableHex() ?? presetWallpaperTheme?.background?.colorFromReadableHex() ?? baseColors.background
c.surface = perChatTheme?.surface?.colorFromReadableHex() ?? perUserTheme?.surface?.colorFromReadableHex() ?? colors.surface?.colorFromReadableHex() ?? presetWallpaperTheme?.surface?.colorFromReadableHex() ?? baseColors.surface
return c
}
func toAppColors(_ base: DefaultTheme, _ perChatTheme: ThemeColors?, _ perChatWallpaperType: WallpaperType?, _ perUserTheme: ThemeColors?, _ perUserWallpaperType: WallpaperType?, _ presetWallpaperTheme: ThemeColors?) -> AppColors {
let baseColors = switch base {
case DefaultTheme.LIGHT: LightColorPaletteApp
case DefaultTheme.DARK: DarkColorPaletteApp
case DefaultTheme.SIMPLEX: SimplexColorPaletteApp
case DefaultTheme.BLACK: BlackColorPaletteApp
}
let sentMessageFallback = colors.sentMessage?.colorFromReadableHex() ?? presetWallpaperTheme?.sentMessage?.colorFromReadableHex() ?? baseColors.sentMessage
let sentQuoteFallback = colors.sentQuote?.colorFromReadableHex() ?? presetWallpaperTheme?.sentQuote?.colorFromReadableHex() ?? baseColors.sentQuote
let receivedMessageFallback = colors.receivedMessage?.colorFromReadableHex() ?? presetWallpaperTheme?.receivedMessage?.colorFromReadableHex() ?? baseColors.receivedMessage
let receivedQuoteFallback = colors.receivedQuote?.colorFromReadableHex() ?? presetWallpaperTheme?.receivedQuote?.colorFromReadableHex() ?? baseColors.receivedQuote
let c = baseColors.clone()
c.title = perChatTheme?.title?.colorFromReadableHex() ?? perUserTheme?.title?.colorFromReadableHex() ?? colors.title?.colorFromReadableHex() ?? presetWallpaperTheme?.title?.colorFromReadableHex() ?? baseColors.title
c.primaryVariant2 = perChatTheme?.primaryVariant2?.colorFromReadableHex() ?? perUserTheme?.primaryVariant2?.colorFromReadableHex() ?? colors.primaryVariant2?.colorFromReadableHex() ?? presetWallpaperTheme?.primaryVariant2?.colorFromReadableHex() ?? baseColors.primaryVariant2
c.sentMessage = if let c = perChatTheme?.sentMessage { c.colorFromReadableHex() } else if let perUserTheme, (perChatWallpaperType == nil || perUserWallpaperType == nil || perChatWallpaperType!.sameType(perUserWallpaperType)) { perUserTheme.sentMessage?.colorFromReadableHex() ?? sentMessageFallback } else { sentMessageFallback }
c.sentQuote = if let c = perChatTheme?.sentQuote { c.colorFromReadableHex() } else if let perUserTheme, (perChatWallpaperType == nil || perUserWallpaperType == nil || perChatWallpaperType!.sameType(perUserWallpaperType)) { perUserTheme.sentQuote?.colorFromReadableHex() ?? sentQuoteFallback } else { sentQuoteFallback }
c.receivedMessage = if let c = perChatTheme?.receivedMessage { c.colorFromReadableHex() } else if let perUserTheme, (perChatWallpaperType == nil || perUserWallpaperType == nil || perChatWallpaperType!.sameType(perUserWallpaperType)) { perUserTheme.receivedMessage?.colorFromReadableHex() ?? receivedMessageFallback }
else { receivedMessageFallback }
c.receivedQuote = if let c = perChatTheme?.receivedQuote { c.colorFromReadableHex() } else if let perUserTheme, (perChatWallpaperType == nil || perUserWallpaperType == nil || perChatWallpaperType!.sameType(perUserWallpaperType)) { perUserTheme.receivedQuote?.colorFromReadableHex() ?? receivedQuoteFallback } else { receivedQuoteFallback }
return c
}
func toAppWallpaper(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverride?, _ themeBackgroundColor: Color) -> AppWallpaper {
let mainType: WallpaperType
if let t = themeOverridesForType { mainType = t }
// type can be nil if override is empty `"wallpaper": "{}"`, in this case no wallpaper is needed, empty.
// It's not nil to override upper level wallpaper
else if let w = perChatTheme?.wallpaper { mainType = w.toAppWallpaper().type }
else if let w = perUserTheme?.wallpaper { mainType = w.toAppWallpaper().type }
else if let w = wallpaper { mainType = w.toAppWallpaper().type }
else { return AppWallpaper(background: nil, tint: nil, type: WallpaperType.Empty) }
let first: ThemeWallpaper? = if mainType.sameType(perChatTheme?.wallpaper?.toAppWallpaper().type) { perChatTheme?.wallpaper } else { nil }
let second: ThemeWallpaper? = if mainType.sameType(perUserTheme?.wallpaper?.toAppWallpaper().type) { perUserTheme?.wallpaper } else { nil }
let third: ThemeWallpaper? = if mainType.sameType(self.wallpaper?.toAppWallpaper().type) { self.wallpaper } else { nil }
let wallpaper: WallpaperType
switch mainType {
case let WallpaperType.Preset(preset, scale):
wallpaper = WallpaperType.Preset(preset, scale ?? first?.scale ?? second?.scale ?? third?.scale)
case let WallpaperType.Image(filename, scale, scaleType):
let scale = if themeOverridesForType == nil { scale ?? first?.scale ?? second?.scale ?? third?.scale } else { second?.scale ?? third?.scale ?? scale }
let scaleType = if themeOverridesForType == nil { scaleType ?? first?.scaleType ?? second?.scaleType ?? third?.scaleType } else { second?.scaleType ?? third?.scaleType ?? scaleType }
let imageFile = if themeOverridesForType == nil { filename } else { first?.imageFile ?? second?.imageFile ?? third?.imageFile ?? filename }
wallpaper = WallpaperType.Image(imageFile, scale, scaleType)
case WallpaperType.Empty:
wallpaper = WallpaperType.Empty
}
let background = (first?.background ?? second?.background ?? third?.background)?.colorFromReadableHex() ?? mainType.defaultBackgroundColor(base, themeBackgroundColor)
let tint = (first?.tint ?? second?.tint ?? third?.tint)?.colorFromReadableHex() ?? mainType.defaultTintColor(base)
return AppWallpaper(background: background, tint: tint, type: wallpaper)
}
func withFilledColors(_ base: DefaultTheme, _ perChatTheme: ThemeColors?, _ perChatWallpaperType: WallpaperType?, _ perUserTheme: ThemeColors?, _ perUserWallpaperType: WallpaperType?, _ presetWallpaperTheme: ThemeColors?) -> ThemeColors {
let c = toColors(base, perChatTheme, perUserTheme, presetWallpaperTheme)
let ac = toAppColors(base, perChatTheme, perChatWallpaperType, perUserTheme, perUserWallpaperType, presetWallpaperTheme)
return ThemeColors(
primary: c.primary.toReadableHex(),
primaryVariant: c.primaryVariant.toReadableHex(),
secondary: c.secondary.toReadableHex(),
secondaryVariant: c.secondaryVariant.toReadableHex(),
background: c.background.toReadableHex(),
surface: c.surface.toReadableHex(),
title: ac.title.toReadableHex(),
primaryVariant2: ac.primaryVariant2.toReadableHex(),
sentMessage: ac.sentMessage.toReadableHex(),
sentQuote: ac.sentQuote.toReadableHex(),
receivedMessage: ac.receivedMessage.toReadableHex(),
receivedQuote: ac.receivedQuote.toReadableHex()
)
}
}
extension [ThemeOverrides] {
func getTheme(_ themeId: String?) -> ThemeOverrides? {
self.first { $0.themeId == themeId }
}
func getTheme(_ themeId: String?, _ type: WallpaperType?, _ base: DefaultTheme) -> ThemeOverrides? {
self.first { $0.themeId == themeId || $0.isSame(type, base.themeName) }
}
func replace(_ theme: ThemeOverrides) -> [ThemeOverrides] {
let index = self.firstIndex { $0.themeId == theme.themeId ||
// prevent situation when two themes has the same type but different theme id (maybe something was changed in prefs by hand)
$0.isSame(WallpaperType.from(theme.wallpaper), theme.base.themeName)
}
var a = self.map { $0 }
if let index {
a[index] = theme
} else {
a.append(theme)
}
return a
}
func sameTheme(_ type: WallpaperType?, _ themeName: String) -> ThemeOverrides? { first { $0.isSame(type, themeName) } }
func skipDuplicates() -> [ThemeOverrides] {
var res: [ThemeOverrides] = []
self.forEach { theme in
let themeType = WallpaperType.from(theme.wallpaper)
if !res.contains(where: { $0.themeId == theme.themeId || $0.isSame(themeType, theme.base.themeName) }) {
res.append(theme)
}
}
return res
}
}
struct ThemeModeOverrides: Codable {
var light: ThemeModeOverride? = nil
var dark: ThemeModeOverride? = nil
func preferredMode(_ darkTheme: Bool) -> ThemeModeOverride? {
darkTheme ? dark : light
}
}
struct ThemeModeOverride: Codable {
var mode: DefaultThemeMode = CurrentColors.base.mode
var colors: ThemeColors = ThemeColors()
var wallpaper: ThemeWallpaper? = nil
var type: WallpaperType? { WallpaperType.from(wallpaper) }
func withUpdatedColor(_ name: ThemeColor, _ color: String?) -> ThemeModeOverride {
var c = colors
var w = wallpaper
switch (name) {
case ThemeColor.PRIMARY: c.primary = color
case ThemeColor.PRIMARY_VARIANT: c.primaryVariant = color
case ThemeColor.SECONDARY: c.secondary = color
case ThemeColor.SECONDARY_VARIANT: c.secondaryVariant = color
case ThemeColor.BACKGROUND: c.background = color
case ThemeColor.SURFACE: c.surface = color
case ThemeColor.TITLE: c.title = color
case ThemeColor.PRIMARY_VARIANT2: c.primaryVariant2 = color
case ThemeColor.SENT_MESSAGE: c.sentMessage = color
case ThemeColor.SENT_QUOTE: c.sentQuote = color
case ThemeColor.RECEIVED_MESSAGE: c.receivedMessage = color
case ThemeColor.RECEIVED_QUOTE: c.receivedQuote = color
case ThemeColor.WALLPAPER_BACKGROUND: w?.background = color
case ThemeColor.WALLPAPER_TINT: w?.tint = color
}
return ThemeModeOverride(mode: mode, colors: c, wallpaper: w)
}
static func withFilledAppDefaults(_ mode: DefaultThemeMode, _ base: DefaultTheme) -> ThemeModeOverride {
ThemeModeOverride(
mode: mode,
colors: ThemeOverrides(base: base).withFilledColors(base, nil, nil, nil, nil, nil),
wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename)
)
}
}
struct ThemedBackground: ViewModifier {
@EnvironmentObject var theme: AppTheme
func body(content: Content) -> some View {
content
.background(
theme.base == DefaultTheme.SIMPLEX
? LinearGradient(
colors: [
theme.colors.background.lighter(0.4),
theme.colors.background.darker(0.4)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
: LinearGradient(
colors: [],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.background(
theme.base == DefaultTheme.SIMPLEX
? Color.clear
: theme.colors.background
)
}
}
let DarkColorPalette = Colors(
primary: SimplexBlue,
primaryVariant: SimplexBlue,
secondary: HighOrLowlight,
secondaryVariant: DarkGray,
background: Color(0xFF121212),
surface: Color(0xFF222222),
error: Color.red,
onBackground: Color(0xFFFFFBFA),
onSurface: Color(0xFFFFFBFA),
isLight: false
)
let DarkColorPaletteApp = AppColors(
title: SimplexBlue,
primaryVariant2: Color(0xFF18262E),
sentMessage: Color(0xFF18262E),
sentQuote: Color(0xFF1D3847),
receivedMessage: Color(0xff262627),
receivedQuote: Color(0xff373739)
)
let LightColorPalette = Colors (
primary: SimplexBlue,
primaryVariant: SimplexBlue,
secondary: HighOrLowlight,
secondaryVariant: LightGray,
background: Color.white,
surface: Color.white,
error: Color.red,
onBackground: Color.black,
onSurface: Color.black,
isLight: true
)
let LightColorPaletteApp = AppColors(
title: SimplexBlue,
primaryVariant2: Color(0xFFE9F7FF),
sentMessage: Color(0xFFE9F7FF),
sentQuote: Color(0xFFD6F0FF),
receivedMessage: Color(0xfff5f5f6),
receivedQuote: Color(0xffececee)
)
let SimplexColorPalette = Colors(
primary: Color(0xFF70F0F9),
primaryVariant: Color(0xFF1298A5),
secondary: HighOrLowlight,
secondaryVariant: Color(0xFF2C464D),
background: Color(0xFF111528),
surface: Color(0xFF121C37),
error: Color.red,
onBackground: Color(0xFFFFFBFA),
onSurface: Color(0xFFFFFBFA),
isLight: false
)
let SimplexColorPaletteApp = AppColors(
title: Color(0xFF267BE5),
primaryVariant2: Color(0xFF172941),
sentMessage: Color(0xFF172941),
sentQuote: Color(0xFF1C3A57),
receivedMessage: Color(0xff25283a),
receivedQuote: Color(0xff36394a)
)
let BlackColorPalette = Colors(
primary: Color(0xff0077e0),
primaryVariant: Color(0xff0077e0),
secondary: HighOrLowlight,
secondaryVariant: DarkGray,
background: Color(0xff070707),
surface: Color(0xff161617),
error: Color.red,
onBackground: Color(0xFFFFFBFA),
onSurface: Color(0xFFFFFBFA),
isLight: false
)
let BlackColorPaletteApp = AppColors(
title: Color(0xff0077e0),
primaryVariant2: Color(0xff243747),
sentMessage: Color(0xFF18262E),
sentQuote: Color(0xFF1D3847),
receivedMessage: Color(0xff1b1b1b),
receivedQuote: Color(0xff29292b)
)
var systemInDarkThemeCurrently: Bool = false
extension User {
var uiThemes: ThemeModeOverrides? {
ThemeModeOverrides() // LALAL remove it
}
}
extension Contact {
var uiThemes: ThemeModeOverrides? {
nil
//ThemeModeOverrides(dark: ThemeModeOverride(mode: DefaultThemeMode.dark, colors: ThemeColors(primary: Color.green.toReadableHex(), secondary: Color.red.toReadableHex(), background: Color.white.toReadableHex(), sentMessage: Color.yellow.toReadableHex()))) // LALAL remove it
}
}
extension GroupInfo {
var uiThemes: ThemeModeOverrides? {
ThemeModeOverrides() // LALAL remove it
}
}
var CurrentColors: ThemeManager.ActiveTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) {
didSet {
AppTheme.shared.name = CurrentColors.name
AppTheme.shared.base = CurrentColors.base
AppTheme.shared.colors.updateColorsFrom(CurrentColors.colors)
AppTheme.shared.appColors.updateColorsFrom(CurrentColors.appColors)
AppTheme.shared.wallpaper.updateWallpaperFrom(CurrentColors.wallpaper)
AppTheme.shared.objectWillChange.send()
}
}
func isInDarkTheme() -> Bool { !CurrentColors.colors.isLight }
//func isSystemInDarkTheme(): Bool
class AppTheme: ObservableObject {
static let shared = AppTheme(name: CurrentColors.name, base: CurrentColors.base, colors: CurrentColors.colors, appColors: CurrentColors.appColors, wallpaper: CurrentColors.wallpaper)
var name: String
var base: DefaultTheme
@ObservedObject var colors: Colors
@ObservedObject var appColors: AppColors
@ObservedObject var wallpaper: AppWallpaper
init(name: String, base: DefaultTheme, colors: Colors, appColors: AppColors, wallpaper: AppWallpaper) {
self.name = name
self.base = base
self.colors = colors
self.appColors = appColors
self.wallpaper = wallpaper
}
}
extension Colors {
func updateColorsFrom(_ other: Colors) {
primary = other.primary
primaryVariant = other.primaryVariant
secondary = other.secondary
secondaryVariant = other.secondaryVariant
background = other.background
surface = other.surface
error = other.error
onBackground = other.onBackground
onSurface = other.onSurface
isLight = other.isLight
}
}
extension AppColors {
func updateColorsFrom(_ other: AppColors) {
title = other.title
primaryVariant2 = other.primaryVariant2
sentMessage = other.sentMessage
sentQuote = other.sentQuote
receivedMessage = other.receivedMessage
receivedQuote = other.receivedQuote
}
}
extension AppWallpaper {
func updateWallpaperFrom(_ other: AppWallpaper) {
background = other.background
tint = other.tint
type = other.type
}
}
func reactOnDarkThemeChanges(_ isDark: Bool) {
systemInDarkThemeCurrently = isDark
//sceneDelegate.window?.overrideUserInterfaceStyle == .unspecified
if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME && CurrentColors.colors.isLight == isDark {
// Change active colors from light to dark and back based on system theme
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
}
}
//@Composable
//func SimpleXTheme(darkTheme: Bool? = nil, content: @Composable () -> Void) {
// val systemDark = rememberUpdatedState(isSystemInDarkTheme())
// LaunchedEffect(Void) {
// // snapshotFlow vs LaunchedEffect reduce number of recomposes
// snapshotFlow { systemDark.value }
// .collect {
// reactOnDarkThemeChanges(systemDark.value)
// }
// }
// val theme by CurrentColors.collectAsState()
// LaunchedEffect(Void) {
// // snapshotFlow vs LaunchedEffect reduce number of recomposes when user is changed or it's themes
// snapshotFlow { chatModel.currentUser.value?.uiThemes }
// .collect {
// ThemeManager.applyTheme(appPrefs.currentTheme.get()!!)
// }
// }
// MaterialTheme(
// colors = theme.colors,
// typography = Typography,
// shapes = Shapes,
// content = {
// val rememberedAppColors = remember {
// // Explicitly creating a new object here so we don't mutate the initial [appColors]
// // provided, and overwrite the values set in it.
// theme.appColors.copy()
// }.apply { updateColorsFrom(theme.appColors) }
// val rememberedWallpaper = remember {
// // Explicitly creating a new object here so we don't mutate the initial [wallpaper]
// // provided, and overwrite the values set in it.
// theme.wallpaper.copy()
// }.apply { updateWallpaperFrom(theme.wallpaper) }
// CompositionLocalProvider(
// LocalContentColor provides theme.colors.onBackground,
// LocalAppColors provides rememberedAppColors,
// LocalAppWallpaper provides rememberedWallpaper,
// content = content)
// }
// )
//}
//
//@Composable
//func SimpleXThemeOverride(theme: ThemeManager.ActiveTheme, content: @Composable () -> Void) {
// MaterialTheme(
// colors = theme.colors,
// typography = Typography,
// shapes = Shapes,
// content = {
// val rememberedAppColors = remember {
// // Explicitly creating a new object here so we don't mutate the initial [appColors]
// // provided, and overwrite the values set in it.
// theme.appColors.copy()
// }.apply { updateColorsFrom(theme.appColors) }
// val rememberedWallpaper = remember {
// // Explicitly creating a new object here so we don't mutate the initial [wallpaper]
// // provided, and overwrite the values set in it.
// theme.wallpaper.copy()
// }.apply { updateWallpaperFrom(theme.wallpaper) }
// CompositionLocalProvider(
// LocalContentColor provides theme.colors.onBackground,
// LocalAppColors provides rememberedAppColors,
// LocalAppWallpaper provides rememberedWallpaper,
// content = content)
// }
// )
//}
+333
View File
@@ -0,0 +1,333 @@
//
// ThemeManager.swift
// SimpleX (iOS)
//
// Created by Avently on 03.06.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import Foundation
import SwiftUI
import SimpleXChat
public class ThemeManager {
public struct ActiveTheme {
let name: String
let base: DefaultTheme
let colors: Colors
let appColors: AppColors
var wallpaper: AppWallpaper = AppWallpaper(background: nil, tint: nil, type: .Empty)
}
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(colors: defaultTheme?.colors ?? ThemeColors(), wallpaper: defaultTheme?.wallpaper)
}
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 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(_ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?) -> ThemeOverrides {
let current = currentColors(nil, 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())
SceneDelegate.windowStatic?.tintColor = UIColor(CurrentColors.colors.primary)
SceneDelegate.windowStatic?.backgroundColor = UIColor(CurrentColors.colors.background)
SceneDelegate.windowStatic?.overrideUserInterfaceStyle = switch currentThemeDefault.get() {
case DefaultTheme.LIGHT.themeName: .light
case DefaultTheme.SYSTEM_THEME_NAME: .unspecified
default: .dark
}
}
static func changeDarkTheme(_ theme: String) {
systemDarkThemeDefault.set(theme)
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
}
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)
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
}
static func applyThemeColor(name: ThemeColor, color: Color? = nil, pref: Binding<ThemeModeOverride>) {
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)
CurrentColors = currentColors( nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
}
static func copyFromSameThemeOverrides(_ type: WallpaperType?, _ lowerLevelOverride: ThemeModeOverride?, _ pref: Binding<ThemeModeOverride>) -> 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
pref.wrappedValue = ThemeModeOverride(wallpaper: w)
} else {
// Make an empty wallpaper to override any top level ones
pref.wrappedValue = ThemeModeOverride(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
// LALAL
let filename: String? = "LALAL"//saveWallpaperFile(File(getWallpaperFilePath(filename)).toURI())
if let 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
prevValue.colors = ThemeColors()
prevValue.wallpaper = w
pref.wrappedValue = prevValue
return true
}
static func applyWallpaper(_ type: WallpaperType?, _ pref: Binding<ThemeModeOverride>) {
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 prevValue.wallpaper?.imageFile != nil {
// LALAL
//File(getWallpaperFilePath(prevValue.wallpaper.imageFile)).delete()
}
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)
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
}
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))
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
}
static func resetAllThemeColors(_ pref: Binding<ThemeModeOverride>) {
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)
}
}
extension String {
func colorFromReadableHex() -> Color {
// https://stackoverflow.com/a/56874327
let hex = self.trimmingCharacters(in: ["#", " "])
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (1, 1, 1, 0)
}
return Color(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
}
extension Color {
init(_ argb: Int64) {
let a = Double((argb & 0xFF000000) >> 24) / 255.0
let r = Double((argb & 0xFF0000) >> 16) / 255.0
let g = Double((argb & 0xFF00) >> 8) / 255.0
let b = Double((argb & 0xFF)) / 255.0
self.init(.sRGB, red: r, green: g, blue: b, opacity: a)
}
init(_ r: Int, _ g: Int, _ b: Int, a: Int) {
self.init(.sRGB, red: Double(r) / 255.0, green: Double(g) / 255.0, blue: Double(b) / 255.0, opacity: Double(a) / 255.0)
}
func toReadableHex() -> String {
let uiColor: UIColor = .init(self)
var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
uiColor.getRed(&r, green: &g, blue: &b, alpha: &a)
return String(format: "#%02x%02x%02x%02x",
Int(a * 255),
Int(r * 255),
Int(g * 255),
Int(b * 255)
)
}
func darker(_ factor: CGFloat = 0.1) -> Color {
var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &a)
return Color(.sRGB, red: max(r * (1 - factor), 0), green: max(g * (1 - factor), 0), blue: max(b * (1 - factor), 0), opacity: a)
}
func lighter(_ factor: CGFloat = 0.1) -> Color {
var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &a)
return Color(.sRGB, red: min(r * (1 + factor), 1), green: min(g * (1 + factor), 1), blue: min(b * (1 + factor), 1), opacity: a)
}
}
@@ -2,7 +2,7 @@
// MPVolumeView.swift
// SimpleX (iOS)
//
// Created by Stanislav on 24.04.2024.
// Created by Avently on 24.04.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
@@ -11,6 +11,7 @@ import SimpleXChat
struct IncomingCallView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject var cc = CallController.shared
var body: some View {
@@ -43,7 +44,7 @@ struct IncomingCallView: View {
cc.endCall(invitation: invitation)
}
callButton("Ignore", "multiply", .accentColor) {
callButton("Ignore", "multiply", theme.colors.primary) {
cc.activeCallInvitation = nil
}
@@ -63,7 +64,7 @@ struct IncomingCallView: View {
.padding(.horizontal, 16)
.padding(.vertical, 12)
.frame(maxWidth: .infinity)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.modifier(ThemedBackground())
.onAppear { dismissAllSheets() }
}
@@ -90,6 +90,7 @@ enum SendReceipts: Identifiable, Hashable {
struct ChatInfoView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@ObservedObject var chat: Chat
@State var contact: Contact
@@ -241,6 +242,7 @@ struct ChatInfoView: View {
}
}
}
.modifier(ThemedBackground())
.navigationBarHidden(true)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
@@ -370,6 +372,7 @@ struct ChatInfoView: View {
)
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Security code")
.modifier(ThemedBackground())
} label: {
Label(
contact.verified ? "View security code" : "Verify security code",
@@ -386,6 +389,7 @@ struct ChatInfoView: View {
currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences)
)
.navigationBarTitle("Contact preferences")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
Label("Contact preferences", systemImage: "switch.2")
@@ -434,7 +438,7 @@ struct ChatInfoView: View {
HStack {
Text("Network status")
Image(systemName: "info.circle")
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
.font(.system(size: 14))
Spacer()
Text(chatModel.contactNetworkStatus(contact).statusString)
@@ -11,6 +11,7 @@ import SimpleXChat
struct CICallItemView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
var chatItem: ChatItem
var status: CICallStatus
@@ -35,7 +36,7 @@ struct CICallItemView: View {
case .error: missedCallIcon(sent).foregroundColor(.orange)
}
CIMetaView(chat: chat, chatItem: chatItem, showStatus: false, showEdited: false)
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary, showStatus: false, showEdited: false)
.padding(.bottom, 8)
.padding(.horizontal, 12)
}
@@ -11,6 +11,7 @@ import SimpleXChat
struct CIFeaturePreferenceView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
var chatItem: ChatItem
var feature: ChatFeature
var allowed: FeatureAllowed
@@ -45,7 +46,7 @@ struct CIFeaturePreferenceView: View {
r = r
+ Text(acceptText)
.fontWeight(.medium)
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
+ Text(" ")
}
r = r + chatItem.timestampText
@@ -12,6 +12,7 @@ import SimpleXChat
struct CIFileView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
let file: CIFile?
let edited: Bool
@@ -170,7 +171,7 @@ struct CIFileView: View {
case .sndWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10)
case .rcvInvitation:
if fileSizeValid(file) {
fileIcon("arrow.down.doc.fill", color: .accentColor)
fileIcon("arrow.down.doc.fill", color: theme.colors.primary)
} else {
fileIcon("doc.fill", color: .orange, innerIcon: "exclamationmark", innerIconSize: 12)
}
@@ -182,7 +183,7 @@ struct CIFileView: View {
progressView()
}
case .rcvAborted:
fileIcon("doc.fill", color: .accentColor, innerIcon: "exclamationmark.arrow.circlepath", innerIconSize: 12)
fileIcon("doc.fill", color: theme.colors.primary, innerIcon: "exclamationmark.arrow.circlepath", innerIconSize: 12)
case .rcvComplete: fileIcon("doc.fill")
case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
@@ -11,7 +11,7 @@ import SimpleXChat
struct CIGroupInvitationView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
var chatItem: ChatItem
var groupInvitation: CIGroupInvitation
@@ -42,7 +42,7 @@ struct CIGroupInvitationView: View {
.overlay(DetermineWidth())
(
Text(chatIncognito ? "Tap to join incognito" : "Tap to join")
.foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor)
.foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : theme.colors.primary)
.font(.callout)
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
@@ -65,11 +65,11 @@ struct CIGroupInvitationView: View {
}
}
CIMetaView(chat: chat, chatItem: chatItem, showStatus: false, showEdited: false)
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary, showStatus: false, showEdited: false)
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(chatItem, colorScheme))
.background(chatItemFrameColor(chatItem, theme))
.cornerRadius(18)
.textSelection(.disabled)
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
@@ -99,7 +99,7 @@ struct CIGroupInvitationView: View {
private func groupInfoView(_ action: Bool) -> some View {
var color: Color
if action && !inProgress {
color = chatIncognito ? .indigo : .accentColor
color = chatIncognito ? .indigo : theme.colors.primary
} else {
color = Color(uiColor: .tertiaryLabel)
}
@@ -44,6 +44,7 @@ func invalidJSONView(_ json: String) -> some View {
}
.frame(maxHeight: .infinity)
.padding()
.modifier(ThemedBackground())
}
struct CIInvalidJSONView_Previews: PreviewProvider {
@@ -11,6 +11,7 @@ import SimpleXChat
struct CIMemberCreatedContactView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
var chatItem: ChatItem
var body: some View {
@@ -43,7 +44,7 @@ struct CIMemberCreatedContactView: View {
r = r
+ Text(openText)
.fontWeight(.medium)
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
+ Text(" ")
}
r = r + chatItem.timestampText
@@ -11,8 +11,9 @@ import SimpleXChat
struct CIMetaView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
var chatItem: ChatItem
var metaColor = Color.secondary
var metaColor: Color
var paleMetaColor = Color(UIColor.tertiaryLabel)
var showStatus = true
var showEdited = true
@@ -63,6 +64,7 @@ func ciMetaText(
chatTTL: Int?,
encrypted: Bool?,
color: Color = .clear,
primaryColor: Color = .accentColor,
transparent: Bool = false,
sent: SentCheckmark? = nil,
showStatus: Bool = true,
@@ -85,7 +87,7 @@ func ciMetaText(
r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2)
}
if showStatus {
if let (icon, statusColor) = meta.statusIcon(color) {
if let (icon, statusColor) = meta.statusIcon(color, primaryColor) {
let t = Text(Image(systemName: icon)).font(.caption2)
let gap = Text(" ").kerning(-1.25)
let t1 = t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67))
@@ -112,15 +114,16 @@ private func statusIconText(_ icon: String, _ color: Color) -> Text {
}
struct CIMetaView_Previews: PreviewProvider {
static let metaColor = Color.secondary
static var previews: some View {
Group {
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete)))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial)))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete)))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial)))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete)))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample())
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete)), metaColor: metaColor)
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial)), metaColor: metaColor)
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete)), metaColor: metaColor)
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial)), metaColor: metaColor)
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete)), metaColor: metaColor)
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true), metaColor: metaColor)
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), metaColor: metaColor)
}
.previewLayout(.fixed(width: 360, height: 100))
}
@@ -13,6 +13,7 @@ let decryptErrorReason: LocalizedStringKey = "It can happen when you or your con
struct CIRcvDecryptionError: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
var msgDecryptError: MsgDecryptError
var msgCount: UInt32
@@ -114,18 +115,18 @@ struct CIRcvDecryptionError: View {
}
(
Text(Image(systemName: "exclamationmark.arrow.triangle.2.circlepath"))
.foregroundColor(syncSupported ? .accentColor : .secondary)
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
.font(.callout)
+ Text(" ")
+ Text("Fix connection")
.foregroundColor(syncSupported ? .accentColor : .secondary)
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
.font(.callout)
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
)
}
.padding(.horizontal, 12)
CIMetaView(chat: chat, chatItem: chatItem)
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
.padding(.horizontal, 12)
}
.onTapGesture(perform: { onClick() })
@@ -145,7 +146,7 @@ struct CIRcvDecryptionError: View {
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
}
.padding(.horizontal, 12)
CIMetaView(chat: chat, chatItem: chatItem)
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
.padding(.horizontal, 12)
}
.onTapGesture(perform: { onClick() })
@@ -11,6 +11,7 @@ import SimpleXChat
struct CIVoiceView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
var chatItem: ChatItem
let recordingFile: CIFile?
let duration: Int
@@ -92,7 +93,7 @@ struct CIVoiceView: View {
}
private func metaView() -> some View {
CIMetaView(chat: chat, chatItem: chatItem)
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
}
}
@@ -118,7 +119,7 @@ struct VoiceMessagePlayerTime: View {
struct VoiceMessagePlayer: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
var chatItem: ChatItem
var recordingFile: CIFile?
var recordingTime: TimeInterval
@@ -244,7 +245,7 @@ struct VoiceMessagePlayer: View {
.foregroundColor(color)
.padding(.leading, image == "play.fill" ? 4 : 0)
.frame(width: 56, height: 56)
.background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear)
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
.clipShape(Circle())
if recordingTime > 0 {
ProgressCircle(length: recordingTime, progress: $playbackTime)
@@ -266,6 +267,7 @@ struct VoiceMessagePlayer: View {
}
private struct ProgressCircle: View {
@EnvironmentObject var theme: AppTheme
var length: TimeInterval
@Binding var progress: TimeInterval?
@@ -273,7 +275,7 @@ struct VoiceMessagePlayer: View {
Circle()
.trim(from: 0, to: ((progress ?? TimeInterval(0)) / length))
.stroke(
Color.accentColor,
theme.colors.primary,
style: StrokeStyle(lineWidth: 3)
)
.rotationEffect(.degrees(-90))
@@ -288,7 +290,7 @@ struct VoiceMessagePlayer: View {
.frame(width: size, height: size)
.foregroundColor(Color(uiColor: .tertiaryLabel))
.frame(width: 56, height: 56)
.background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear)
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
.clipShape(Circle())
}
@@ -296,7 +298,7 @@ struct VoiceMessagePlayer: View {
ProgressView()
.frame(width: 30, height: 30)
.frame(width: 56, height: 56)
.background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear)
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
.clipShape(Circle())
}
@@ -10,7 +10,7 @@ import SwiftUI
import SimpleXChat
struct DeletedItemView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
var chatItem: ChatItem
@@ -19,12 +19,12 @@ struct DeletedItemView: View {
Text(chatItem.content.text)
.foregroundColor(.secondary)
.italic()
CIMetaView(chat: chat, chatItem: chatItem)
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
.padding(.horizontal, 12)
}
.padding(.leading, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(chatItem, colorScheme))
.background(chatItemFrameColor(chatItem, theme))
.cornerRadius(18)
.textSelection(.disabled)
}
@@ -11,6 +11,7 @@ import SimpleXChat
struct EmojiItemView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
var chatItem: ChatItem
var body: some View {
@@ -18,7 +19,7 @@ struct EmojiItemView: View {
emojiText(chatItem.content.text)
.padding(.top, 8)
.padding(.horizontal, 6)
CIMetaView(chat: chat, chatItem: chatItem)
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
.padding(.bottom, 8)
.padding(.horizontal, 12)
}
@@ -9,16 +9,9 @@
import SwiftUI
import SimpleXChat
let notesChatColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.21)
let notesChatColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.19)
let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12)
let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17)
private let sentQuoteColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.11)
private let sentQuoteColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.09)
struct FramedItemView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
var chatItem: ChatItem
@Binding var revealed: Bool
@@ -83,7 +76,10 @@ struct FramedItemView: View {
.accessibilityLabel("")
}
}
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, colorScheme))
.onAppear {
metaColor = metaColor == .secondary ? theme.colors.secondary : metaColor
}
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, theme))
.cornerRadius(18)
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
@@ -175,13 +171,13 @@ struct FramedItemView: View {
.font(.caption)
.lineLimit(1)
}
.foregroundColor(.secondary)
.foregroundColor(theme.colors.secondary)
.padding(.horizontal, 12)
.padding(.top, 6)
.padding(.bottom, pad || (chatItem.quotedItem == nil && chatItem.meta.itemForwarded == nil) ? 6 : 0)
.overlay(DetermineWidth())
.frame(minWidth: msgWidth, alignment: .leading)
.background(chatItemFrameContextColor(chatItem, colorScheme))
.background(chatItemFrameContextColor(chatItem, theme))
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
v.frame(maxWidth: mediaWidth, alignment: .leading)
} else {
@@ -233,7 +229,7 @@ struct FramedItemView: View {
// if enable this always, size of the framed voice message item will be incorrect after end of playback
.overlay { if case .voice = chatItem.content.msgContent {} else { DetermineWidth() } }
.frame(minWidth: msgWidth, alignment: .leading)
.background(chatItemFrameContextColor(chatItem, colorScheme))
.background(chatItemFrameContextColor(chatItem, theme))
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
v.frame(maxWidth: mediaWidth, alignment: .leading)
@@ -248,7 +244,7 @@ struct FramedItemView: View {
VStack(alignment: .leading, spacing: 2) {
Text(sender)
.font(.caption)
.foregroundColor(.secondary)
.foregroundColor(theme.colors.secondary)
.lineLimit(1)
ciQuotedMsgTextView(qi, lines: 2)
}
@@ -363,22 +359,22 @@ func onlyImageOrVideo(_ ci: ChatItem) -> Bool {
return false
}
func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ theme: AppTheme) -> Color {
onlyImageOrVideo(ci)
? Color.clear
: chatItemFrameColor(ci, colorScheme)
: chatItemFrameColor(ci, theme)
}
func chatItemFrameColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
func chatItemFrameColor(_ ci: ChatItem, _ theme: AppTheme) -> Color {
ci.chatDir.sent
? (colorScheme == .light ? sentColorLight : sentColorDark)
: Color(uiColor: .tertiarySystemGroupedBackground)
? theme.appColors.sentMessage
: theme.appColors.receivedMessage
}
func chatItemFrameContextColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
func chatItemFrameContextColor(_ ci: ChatItem, _ theme: AppTheme) -> Color {
ci.chatDir.sent
? (colorScheme == .light ? sentQuoteColorLight : sentQuoteColorDark)
: Color(uiColor: .quaternarySystemFill)
? theme.appColors.sentQuote
: theme.appColors.receivedQuote
}
struct FramedItemView_Previews: PreviewProvider {
@@ -11,6 +11,7 @@ import SimpleXChat
struct IntegrityErrorItemView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
var msgError: MsgErrorType
var chatItem: ChatItem
@@ -54,6 +55,7 @@ struct IntegrityErrorItemView: View {
struct CIMsgError: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
var chatItem: ChatItem
var onTap: () -> Void
@@ -62,7 +64,7 @@ struct CIMsgError: View {
Text(chatItem.content.text)
.foregroundColor(.red)
.italic()
CIMetaView(chat: chat, chatItem: chatItem)
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
.padding(.horizontal, 12)
}
.padding(.leading, 12)
@@ -11,7 +11,7 @@ import SimpleXChat
struct MarkedDeletedItemView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
var chatItem: ChatItem
@Binding var revealed: Bool
@@ -22,7 +22,7 @@ struct MarkedDeletedItemView: View {
.foregroundColor(.secondary)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(chatItem, colorScheme))
.background(chatItemFrameColor(chatItem, theme))
.cornerRadius(18)
.textSelection(.disabled)
}
@@ -35,6 +35,7 @@ struct ChatItemForwardingView: View {
}
}
}
.modifier(ThemedBackground())
}
@ViewBuilder private func forwardListView() -> some View {
@@ -59,9 +60,13 @@ struct ChatItemForwardingView: View {
.cornerRadius(12)
.padding(.horizontal)
}
.background(Color(.systemGroupedBackground))
.modifier(ThemedBackground())
} else {
emptyList()
ZStack {
emptyList()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.modifier(ThemedBackground())
}
}
}
@@ -12,7 +12,7 @@ import SimpleXChat
struct ChatItemInfoView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
var ci: ChatItem
@Binding var chatItemInfo: ChatItemInfo?
@State private var selection: CIInfoTab = .history
@@ -101,12 +101,14 @@ struct ChatItemInfoView: View {
Label("History", systemImage: "clock")
}
.tag(CIInfoTab.history)
.modifier(ThemedBackground())
if let qi = ci.quotedItem {
quoteTab(qi)
.tabItem {
Label("In reply to", systemImage: "arrowshape.turn.up.left")
}
.tag(CIInfoTab.quote)
.modifier(ThemedBackground())
}
if let forwardedFromItem = chatItemInfo?.forwardedFromChatItem {
forwardedFromTab(forwardedFromItem)
@@ -114,6 +116,7 @@ struct ChatItemInfoView: View {
Label(local ? "Saved" : "Forwarded", systemImage: "arrowshape.turn.up.forward")
}
.tag(CIInfoTab.forwarded)
.modifier(ThemedBackground())
}
}
.onAppear {
@@ -123,6 +126,7 @@ struct ChatItemInfoView: View {
}
} else {
historyTab()
.modifier(ThemedBackground())
}
}
@@ -227,7 +231,7 @@ struct ChatItemInfoView: View {
textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(ci, colorScheme))
.background(chatItemFrameColor(ci, theme))
.cornerRadius(18)
.contextMenu {
if itemVersion.msgContent.text != "" {
@@ -296,7 +300,7 @@ struct ChatItemInfoView: View {
textBubble(qi.text, qi.formattedText, qi.getSender(nil))
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(quotedMsgFrameColor(qi, colorScheme))
.background(quotedMsgFrameColor(qi, theme))
.cornerRadius(18)
.contextMenu {
if qi.text != "" {
@@ -320,10 +324,10 @@ struct ChatItemInfoView: View {
.frame(maxWidth: maxWidth, alignment: .leading)
}
func quotedMsgFrameColor(_ qi: CIQuote, _ colorScheme: ColorScheme) -> Color {
func quotedMsgFrameColor(_ qi: CIQuote, _ theme: AppTheme) -> Color {
(qi.chatDir?.sent ?? false)
? (colorScheme == .light ? sentColorLight : sentColorDark)
: Color(uiColor: .tertiarySystemGroupedBackground)
? theme.appColors.sentMessage
: theme.appColors.receivedMessage
}
@ViewBuilder private func forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View {
@@ -445,7 +449,7 @@ struct ChatItemInfoView: View {
.foregroundColor(.secondary).opacity(0.67)
}
let v = Group {
if let (icon, statusColor) = status.statusIcon(Color.secondary) {
if let (icon, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary) {
switch status {
case .sndRcvd:
ZStack(alignment: .trailing) {
+42 -8
View File
@@ -14,7 +14,7 @@ private let memberImageSize: CGFloat = 34
struct ChatView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.colorScheme) var colorScheme
@State var theme: AppTheme = buildTheme()
@Environment(\.dismiss) var dismiss
@Environment(\.presentationMode) var presentationMode
@Environment(\.scenePhase) var scenePhase
@@ -60,7 +60,16 @@ struct ChatView: View {
Divider()
}
ZStack(alignment: .trailing) {
let wallpaperImage = theme.wallpaper.type.image
let wallpaperType = theme.wallpaper.type
let backgroundColor = theme.wallpaper.background ?? wallpaperType.defaultBackgroundColor(theme.base, theme.colors.background)
let tintColor = theme.wallpaper.tint ?? wallpaperType.defaultTintColor(theme.base)
chatItemsList()
.if(wallpaperImage != nil) { view in
view.modifier(
ChatViewBackground(image: wallpaperImage!, imageType: wallpaperType, background: backgroundColor, tint: tintColor)
)
}
if let proxy = scrollProxy {
floatingButtons(proxy)
}
@@ -78,7 +87,9 @@ struct ChatView: View {
}
.padding(.top, 1)
.navigationTitle(cInfo.chatViewName)
.background(theme.colors.background)
.navigationBarTitleDisplayMode(.inline)
.environmentObject(theme)
.onAppear {
initChatView()
}
@@ -89,6 +100,7 @@ struct ChatView: View {
chat = c
}
initChatView()
theme = buildTheme()
} else {
dismiss()
}
@@ -384,7 +396,7 @@ struct ChatView: View {
circleButton {
unreadCountText(unreadAbove)
.font(.callout)
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
}
.onTapGesture { scrollUp(proxy) }
.contextMenu {
@@ -404,13 +416,13 @@ struct ChatView: View {
circleButton {
unreadCountText(counts.unreadBelow)
.font(.callout)
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
}
.onTapGesture { scrollToBottom(proxy) }
} else if counts.totalBelow > 16 {
circleButton {
Image(systemName: "chevron.down")
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
}
.onTapGesture { scrollToBottom(proxy) }
}
@@ -513,7 +525,7 @@ struct ChatView: View {
}
}
}
@ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
ChatItemWithMenu(
chat: chat,
@@ -528,7 +540,7 @@ struct ChatView: View {
private struct ChatItemWithMenu: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
var chatItem: ChatItem
var maxWidth: CGFloat
@@ -662,7 +674,8 @@ struct ChatView: View {
playbackState: $playbackState,
playbackTime: $playbackTime
)
.uiKitContextMenu(hasImageOrVideo: ci.content.msgContent?.isImageOrVideo == true, maxWidth: maxWidth, itemWidth: $itemWidth, menu: uiMenu, allowMenu: $allowMenu)
.environmentObject(theme)
.uiKitContextMenu(hasImageOrVideo: ci.content.msgContent?.isImageOrVideo == true, maxWidth: maxWidth, itemWidth: $itemWidth, menu: uiMenu, allowMenu: $allowMenu, backgroundColor: theme.colors.background)
.accessibilityLabel("")
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
chatItemReactions(ci)
@@ -729,7 +742,7 @@ struct ChatView: View {
Text("\(r.totalReacted)")
.font(.caption)
.fontWeight(r.userReacted ? .bold : .light)
.foregroundColor(r.userReacted ? .accentColor : .secondary)
.foregroundColor(r.userReacted ? theme.colors.primary : theme.colors.secondary)
}
}
.padding(.horizontal, 6)
@@ -1219,6 +1232,27 @@ struct ChatView: View {
}
}
private func buildTheme() -> AppTheme {
if let cId = ChatModel.shared.chatId, let chat = ChatModel.shared.getChat(cId) {
let perChatTheme = if case let .direct(contact) = chat.chatInfo {
contact.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight)
} else if case let .group(groupInfo) = chat.chatInfo {
groupInfo.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight)
} else {
nil as ThemeModeOverride?
}
let overrides = if perChatTheme != nil {
ThemeManager.currentColors(nil, perChatTheme, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
} else {
nil as ThemeManager.ActiveTheme?
}
let theme = overrides ?? CurrentColors
return AppTheme(name: theme.name, base: theme.base, colors: theme.colors, appColors: theme.appColors, wallpaper: theme.wallpaper)
} else {
return AppTheme.shared
}
}
struct ToggleNtfsButton: View {
@ObservedObject var chat: Chat
@@ -9,7 +9,7 @@
import SwiftUI
struct ComposeFileView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
let fileName: String
let cancelFile: (() -> Void)
let cancelEnabled: Bool
@@ -33,7 +33,7 @@ struct ComposeFileView: View {
.padding(.vertical, 1)
.padding(.trailing, 12)
.frame(height: 50)
.background(colorScheme == .light ? sentColorLight : sentColorDark)
.background(theme.appColors.sentMessage)
.frame(maxWidth: .infinity)
.padding(.top, 8)
}
@@ -10,7 +10,7 @@ import SwiftUI
import SimpleXChat
struct ComposeImageView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
let images: [String]
let cancelImage: (() -> Void)
let cancelEnabled: Bool
@@ -48,7 +48,7 @@ struct ComposeImageView: View {
}
.padding(.vertical, 1)
.padding(.trailing, 12)
.background(colorScheme == .light ? sentColorLight : sentColorDark)
.background(theme.appColors.sentMessage)
.frame(maxWidth: .infinity)
.padding(.top, 8)
}
@@ -40,7 +40,7 @@ func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) {
}
struct ComposeLinkView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
let linkPreview: LinkPreview?
var cancelPreview: (() -> Void)? = nil
let cancelEnabled: Bool
@@ -62,7 +62,7 @@ struct ComposeLinkView: View {
}
.padding(.vertical, 1)
.padding(.trailing, 12)
.background(colorScheme == .light ? sentColorLight : sentColorDark)
.background(theme.appColors.sentMessage)
.frame(maxWidth: .infinity)
.padding(.top, 8)
}
@@ -254,6 +254,7 @@ enum UploadContent: Equatable {
struct ComposeView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
@Binding var composeState: ComposeState
@Binding var keyboardVisible: Bool
@@ -353,10 +354,10 @@ struct ComposeView: View {
keyboardVisible: $keyboardVisible,
sendButtonColor: chat.chatInfo.incognito
? .indigo.opacity(colorScheme == .dark ? 1 : 0.7)
: .accentColor
: theme.colors.primary
)
.padding(.trailing, 12)
.background(.background)
.background(theme.colors.background)
.disabled(!chat.userCanSend)
if chat.userIsObserver {
@@ -25,7 +25,7 @@ func voiceMessageTime_(_ time: TimeInterval?) -> String {
struct ComposeVoiceView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
var recordingFileName: String
@Binding var recordingTime: TimeInterval?
@Binding var recordingState: VoiceMessageRecordingState
@@ -50,7 +50,7 @@ struct ComposeVoiceView: View {
}
.padding(.vertical, 1)
.frame(height: ComposeVoiceView.previewHeight)
.background(colorScheme == .light ? sentColorLight : sentColorDark)
.background(theme.appColors.sentMessage)
.frame(maxWidth: .infinity)
.padding(.top, 8)
}
@@ -80,7 +80,7 @@ struct ComposeVoiceView: View {
Button {
startPlayback()
} label: {
playPauseIcon("play.fill")
playPauseIcon("play.fill", theme.colors.primary)
}
Text(voiceMessageTime_(recordingTime))
case .playing:
@@ -88,7 +88,7 @@ struct ComposeVoiceView: View {
audioPlayer?.pause()
playbackState = .paused
} label: {
playPauseIcon("pause.fill")
playPauseIcon("pause.fill", theme.colors.primary)
}
Text(voiceMessageTime_(playbackTime))
case .paused:
@@ -96,7 +96,7 @@ struct ComposeVoiceView: View {
audioPlayer?.play()
playbackState = .playing
} label: {
playPauseIcon("play.fill")
playPauseIcon("play.fill", theme.colors.primary)
}
Text(voiceMessageTime_(playbackTime))
}
@@ -131,7 +131,7 @@ struct ComposeVoiceView: View {
}
}
private func playPauseIcon(_ image: String, _ color: Color = .accentColor) -> some View {
private func playPauseIcon(_ image: String, _ color: Color) -> some View {
Image(systemName: image)
.resizable()
.aspectRatio(contentMode: .fit)
@@ -162,6 +162,7 @@ struct ComposeVoiceView: View {
}
private struct ProgressBar: View {
@EnvironmentObject var theme: AppTheme
var length: TimeInterval
@Binding var progress: TimeInterval?
@@ -169,7 +170,7 @@ struct ComposeVoiceView: View {
GeometryReader { geometry in
ZStack {
Rectangle()
.fill(Color.accentColor)
.fill(theme.colors.primary)
.frame(width: min(CGFloat((progress ?? TimeInterval(0)) / length) * geometry.size.width, geometry.size.width), height: 4)
.animation(.linear, value: progress)
}
@@ -9,7 +9,7 @@
import SwiftUI
struct ContextInvitingContactMemberView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
var body: some View {
HStack {
@@ -20,7 +20,7 @@ struct ContextInvitingContactMemberView: View {
.padding(12)
.frame(minHeight: 50)
.frame(maxWidth: .infinity, alignment: .leading)
.background(colorScheme == .light ? sentColorLight : sentColorDark)
.background(theme.appColors.sentMessage)
.padding(.top, 8)
}
}
@@ -10,7 +10,7 @@ import SwiftUI
import SimpleXChat
struct ContextItemView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
let contextItem: ChatItem
let contextIcon: String
@@ -44,7 +44,7 @@ struct ContextItemView: View {
.padding(12)
.frame(minHeight: 50)
.frame(maxWidth: .infinity)
.background(chatItemFrameColor(contextItem, colorScheme))
.background(chatItemFrameColor(contextItem, theme))
.padding(.top, 8)
}
@@ -28,6 +28,7 @@ struct NativeTextEditor: UIViewRepresentable {
func makeUIView(context: Context) -> UITextView {
let field = CustomUITextField(height: _height)
field.backgroundColor = .clear
field.text = text
field.textAlignment = alignment(text)
field.autocapitalizationType = .sentences
@@ -13,6 +13,7 @@ private let liveMsgInterval: UInt64 = 3000_000000
struct SendMessageView: View {
@Binding var composeState: ComposeState
@EnvironmentObject var theme: AppTheme
var sendMessage: (Int?) -> Void
var sendLiveMessage: (() async -> Void)? = nil
var updateLiveMessage: (() async -> Void)? = nil
@@ -247,6 +248,7 @@ struct SendMessageView: View {
}
private struct RecordVoiceMessageButton: View {
@EnvironmentObject var theme: AppTheme
var startVoiceMessageRecording: (() -> Void)?
var finishVoiceMessageRecording: (() -> Void)?
@Binding var holdingVMR: Bool
@@ -256,7 +258,7 @@ struct SendMessageView: View {
var body: some View {
Button(action: {}) {
Image(systemName: "mic.fill")
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
}
.disabled(disabled)
.frame(width: 29, height: 29)
@@ -323,7 +325,7 @@ struct SendMessageView: View {
Image(systemName: "multiply")
.resizable()
.scaledToFit()
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
.frame(width: 15, height: 15)
}
.frame(width: 29, height: 29)
@@ -340,7 +342,7 @@ struct SendMessageView: View {
Image(systemName: "bolt.fill")
.resizable()
.scaledToFit()
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
.frame(width: 20, height: 20)
}
.frame(width: 29, height: 29)
@@ -383,7 +385,7 @@ struct SendMessageView: View {
}
Task {
_ = try? await Task.sleep(nanoseconds: liveMsgInterval)
while composeState.liveMessage != nil {
while await composeState.liveMessage != nil {
await update()
_ = try? await Task.sleep(nanoseconds: liveMsgInterval)
}
@@ -394,7 +396,7 @@ struct SendMessageView: View {
private func finishVoiceMessageRecordingButton() -> some View {
Button(action: { finishVoiceMessageRecording?() }) {
Image(systemName: "stop.fill")
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
}
.disabled(composeState.inProgress)
.frame(width: 29, height: 29)
@@ -21,6 +21,7 @@ struct AddGroupMembersView: View {
struct AddGroupMembersViewCommon: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
var chat: Chat
@State var groupInfo: GroupInfo
var creatingGroup: Bool = false
@@ -125,6 +126,7 @@ struct AddGroupMembersViewCommon: View {
.onChange(of: selectedContacts) { _ in
searchFocussed = false
}
.modifier(ThemedBackground())
}
private func inviteMembersButton() -> some View {
@@ -176,7 +178,7 @@ struct AddGroupMembersViewCommon: View {
} else {
if checked {
icon = "checkmark.circle.fill"
iconColor = .accentColor
iconColor = theme.colors.primary
} else {
icon = "circle"
iconColor = Color(uiColor: .tertiaryLabel)
@@ -135,6 +135,7 @@ struct GroupChatInfoView: View {
}
}
}
.modifier(ThemedBackground())
.navigationBarHidden(true)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
@@ -210,6 +211,7 @@ struct GroupChatInfoView: View {
private struct MemberRowView: View {
var groupInfo: GroupInfo
@ObservedObject var groupMember: GMember
@EnvironmentObject var theme: AppTheme
var user: Bool = false
@Binding var alert: GroupChatInfoViewAlert?
@@ -282,7 +284,7 @@ struct GroupChatInfoView: View {
Button {
alert = .unblockMemberAlert(mem: member)
} label: {
Label("Unblock member", systemImage: "hand.raised.slash").foregroundColor(.accentColor)
Label("Unblock member", systemImage: "hand.raised.slash").foregroundColor(theme.colors.primary)
}
}
}
@@ -294,7 +296,7 @@ struct GroupChatInfoView: View {
Button {
alert = .unblockForAllAlert(mem: member)
} label: {
Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(.accentColor)
Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(theme.colors.primary)
}
} else {
Button {
@@ -333,6 +335,7 @@ struct GroupChatInfoView: View {
creatingGroup: false
)
.navigationBarTitle("Group link")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
if groupLink == nil {
@@ -350,6 +353,7 @@ struct GroupChatInfoView: View {
groupProfile: groupInfo.groupProfile
)
.navigationBarTitle("Group profile")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
Label("Edit group profile", systemImage: "pencil")
@@ -364,6 +368,7 @@ struct GroupChatInfoView: View {
welcomeText: groupInfo.groupProfile.description ?? ""
)
.navigationTitle("Welcome message")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
groupInfo.groupProfile.description == nil
@@ -518,6 +523,7 @@ func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bo
creatingGroup: creatingGroup
)
.navigationBarTitle("Group preferences")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
if creatingGroup {
@@ -133,6 +133,7 @@ struct GroupLinkView: View {
shouldCreate = false
}
}
.modifier(ThemedBackground())
}
private func createGroupLink() {
@@ -247,6 +247,7 @@ struct GroupMemberInfoView: View {
ProgressView().scaleEffect(2)
}
}
.modifier(ThemedBackground())
}
func connectViaAddressButton(_ contactLink: String) -> some View {
@@ -374,6 +375,7 @@ struct GroupMemberInfoView: View {
)
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Security code")
.modifier(ThemedBackground())
} label: {
Label(
member.verified ? "View security code" : "Verify security code",
@@ -66,6 +66,7 @@ struct VerifyCodeView: View {
ScanCodeView(connectionVerified: $connectionVerified, verify: verify)
.navigationBarTitleDisplayMode(.large)
.navigationTitle("Scan code")
.modifier(ThemedBackground())
} label: {
Label("Scan code", systemImage: "qrcode")
}
@@ -26,6 +26,7 @@ private let rowHeights: [DynamicTypeSize: CGFloat] = [
struct ChatListNavLink: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
@ObservedObject var chat: Chat
@State private var showContactRequestDialog = false
@@ -224,7 +225,7 @@ struct ChatListNavLink: View {
} label: {
Label("Join", systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward")
}
.tint(chat.chatInfo.incognito ? .indigo : .accentColor)
.tint(chat.chatInfo.incognito ? .indigo : theme.colors.primary)
}
@ViewBuilder private func markReadButton() -> some View {
@@ -234,14 +235,14 @@ struct ChatListNavLink: View {
} label: {
Label("Read", systemImage: "checkmark")
}
.tint(Color.accentColor)
.tint(theme.colors.primary)
} else {
Button {
Task { await markChatUnread(chat) }
} label: {
Label("Unread", systemImage: "circlebadge.fill")
}
.tint(Color.accentColor)
.tint(theme.colors.primary)
}
}
@@ -306,7 +307,7 @@ struct ChatListNavLink: View {
Button {
Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) }
} label: { Label("Accept", systemImage: "checkmark") }
.tint(.accentColor)
.tint(theme.colors.primary)
Button {
Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) }
} label: {
@@ -346,7 +347,7 @@ struct ChatListNavLink: View {
} label: {
Label("Name", systemImage: "pencil")
}
.tint(.accentColor)
.tint(theme.colors.primary)
}
.frame(height: rowHeights[dynamicTypeSize])
.appSheet(isPresented: $showContactConnectionInfo) {
@@ -354,6 +355,7 @@ struct ChatListNavLink: View {
if case let .contactConnection(contactConnection) = chat.chatInfo {
ContactConnectionInfo(contactConnection: contactConnection)
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
.modifier(ThemedBackground())
}
}
}
@@ -11,6 +11,7 @@ import SimpleXChat
struct ChatListView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding var showSettings: Bool
@State private var searchMode = false
@FocusState private var searchFocussed
@@ -86,6 +87,7 @@ struct ChatListView: View {
))
}
.listStyle(.plain)
.background(theme.colors.background)
.navigationBarTitleDisplayMode(.inline)
.navigationBarHidden(searchMode)
.toolbar {
@@ -136,7 +138,7 @@ struct ChatListView: View {
showUnreadAndFavorites = !showUnreadAndFavorites
} label: {
Image(systemName: "line.3.horizontal.decrease.circle" + (showUnreadAndFavorites ? ".fill" : ""))
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
}
}
@@ -154,12 +156,14 @@ struct ChatListView: View {
searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink
)
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
.frame(maxWidth: .infinity)
}
ForEach(cs, id: \.viewId) { chat in
ChatListNavLink(chat: chat)
.padding(.trailing, -16)
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
.listRowBackground(Color.clear)
}
.offset(x: -8)
}
@@ -179,7 +183,7 @@ struct ChatListView: View {
private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View {
Circle()
.frame(width: size, height: size)
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
}
private func onboardingButtons() -> some View {
@@ -190,7 +194,7 @@ struct ChatListView: View {
p.addLine(to: CGPoint(x: 0, y: 10))
p.addLine(to: CGPoint(x: 8, y: 0))
}
.fill(Color.accentColor)
.fill(theme.colors.primary)
.frame(width: 20, height: 10)
.padding(.trailing, 12)
@@ -213,7 +217,7 @@ struct ChatListView: View {
.padding(.vertical, 10)
.padding(.horizontal, 20)
}
.background(Color.accentColor)
.background(theme.colors.primary)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
@@ -276,6 +280,7 @@ struct ChatListView: View {
struct ChatListSearchBar: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding var searchMode: Bool
@FocusState.Binding var searchFocussed: Bool
@Binding var searchText: String
@@ -330,7 +335,7 @@ struct ChatListSearchBar: View {
if searchFocussed {
Text("Cancel")
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
.onTapGesture {
searchText = ""
searchFocussed = false
@@ -11,6 +11,7 @@ import SimpleXChat
struct ChatPreviewView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
@Binding var progressByTimeout: Bool
@State var deleting: Bool = false
@@ -94,7 +95,7 @@ struct ChatPreviewView: View {
case let .group(groupInfo):
let v = previewTitle(t)
switch (groupInfo.membership.memberStatus) {
case .memInvited: v.foregroundColor(deleting ? .secondary : chat.chatInfo.incognito ? .indigo : .accentColor)
case .memInvited: v.foregroundColor(deleting ? .secondary : chat.chatInfo.incognito ? .indigo : theme.colors.primary)
case .memAccepted: v.foregroundColor(.secondary)
default: if deleting { v.foregroundColor(.secondary) } else { v }
}
@@ -133,7 +134,7 @@ struct ChatPreviewView: View {
.foregroundColor(.white)
.padding(.horizontal, 4)
.frame(minWidth: 18, minHeight: 18)
.background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? Color.accentColor : Color.secondary)
.background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? theme.colors.primary : theme.colors.secondary)
.cornerRadius(10)
} else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local {
Image(systemName: "speaker.slash.fill")
@@ -151,7 +152,7 @@ struct ChatPreviewView: View {
private func messageDraft(_ draft: ComposeState) -> Text {
let msg = draft.message
return image("rectangle.and.pencil.and.ellipsis", color: .accentColor)
return image("rectangle.and.pencil.and.ellipsis", color: theme.colors.primary)
+ attachment()
+ messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true, showSecrets: false)
@@ -206,7 +207,7 @@ struct ChatPreviewView: View {
case let .direct(contact):
if contact.activeConn == nil && contact.profile.contactLink != nil {
chatPreviewInfoText("Tap to Connect")
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
} else if !contact.ready && contact.activeConn != nil {
if contact.nextSendGrpInv {
chatPreviewInfoText("send direct message")
@@ -11,6 +11,7 @@ import SimpleXChat
struct ContactConnectionInfo: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@State var contactConnection: PendingContactConnection
@State private var alert: CCInfoAlert?
@@ -82,6 +83,7 @@ struct ContactConnectionInfo: View {
}
}
}
.modifier(ThemedBackground())
if #available(iOS 16, *) {
v
} else {
@@ -149,7 +151,7 @@ struct ContactConnectionInfo: View {
HStack(spacing: 6) {
Text("Incognito")
Image(systemName: "info.circle")
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
.font(.system(size: 14))
}
.onTapGesture {
@@ -178,6 +180,7 @@ private func oneTimeLinkLearnMoreButton() -> some View {
NavigationLink {
AddContactLearnMore(showTitle: false)
.navigationTitle("One-time invitation link")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("info.circle") {
@@ -11,6 +11,7 @@ import SimpleXChat
struct ContactRequestView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
var contactRequest: UserContactRequest
@ObservedObject var chat: Chat
@@ -23,7 +24,7 @@ struct ContactRequestView: View {
Text(contactRequest.chatViewName)
.font(.title3)
.fontWeight(.bold)
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
.padding(.leading, 8)
.frame(alignment: .topLeading)
Spacer()
@@ -6,13 +6,11 @@
import SwiftUI
import SimpleXChat
private let fillColorDark = Color(uiColor: UIColor(red: 0.11, green: 0.11, blue: 0.11, alpha: 255))
private let fillColorLight = Color(uiColor: UIColor(red: 0.99, green: 0.99, blue: 0.99, alpha: 255))
struct UserPicker: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
@Environment(\.scenePhase) var scenePhase
@EnvironmentObject var theme: AppTheme
@Binding var showSettings: Bool
@Binding var showConnectDesktop: Bool
@Binding var userPickerVisible: Bool
@@ -21,9 +19,6 @@ struct UserPicker: View {
private let menuButtonHeight: CGFloat = 68
@State var chatViewNameWidth: CGFloat = 0
var fillColor: Color {
colorScheme == .dark ? fillColorDark : fillColorLight
}
var body: some View {
VStack {
@@ -82,7 +77,7 @@ struct UserPicker: View {
.clipShape(RoundedRectangle(cornerRadius: 16))
.background(
Rectangle()
.fill(fillColor)
.fill(theme.colors.surface)
.cornerRadius(16)
.shadow(color: .black.opacity(0.12), radius: 24, x: 0, y: 0)
)
@@ -137,7 +132,7 @@ struct UserPicker: View {
if user.activeUser {
Image(systemName: "checkmark")
} else if u.unreadCount > 0 {
unreadCounter(u.unreadCount, color: user.showNtfs ? .accentColor : .secondary)
unreadCounter(u.unreadCount, color: user.showNtfs ? theme.colors.primary : theme.colors.secondary)
} else if !user.showNtfs {
Image(systemName: "speaker.slash")
}
@@ -145,7 +140,7 @@ struct UserPicker: View {
.padding(.trailing)
.padding([.leading, .vertical], 12)
})
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
.buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
}
private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View {
@@ -162,7 +157,7 @@ struct UserPicker: View {
.padding(.vertical, 22)
.frame(height: menuButtonHeight)
}
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
.buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
}
}
@@ -118,6 +118,7 @@ struct DatabaseView: View {
NavigationLink {
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false)
.navigationTitle("Database passphrase")
.modifier(ThemedBackground())
} label: {
Text("Database passphrase")
}
@@ -144,6 +145,7 @@ struct DatabaseView: View {
NavigationLink {
ChatArchiveView(archiveName: archiveName)
.navigationTitle(title)
.modifier(ThemedBackground())
} label: {
Text(title)
}
@@ -10,7 +10,7 @@ import SwiftUI
import SimpleXChat
struct ChatInfoImage: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
var size: CGFloat
var color = Color(uiColor: .tertiarySystemGroupedBackground)
@@ -24,8 +24,7 @@ struct ChatInfoImage: View {
case .contactRequest: iconName = "person.crop.circle.fill"
default: iconName = "circle.fill"
}
let notesColor = colorScheme == .light ? notesChatColorLight : notesChatColorDark
let iconColor = if case .local = chat.chatInfo { notesColor } else { color }
let iconColor = if case .local = chat.chatInfo { theme.appColors.primaryVariant2 } else { color }
return ProfileImage(
imageStr: chat.chatInfo.image,
iconName: iconName,
@@ -0,0 +1,462 @@
//
// ChatWallpaper.swift
// SimpleX (iOS)
//
// Created by Avently on 06.06.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import Foundation
import SwiftUI
import SimpleXChat
public enum PresetWallpaper {
case cats
case flowers
case hearts
case kids
case school
case travel
var res: UIImage {
UIImage(named: "wallpaper_\(filename)")!
}
var filename: String {
switch self {
case .cats: "cats"
case .flowers: "flowers"
case .hearts: "hearts"
case .kids: "kids"
case .school: "school"
case .travel: "travel"
}
}
var scale: Float {
switch self {
case .cats: 0.63
case .flowers: 0.53
case .hearts: 0.59
case .kids: 0.53
case .school: 0.53
case .travel: 0.68
}
}
var background: [DefaultTheme: Color] {
switch self {
case .cats: wallpaperBackgrounds(light: "#ffF8F6EA")
case .flowers: wallpaperBackgrounds(light: "#ffE2FFE4")
case .hearts: wallpaperBackgrounds(light: "#ffFDECEC")
case .kids: wallpaperBackgrounds(light: "#ffdbfdfb")
case .school: wallpaperBackgrounds(light: "#ffE7F5FF")
case .travel: wallpaperBackgrounds(light: "#fff9eeff")
}
}
var tint: [DefaultTheme: Color] {
switch self {
case .cats: [
DefaultTheme.LIGHT: "#ffefdca6".colorFromReadableHex(),
DefaultTheme.DARK: "#ff4b3b0e".colorFromReadableHex(),
DefaultTheme.SIMPLEX: "#ff51400f".colorFromReadableHex(),
DefaultTheme.BLACK: "#ff4b3b0e".colorFromReadableHex()
]
case .flowers: [
DefaultTheme.LIGHT: "#ff9CEA59".colorFromReadableHex(),
DefaultTheme.DARK: "#ff31560D".colorFromReadableHex(),
DefaultTheme.SIMPLEX: "#ff36600f".colorFromReadableHex(),
DefaultTheme.BLACK: "#ff31560D".colorFromReadableHex()
]
case .hearts: [
DefaultTheme.LIGHT: "#fffde0e0".colorFromReadableHex(),
DefaultTheme.DARK: "#ff3c0f0f".colorFromReadableHex(),
DefaultTheme.SIMPLEX: "#ff411010".colorFromReadableHex(),
DefaultTheme.BLACK: "#ff3C0F0F".colorFromReadableHex()
]
case .kids: [
DefaultTheme.LIGHT: "#ffadeffc".colorFromReadableHex(),
DefaultTheme.DARK: "#ff16404B".colorFromReadableHex(),
DefaultTheme.SIMPLEX: "#ff184753".colorFromReadableHex(),
DefaultTheme.BLACK: "#ff16404B".colorFromReadableHex()
]
case .school: [
DefaultTheme.LIGHT: "#ffCEEBFF".colorFromReadableHex(),
DefaultTheme.DARK: "#ff0F293B".colorFromReadableHex(),
DefaultTheme.SIMPLEX: "#ff112f43".colorFromReadableHex(),
DefaultTheme.BLACK: "#ff0F293B".colorFromReadableHex()
]
case .travel: [
DefaultTheme.LIGHT: "#ffeedbfe".colorFromReadableHex(),
DefaultTheme.DARK: "#ff311E48".colorFromReadableHex(),
DefaultTheme.SIMPLEX: "#ff35204e".colorFromReadableHex(),
DefaultTheme.BLACK: "#ff311E48".colorFromReadableHex()
]
}
}
var colors: [DefaultTheme: ThemeColors] {
switch self {
case .cats: [
DefaultTheme.LIGHT: ThemeColors.from(
sentMessage: "#fffffaed",
sentQuote: "#fffaf0d6",
receivedMessage: "#ffF8F7F4",
receivedQuote: "#ffefede9"
),
DefaultTheme.DARK: ThemeColors.from(
sentMessage: "#ff2f2919",
sentQuote: "#ff473a1d",
receivedMessage: "#ff272624",
receivedQuote: "#ff373633"
),
DefaultTheme.SIMPLEX: ThemeColors.from(
sentMessage: "#ff41371b",
sentQuote: "#ff654f1c",
receivedMessage: "#ff272624",
receivedQuote: "#ff373633"
),
DefaultTheme.BLACK: ThemeColors.from(
sentMessage: "#ff41371b",
sentQuote: "#ff654f1c",
receivedMessage: "#ff1f1e1b",
receivedQuote: "#ff2f2d27"
)
]
case .flowers: [
DefaultTheme.LIGHT: ThemeColors.from(
sentMessage: "#fff1ffe5",
sentQuote: "#ffdcf9c4",
receivedMessage: "#ffF4F8F2",
receivedQuote: "#ffe7ece7"
),
DefaultTheme.DARK: ThemeColors.from(
sentMessage: "#ff163521",
sentQuote: "#ff1B5330",
receivedMessage: "#ff242523",
receivedQuote: "#ff353733"
),
DefaultTheme.SIMPLEX: ThemeColors.from(
sentMessage: "#ff184739",
sentQuote: "#ff1F6F4B",
receivedMessage: "#ff242523",
receivedQuote: "#ff353733"
),
DefaultTheme.BLACK: ThemeColors.from(
sentMessage: "#ff184739",
sentQuote: "#ff1F6F4B",
receivedMessage: "#ff1c1f1a",
receivedQuote: "#ff282b25"
)
]
case .hearts: [
DefaultTheme.LIGHT: ThemeColors.from(
sentMessage: "#fffff4f4",
sentQuote: "#ffffdfdf",
receivedMessage: "#fff8f6f6",
receivedQuote: "#ffefebeb"
),
DefaultTheme.DARK: ThemeColors.from(
sentMessage: "#ff301515",
sentQuote: "#ff4C1818",
receivedMessage: "#ff242121",
receivedQuote: "#ff3b3535"
),
DefaultTheme.SIMPLEX: ThemeColors.from(
sentMessage: "#ff491A28",
sentQuote: "#ff761F29",
receivedMessage: "#ff242121",
receivedQuote: "#ff3b3535"
),
DefaultTheme.BLACK: ThemeColors.from(
sentMessage: "#ff491A28",
sentQuote: "#ff761F29",
receivedMessage: "#ff1f1b1b",
receivedQuote: "#ff2e2626"
)
]
case .kids: [
DefaultTheme.LIGHT: ThemeColors.from(
sentMessage: "#ffeafeff",
sentQuote: "#ffcbf4f7",
receivedMessage: "#fff3fafa",
receivedQuote: "#ffe4efef"
),
DefaultTheme.DARK: ThemeColors.from(
sentMessage: "#ff16302F",
sentQuote: "#ff1a4a49",
receivedMessage: "#ff252626",
receivedQuote: "#ff373A39"
),
DefaultTheme.SIMPLEX: ThemeColors.from(
sentMessage: "#ff1a4745",
sentQuote: "#ff1d6b69",
receivedMessage: "#ff252626",
receivedQuote: "#ff373a39"
),
DefaultTheme.BLACK: ThemeColors.from(
sentMessage: "#ff1a4745",
sentQuote: "#ff1d6b69",
receivedMessage: "#ff1e1f1f",
receivedQuote: "#ff262b29"
)
]
case .school: [
DefaultTheme.LIGHT: ThemeColors.from(
sentMessage: "#ffeef9ff",
sentQuote: "#ffD6EDFA",
receivedMessage: "#ffF3F5F9",
receivedQuote: "#ffe4e8ee"
),
DefaultTheme.DARK: ThemeColors.from(
sentMessage: "#ff172833",
sentQuote: "#ff1C3E4F",
receivedMessage: "#ff26282c",
receivedQuote: "#ff393c40"
),
DefaultTheme.SIMPLEX: ThemeColors.from(
sentMessage: "#ff1A3C5D",
sentQuote: "#ff235b80",
receivedMessage: "#ff26282c",
receivedQuote: "#ff393c40"
),
DefaultTheme.BLACK: ThemeColors.from(
sentMessage: "#ff1A3C5D",
sentQuote: "#ff235b80",
receivedMessage: "#ff1d1e22",
receivedQuote: "#ff292b2f"
)
]
case .travel: [
DefaultTheme.LIGHT: ThemeColors.from(
sentMessage: "#fffcf6ff",
sentQuote: "#fff2e0fc",
receivedMessage: "#ffF6F4F7",
receivedQuote: "#ffede9ee"
),
DefaultTheme.DARK: ThemeColors.from(
sentMessage: "#ff33263B",
sentQuote: "#ff53385E",
receivedMessage: "#ff272528",
receivedQuote: "#ff3B373E"
),
DefaultTheme.SIMPLEX: ThemeColors.from(
sentMessage: "#ff3C255D",
sentQuote: "#ff623485",
receivedMessage: "#ff26273B",
receivedQuote: "#ff3A394F"
),
DefaultTheme.BLACK: ThemeColors.from(
sentMessage: "#ff3C255D",
sentQuote: "#ff623485",
receivedMessage: "#ff231f23",
receivedQuote: "#ff2c2931"
)
]
}
}
func toType(_ base: DefaultTheme, _ scale: Float? = nil) -> WallpaperType {
WallpaperType.Preset(
filename,
scale ?? themeOverridesDefault.get().first { $0.wallpaper != nil && $0.wallpaper!.preset == filename && $0.base == base }?.wallpaper?.scale ?? 1
)
}
static func from(_ filename: String) -> PresetWallpaper? {
switch filename {
case PresetWallpaper.cats.filename: PresetWallpaper.cats
case PresetWallpaper.flowers.filename: PresetWallpaper.flowers
case PresetWallpaper.hearts.filename: PresetWallpaper.hearts
case PresetWallpaper.kids.filename: PresetWallpaper.kids
case PresetWallpaper.school.filename: PresetWallpaper.school
case PresetWallpaper.travel.filename: PresetWallpaper.travel
default: nil
}
}
}
func wallpaperBackgrounds(light: String) -> [DefaultTheme : Color] {
[
DefaultTheme.LIGHT: light.colorFromReadableHex(),
DefaultTheme.DARK: "#ff121212".colorFromReadableHex(),
DefaultTheme.SIMPLEX: "#ff111528".colorFromReadableHex(),
DefaultTheme.BLACK: "#ff070707".colorFromReadableHex()
]
}
public enum WallpaperScaleType/*(val contentScale: ContentScale)*/: Codable {
case fill/* (ContentScale.Crop)*/
case fit/* (ContentScale.Fit)*/
case `repeat`/* (ContentScale.Fit)*/
var text: String {
switch self {
case .fill: "Fill"
case .fit: "Fit"
case .repeat: "Repeat"
}
}
}
public enum WallpaperType {
var image: SwiftUI.Image? {
if let uiImage {
return SwiftUI.Image(uiImage: uiImage)
}
return nil
}
var uiImage: UIImage? {
let filename: String
switch self {
case let .Preset(f, _): filename = f
case let .Image(f, _, _): filename = f
default: return nil
}
if filename == "" { return nil }
if let image = WallpaperType.cachedImages[filename] {
return image
} else {
let res: UIImage?
if case let .Preset(filename, _) = self {
res = (PresetWallpaper.from(filename) ?? PresetWallpaper.cats).res
} else {
do {
// LALAL REMOVE
res = nil
// In case of unintentional image deletion don't crash the app
//File(getWallpaperFilePath(filename)).inputStream().use { loadImageBitmap(it) }
} catch let e {
logger.error("Error while loading wallpaper file: \(e)")
res = nil
}
}
if let res {
WallpaperType.cachedImages[filename] = res
}
return res
}
}
func sameType(_ other: WallpaperType?) -> Bool {
if case let .Preset(filename, _) = self, case let .Preset(otherFilename, _) = other { filename == otherFilename }
else if case .Image = self, case .Image = other { true }
else if case .Empty = self, case .Empty = other { true }
else { false }
}
func samePreset(other: PresetWallpaper?) -> Bool { if case let .Preset(filename, _) = self, filename == other?.filename { true } else { false } }
case Preset(_ filename: String, _ scale: Float?)
case Image(_ filename: String, _ scale: Float?, _ scaleType: WallpaperScaleType?)
case Empty
func defaultBackgroundColor(_ theme: DefaultTheme, _ themeBackground: Color) -> Color {
if case let .Preset(filename, _) = self {
(PresetWallpaper.from(filename) ?? PresetWallpaper.cats).background[theme]!
} else {
themeBackground
}
}
func defaultTintColor(_ theme: DefaultTheme) -> Color {
if case let .Preset(filename, _) = self {
(PresetWallpaper.from(filename) ?? PresetWallpaper.cats).tint[theme]!
} else if case let .Image(_, _, scaleType) = self, scaleType == WallpaperScaleType.repeat {
Color.clear
} else {
Color.clear
}
}
static var cachedImages: [String: UIImage] = [:]
static func from(_ wallpaper: ThemeWallpaper?) -> WallpaperType? {
if wallpaper == nil {
return nil
} else if let preset = wallpaper?.preset {
return WallpaperType.Preset(preset, wallpaper?.scale)
} else if let imageFile = wallpaper?.imageFile {
return WallpaperType.Image(imageFile, wallpaper?.scale, wallpaper?.scaleType)
} else {
return WallpaperType.Empty
}
}
}
struct ChatViewBackground: ViewModifier {
@EnvironmentObject var theme: AppTheme
var image: Image
var imageType: WallpaperType
var background: Color
var tint: Color
func body(content: Content) -> some View {
content.background(
Canvas { context, size in
var image = context.resolve(image)
image.shading = .color(tint)
let rect = CGRectMake(0, 0, size.width, size.height)
func repeatDraw(_ imageScale: CGFloat) {
let scale = imageScale
for h in 0 ... Int(size.height / image.size.height / scale) {
for w in 0 ... Int(size.width / image.size.width / scale) {
let rect = CGRectMake(CGFloat(w) * image.size.width * scale, CGFloat(h) * image.size.height * scale, image.size.width * scale, image.size.height * scale)
context.draw(image, in: rect, style: FillStyle())
}
}
}
context.fill(Path(rect), with: .color(background))
switch imageType {
case let WallpaperType.Preset(filename, scale): repeatDraw(CGFloat((scale ?? 1) * (PresetWallpaper.from(filename)?.scale ?? 1)))
case let WallpaperType.Image(_, scale, scaleType):
let scaleType = scaleType ?? WallpaperScaleType.fill
switch scaleType {
case WallpaperScaleType.repeat: repeatDraw(CGFloat(scale ?? 1))
case WallpaperScaleType.fill: fallthrough
case WallpaperScaleType.fit:
// let scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height))
// let scaledWidth = (image.width * scale.scaleX).roundToInt()
// let scaledHeight = (image.height * scale.scaleY).roundToInt()
// drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
// if (scaleType == WallpaperScaleType.FIT) {
// if (scaledWidth < size.width) {
// // has black lines at left and right sides
// var x = (size.width - scaledWidth) / 2
// while (x > 0) {
// drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
// x -= scaledWidth
// }
// x = size.width - (size.width - scaledWidth) / 2
// while (x < size.width) {
// drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
// x += scaledWidth
// }
// } else {
// // has black lines at top and bottom sides
// var y = (size.height - scaledHeight) / 2
// while (y > 0) {
// drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
// y -= scaledHeight
// }
// y = size.height - (size.height - scaledHeight) / 2
// while (y < size.height) {
// drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
// y += scaledHeight
// }
// }
// }
context.fill(Path(rect), with: .color(tint))
}
case WallpaperType.Empty: ()
}
}
)
}
}
@@ -11,7 +11,7 @@ import UIKit
import SwiftUI
extension View {
func uiKitContextMenu(hasImageOrVideo: Bool, maxWidth: CGFloat, itemWidth: Binding<CGFloat>, menu: Binding<UIMenu>, allowMenu: Binding<Bool>) -> some View {
func uiKitContextMenu(hasImageOrVideo: Bool, maxWidth: CGFloat, itemWidth: Binding<CGFloat>, menu: Binding<UIMenu>, allowMenu: Binding<Bool>, backgroundColor: Color) -> some View {
Group {
if allowMenu.wrappedValue {
if hasImageOrVideo {
@@ -19,10 +19,10 @@ extension View {
self.environmentObject(ChatModel.shared)
.overlay(DetermineWidthImageVideoItem())
.onPreferenceChange(DetermineWidthImageVideoItem.Key.self) { itemWidth.wrappedValue = $0 == 0 ? maxWidth : $0 }
, maxWidth: maxWidth, itemWidth: itemWidth, menu: menu)
, maxWidth: maxWidth, itemWidth: itemWidth, menu: menu, backgroundColor: backgroundColor)
.frame(maxWidth: itemWidth.wrappedValue)
} else {
InteractionView(content: self.environmentObject(ChatModel.shared), maxWidth: maxWidth, itemWidth: itemWidth, menu: menu)
InteractionView(content: self.environmentObject(ChatModel.shared), maxWidth: maxWidth, itemWidth: itemWidth, menu: menu, backgroundColor: backgroundColor)
.fixedSize(horizontal: true, vertical: false)
}
} else {
@@ -42,11 +42,14 @@ struct InteractionView<Content: View>: UIViewRepresentable {
var maxWidth: CGFloat
var itemWidth: Binding<CGFloat>
@Binding var menu: UIMenu
var backgroundColor: Color
func makeUIView(context: Context) -> UIView {
let view = HostingViewHolder()
view.backgroundColor = .clear
let hostView = UIHostingController(rootView: content)
// hostView.view.backgroundColor = UIColor(backgroundColor)
hostView.view.backgroundColor = .clear
view.contentSize = hostView.view.intrinsicContentSize
hostView.view.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
@@ -72,6 +75,7 @@ struct InteractionView<Content: View>: UIViewRepresentable {
if was != (uiView as! HostingViewHolder).contentSize {
uiView.invalidateIntrinsicContentSize()
}
//uiView.backgroundColor = UIColor(backgroundColor)
}
func makeCoordinator() -> Coordinator {
@@ -0,0 +1,19 @@
//
// ViewModifiers.swift
// SimpleX (iOS)
//
// Created by Avently on 12.06.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
extension View {
@ViewBuilder func `if`<Content: View>(_ condition: @autoclosure () -> Bool, transform: (Self) -> Content) -> some View {
if condition() {
transform(self)
} else {
self
}
}
}
@@ -140,7 +140,7 @@ struct PasscodeEntry: View {
ZStack {
Circle()
.frame(width: h, height: h)
.foregroundColor(Color(uiColor: .systemBackground))
.foregroundColor(AppTheme.shared.colors.background)
label()
}
}
@@ -29,7 +29,7 @@ struct PasscodeView: View {
}
.padding(.horizontal, 40)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(uiColor: .systemBackground))
.background(AppTheme.shared.colors.background)
}
private func verticalPasscodeView(_ g: GeometryProxy) -> some View {
@@ -53,6 +53,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable {
struct MigrateFromDevice: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@Binding var showSettings: Bool
@Binding var showProgressOnSettings: Bool
@@ -215,7 +216,7 @@ struct MigrateFromDevice: View {
Section {
Button(action: { migrationState = .archiving }) {
settingsRow("tray.and.arrow.up") {
Text("Archive and upload").foregroundColor(.accentColor)
Text("Archive and upload").foregroundColor(theme.colors.primary)
}
}
} header: {
@@ -249,7 +250,7 @@ struct MigrateFromDevice: View {
}
}
let ratio = Float(uploadedBytes) / Float(totalBytes)
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .binary)) uploaded")
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .binary)) uploaded", theme.colors.primary)
}
.onAppear {
startUploading(totalBytes, archivePath)
@@ -263,7 +264,7 @@ struct MigrateFromDevice: View {
migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil)
}) {
settingsRow("tray.and.arrow.up") {
Text("Repeat upload").foregroundColor(.accentColor)
Text("Repeat upload").foregroundColor(theme.colors.primary)
}
}
} header: {
@@ -299,7 +300,7 @@ struct MigrateFromDevice: View {
}
Button(action: { finishMigration(fileId, ctrl) }) {
settingsRow("checkmark") {
Text("Finalize migration").foregroundColor(.accentColor)
Text("Finalize migration").foregroundColor(theme.colors.primary)
}
}
} footer: {
@@ -340,7 +341,7 @@ struct MigrateFromDevice: View {
}
Button(action: { alert = .deleteChat() }) {
settingsRow("trash.fill") {
Text("Delete database from this device").foregroundColor(.accentColor)
Text("Delete database from this device").foregroundColor(theme.colors.primary)
}
}
} header: {
@@ -379,7 +380,7 @@ struct MigrateFromDevice: View {
.truncationMode(.middle)
}
static func largeProgressView(_ value: Float, _ title: String, _ description: LocalizedStringKey) -> some View {
static func largeProgressView(_ value: Float, _ title: String, _ description: LocalizedStringKey, _ primaryColor: Color) -> some View {
ZStack {
VStack {
Text(description)
@@ -389,7 +390,7 @@ struct MigrateFromDevice: View {
Text(title)
.font(.system(size: 54))
.bold()
.foregroundColor(.accentColor)
.foregroundColor(primaryColor)
Text(description)
.font(.title3)
@@ -398,7 +399,7 @@ struct MigrateFromDevice: View {
Circle()
.trim(from: 0, to: CGFloat(value))
.stroke(
Color.accentColor,
primaryColor,
style: StrokeStyle(lineWidth: 27)
)
.rotationEffect(.degrees(180))
@@ -91,6 +91,7 @@ private enum MigrateToDeviceViewAlert: Identifiable {
struct MigrateToDevice: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@Binding var migrationState: MigrationToState?
@@ -243,7 +244,7 @@ struct MigrateToDevice: View {
}
}
let ratio = Float(downloadedBytes) / Float(max(totalBytes, 1))
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .binary)) downloaded")
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .binary)) downloaded", theme.colors.primary)
}
}
@@ -255,7 +256,7 @@ struct MigrateToDevice: View {
migrationState = .linkDownloading(link: link)
}) {
settingsRow("tray.and.arrow.down") {
Text("Repeat download").foregroundColor(.accentColor)
Text("Repeat download").foregroundColor(theme.colors.primary)
}
}
} header: {
@@ -293,7 +294,7 @@ struct MigrateToDevice: View {
migrationState = .archiveImport(archivePath: archivePath)
}) {
settingsRow("square.and.arrow.down") {
Text("Repeat import").foregroundColor(.accentColor)
Text("Repeat import").foregroundColor(theme.colors.primary)
}
}
} header: {
@@ -334,7 +335,7 @@ struct MigrateToDevice: View {
migrationState = .migration(passphrase: passphrase, confirmation: confirmation, useKeychain: useKeychain)
}) {
settingsRow("square.and.arrow.down") {
Text(button).foregroundColor(.accentColor)
Text(button).foregroundColor(theme.colors.primary)
}
}
} else {
@@ -364,6 +365,7 @@ struct MigrateToDevice: View {
}
struct OnionView: View {
@EnvironmentObject var theme: AppTheme
@State var appSettings: AppSettings
@State private var onionHosts: OnionHosts = .no
var finishMigration: (AppSettings) -> Void
@@ -381,7 +383,7 @@ struct MigrateToDevice: View {
finishMigration(appSettings)
}) {
settingsRow("checkmark") {
Text("Apply").foregroundColor(.accentColor)
Text("Apply").foregroundColor(theme.colors.primary)
}
}
} header: {
@@ -30,6 +30,7 @@ struct AddContactLearnMore: View {
}
.listRowBackground(Color.clear)
}
.modifier(ThemedBackground())
}
}
@@ -11,6 +11,7 @@ import SimpleXChat
struct AddGroupView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
@State private var chat: Chat?
@@ -95,7 +96,7 @@ struct AddGroupView: View {
Section {
groupNameTextField()
Button(action: createGroup) {
settingsRow("checkmark", color: .accentColor) { Text("Create group") }
settingsRow("checkmark", color: theme.colors.primary) { Text("Create group") }
}
.disabled(!canCreateProfile())
IncognitoToggle(incognitoEnabled: $incognitoDefault)
@@ -144,6 +145,7 @@ struct AddGroupView: View {
profile.image = nil
}
}
.modifier(ThemedBackground())
}
func groupNameTextField() -> some View {
@@ -94,7 +94,7 @@ struct NewChatView: View {
.background(
// Rectangle is needed for swipe gesture to work on mostly empty views (creatingLinkProgressView and retryButton)
Rectangle()
.fill(Color(uiColor: .systemGroupedBackground))
.fill(AppTheme.shared.colors.background)
)
.animation(.easeInOut(duration: 0.3333), value: selection)
.gesture(DragGesture(minimumDistance: 20.0, coordinateSpace: .local)
@@ -113,7 +113,7 @@ struct NewChatView: View {
}
)
}
.background(Color(.systemGroupedBackground))
.modifier(ThemedBackground())
.onChange(of: invitationUsed) { used in
if used && !(m.showingInvitation?.connChatUsed ?? true) {
m.markShowingInvitationUsed()
@@ -488,6 +488,7 @@ func strHasSingleSimplexLink(_ str: String) -> FormattedText? {
}
struct IncognitoToggle: View {
@EnvironmentObject var theme: AppTheme
@Binding var incognitoEnabled: Bool
@State private var showIncognitoSheet = false
@@ -501,7 +502,7 @@ struct IncognitoToggle: View {
HStack(spacing: 6) {
Text("Incognito")
Image(systemName: "info.circle")
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
.font(.system(size: 14))
}
.onTapGesture {
@@ -66,6 +66,7 @@ struct CreateProfile: View {
}
}
.navigationTitle("Create your profile")
.modifier(ThemedBackground())
.alert(item: $alert) { a in userProfileAlert(a, $displayName) }
.onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
@@ -78,6 +79,7 @@ struct CreateProfile: View {
struct CreateFirstProfile: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss
@State private var displayName: String = ""
@FocusState private var focusDisplayName
@@ -89,9 +91,9 @@ struct CreateFirstProfile: View {
.font(.largeTitle)
.bold()
Text("Your profile, contacts and delivered messages are stored on your device.")
.foregroundColor(.secondary)
.foregroundColor(theme.colors.secondary)
Text("The profile is only shared with your contacts.")
.foregroundColor(.secondary)
.foregroundColor(theme.colors.secondary)
.padding(.bottom)
}
.padding(.bottom)
@@ -44,6 +44,7 @@ struct HowItWorks: View {
.lineLimit(10)
.padding()
.frame(maxHeight: .infinity, alignment: .top)
.modifier(ThemedBackground())
}
}
@@ -78,6 +78,7 @@ struct SetNotificationsMode: View {
}
struct NtfModeSelector: View {
@EnvironmentObject var theme: AppTheme
var mode: NotificationsMode
@Binding var selection: NotificationsMode
@State private var tapped = false
@@ -87,7 +88,7 @@ struct NtfModeSelector: View {
VStack(alignment: .leading, spacing: 4) {
Text(mode.label)
.font(.headline)
.foregroundColor(selection == mode ? .accentColor : .secondary)
.foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary)
Text(ntfModeDescription(mode))
.lineLimit(10)
.font(.subheadline)
@@ -95,11 +96,11 @@ struct NtfModeSelector: View {
.padding(12)
}
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(uiColor: tapped ? .secondarySystemFill : .systemBackground))
.background(tapped ? Color(uiColor: .secondarySystemFill) : theme.colors.background)
.clipShape(RoundedRectangle(cornerRadius: 18))
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(selection == mode ? Color.accentColor : Color(uiColor: .secondarySystemFill), lineWidth: 2)
.stroke(selection == mode ? theme.colors.primary : Color(uiColor: .secondarySystemFill), lineWidth: 2)
)
._onButtonGesture { down in
tapped = down
@@ -79,7 +79,7 @@ struct SimpleXInfo: View {
MigrateToDevice(migrationState: $m.migrationState)
}
.navigationTitle("Migrate here")
.background(colorScheme == .light ? Color(uiColor: .tertiarySystemGroupedBackground) : .clear)
.modifier(ThemedBackground())
}
}
.sheet(isPresented: $showHowItWorks) {
@@ -178,6 +178,7 @@ struct ConnectDesktopView: View {
}
}
.navigationTitle("Connect to desktop")
.modifier(ThemedBackground())
}
private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View {
@@ -202,6 +203,7 @@ struct ConnectDesktopView: View {
ProgressView().scaleEffect(2)
}
.modifier(ThemedBackground())
}
private func searchingDesktopView() -> some View {
@@ -219,6 +221,7 @@ struct ConnectDesktopView: View {
}
}
.navigationTitle("Connecting to desktop")
.modifier(ThemedBackground())
}
@ViewBuilder private func foundDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo, _ compatible: Bool) -> some View {
@@ -246,6 +249,7 @@ struct ConnectDesktopView: View {
}
}
.navigationTitle("Found desktop")
.modifier(ThemedBackground())
if compatible && connectRemoteViaMulticastAuto {
v.onAppear { confirmKnownDesktop(rc) }
@@ -275,6 +279,7 @@ struct ConnectDesktopView: View {
}
}
.navigationTitle("Verify connection")
.modifier(ThemedBackground())
}
private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text {
@@ -315,6 +320,7 @@ struct ConnectDesktopView: View {
}
}
.navigationTitle("Connected to desktop")
.modifier(ThemedBackground())
}
private func sessionCodeText(_ code: String) -> some View {
@@ -333,7 +339,7 @@ struct ConnectDesktopView: View {
}
}
}
private func scanDesctopAddressView() -> some View {
Section("Scan QR code from desktop") {
ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processDesktopQRCode, scanMode: .oncePerCode)
@@ -389,6 +395,7 @@ struct ConnectDesktopView: View {
}
}
.navigationTitle("Linked desktops")
.modifier(ThemedBackground())
}
private func remoteCtrlView(_ rc: RemoteCtrlInfo) -> some View {
+2
View File
@@ -106,6 +106,7 @@ struct TerminalView: View {
}
.navigationViewStyle(.stack)
.navigationTitle("Chat console")
.modifier(ThemedBackground())
}
func scrollToBottom(_ proxy: ScrollViewProxy, animation: Animation = .default) {
@@ -130,6 +131,7 @@ struct TerminalView: View {
}
}
.onDisappear { terminalItem = nil }
.modifier(ThemedBackground())
}
func consoleSendMessage() {
@@ -10,9 +10,13 @@ import SwiftUI
let defaultAccentColor = CGColor.init(red: 0, green: 0.533, blue: 1, alpha: 1)
let interfaceStyles: [UIUserInterfaceStyle] = [.unspecified, .light, .dark]
//let interfaceStyles: [UIUserInterfaceStyle] = [.unspecified, .light, .dark]
let interfaceStyleNames: [LocalizedStringKey] = ["System", "Light", "Dark"]
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 appSettingsURL = URL(string: UIApplication.openSettingsURLString)!
@@ -22,7 +26,11 @@ struct AppearanceSettings: View {
@EnvironmentObject var sceneDelegate: SceneDelegate
@State private var iconLightTapped = false
@State private var iconDarkTapped = false
@State private var userInterfaceStyle = getUserInterfaceStyleDefault()
//@State private var userInterfaceStyle = getUserInterfaceStyleDefault()
@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.SIMPLEX.themeName
@State private var uiTintColor = getUIAccentColorDefault()
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner
@@ -65,9 +73,15 @@ struct AppearanceSettings: View {
}
Section {
Picker("Theme", selection: $userInterfaceStyle) {
ForEach(interfaceStyles, id: \.self) { style in
Text(interfaceStyleNames[interfaceStyles.firstIndex(of: style) ?? 0])
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) {
ForEach(Array(darkThemesNames.enumerated()), id: \.element) { index, darkTheme in
Text(darkThemesLocalized[index])
}
}
.frame(height: 36)
@@ -82,9 +96,24 @@ struct AppearanceSettings: View {
Text("Reset colors").font(.callout)
}
}
.onChange(of: userInterfaceStyle) { _ in
sceneDelegate.window?.overrideUserInterfaceStyle = userInterfaceStyle
setUserInterfaceStyleDefault(userInterfaceStyle)
.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())
}
}
.onChange(of: uiTintColor) { _ in
sceneDelegate.window?.tintColor = UIColor(cgColor: uiTintColor)
@@ -136,24 +165,24 @@ func setUIAccentColorDefault(_ color: CGColor) {
}
}
func getUserInterfaceStyleDefault() -> UIUserInterfaceStyle {
switch UserDefaults.standard.integer(forKey: DEFAULT_USER_INTERFACE_STYLE) {
case 1: return .light
case 2: return .dark
default: return .unspecified
}
}
func setUserInterfaceStyleDefault(_ style: UIUserInterfaceStyle) {
var v: Int
switch style {
case .unspecified: v = 0
case .light: v = 1
case .dark: v = 2
default: v = 0
}
UserDefaults.standard.set(v, forKey: DEFAULT_USER_INTERFACE_STYLE)
}
//func getUserInterfaceStyleDefault() -> UIUserInterfaceStyle {
// switch UserDefaults.standard.integer(forKey: DEFAULT_USER_INTERFACE_STYLE) {
// case 1: return .light
// case 2: return .dark
// default: return .unspecified
// }
//}
//
//func setUserInterfaceStyleDefault(_ style: UIUserInterfaceStyle) {
// var v: Int
// switch style {
// case .unspecified: v = 0
// case .light: v = 1
// case .dark: v = 2
// default: v = 0
// }
// UserDefaults.standard.set(v, forKey: DEFAULT_USER_INTERFACE_STYLE)
//}
struct AppearanceSettings_Previews: PreviewProvider {
static var previews: some View {
@@ -22,6 +22,7 @@ struct CallSettings: View {
NavigationLink {
RTCServers()
.navigationTitle("Your ICE servers")
.modifier(ThemedBackground())
} label: {
Text("WebRTC ICE servers")
}
@@ -70,6 +70,7 @@ struct HiddenProfileView: View {
message: Text(savePasswordError ?? "")
)
}
.modifier(ThemedBackground())
}
var passwordValid: Bool { hidePassword == hidePassword.trimmingCharacters(in: .whitespaces) }
@@ -27,6 +27,7 @@ struct IncognitoHelp: View {
}
.listRowBackground(Color.clear)
}
.modifier(ThemedBackground())
}
}
@@ -47,6 +47,7 @@ struct NetworkAndServers: View {
NavigationLink {
ProtocolServersView(serverProtocol: .smp)
.navigationTitle("Your SMP servers")
.modifier(ThemedBackground())
} label: {
Text("SMP servers")
}
@@ -54,6 +55,7 @@ struct NetworkAndServers: View {
NavigationLink {
ProtocolServersView(serverProtocol: .xftp)
.navigationTitle("Your XFTP servers")
.modifier(ThemedBackground())
} label: {
Text("XFTP servers")
}
@@ -73,6 +75,7 @@ struct NetworkAndServers: View {
NavigationLink {
AdvancedNetworkSettings()
.navigationTitle("Network settings")
.modifier(ThemedBackground())
} label: {
Text("Advanced network settings")
}
@@ -110,6 +113,7 @@ struct NetworkAndServers: View {
NavigationLink {
RTCServers()
.navigationTitle("Your ICE servers")
.modifier(ThemedBackground())
} label: {
Text("WebRTC ICE servers")
}
@@ -33,6 +33,7 @@ struct NotificationsView: View {
}
}
.navigationTitle("Send notifications")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.inline)
.alert(item: $showAlert) { alert in
if let token = m.deviceToken {
@@ -68,6 +69,7 @@ struct NotificationsView: View {
}
}
.navigationTitle("Show preview")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.inline)
} label: {
HStack {
@@ -172,6 +174,7 @@ func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey {
}
struct SelectionListView<Item: SelectableItem>: View {
@EnvironmentObject var theme: AppTheme
var list: [Item]
@Binding var selection: Item
var onSelection: ((Item) -> Void)?
@@ -185,11 +188,11 @@ struct SelectionListView<Item: SelectableItem>: View {
if selection == item {
Image(systemName: "checkmark")
.resizable().scaledToFit().frame(width: 16)
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
}
}
.contentShape(Rectangle())
.listRowBackground(Color(uiColor: tapped == item ? .secondarySystemFill : .systemBackground))
.listRowBackground(tapped == item ? Color(uiColor: .secondarySystemFill) : theme.colors.background)
.onTapGesture {
if selection == item { return }
if let f = onSelection {
@@ -48,6 +48,7 @@ struct PrivacySettings: View {
NavigationLink {
SimplexLockView(prefPerformLA: $prefPerformLA, currentLAMode: $currentLAMode)
.navigationTitle("SimpleX Lock")
.modifier(ThemedBackground())
} label: {
if prefPerformLA {
settingsRow("lock.fill", color: .green) {
@@ -332,6 +333,7 @@ struct SimplexLockView: View {
@Binding var prefPerformLA: Bool
@Binding var currentLAMode: LAMode
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
@State private var laMode: LAMode = privacyLocalAuthModeDefault.get()
@AppStorage(DEFAULT_LA_LOCK_DELAY) private var laLockDelay = 30
@@ -416,7 +418,7 @@ struct SimplexLockView: View {
HStack(spacing: 6) {
Text("Enable self-destruct")
Image(systemName: "info.circle")
.foregroundColor(.accentColor)
.foregroundColor(theme.colors.primary)
.font(.system(size: 14))
}
.onTapGesture {
@@ -94,6 +94,7 @@ struct ProtocolServersView: View {
}
.sheet(isPresented: $showScanProtoServer) {
ScanProtocolServer(servers: $servers)
.modifier(ThemedBackground())
}
.modifier(BackButton(disabled: Binding.constant(false)) {
if saveDisabled {
@@ -171,6 +172,7 @@ struct ProtocolServersView: View {
serverToEdit: srv
)
.navigationBarTitle(srv.preset ? "Preset server" : "Your server")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
let address = parseServerAddress(srv.server)
@@ -89,7 +89,7 @@ struct SetDeliveryReceiptsView: View {
.padding()
.padding(.horizontal)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(uiColor: .systemBackground))
.background(AppTheme.shared.colors.background)
}
}
@@ -44,7 +44,7 @@ let DEFAULT_ENCRYPTION_STARTED_AT = "encryptionStartedAt"
let DEFAULT_ACCENT_COLOR_RED = "accentColorRed"
let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen"
let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue"
let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle"
//let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle"
let DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius"
let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab"
let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown"
@@ -62,6 +62,11 @@ let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast"
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto"
let DEFAULT_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
let DEFAULT_CURRENT_THEME = "currentTheme"
let DEFAULT_SYSTEM_DARK_THEME = "systemDarkTheme"
let DEFAULT_CURRENT_THEME_IDS = "currentThemeIds"
let DEFAULT_THEME_OVERRIDES = "themeOverrides"
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
let appDefaults: [String: Any] = [
@@ -88,7 +93,7 @@ let appDefaults: [String: Any] = [
DEFAULT_ACCENT_COLOR_RED: 0.000,
DEFAULT_ACCENT_COLOR_GREEN: 0.533,
DEFAULT_ACCENT_COLOR_BLUE: 1.000,
DEFAULT_USER_INTERFACE_STYLE: 0,
//DEFAULT_USER_INTERFACE_STYLE: 0,
DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner,
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false,
@@ -101,7 +106,12 @@ let appDefaults: [String: Any] = [
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true,
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
DEFAULT_SHOW_SENT_VIA_RPOXY: false,
ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue
ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue,
DEFAULT_THEME_OVERRIDES: "{}",
DEFAULT_CURRENT_THEME: DefaultTheme.SYSTEM_THEME_NAME,
DEFAULT_SYSTEM_DARK_THEME: DefaultTheme.SIMPLEX.themeName,
DEFAULT_CURRENT_THEME_IDS: "{}"
]
// not used anymore
@@ -148,14 +158,66 @@ let onboardingStageDefault = EnumDefault<OnboardingStage>(defaults: UserDefaults
let customDisappearingMessageTimeDefault = IntDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME)
let currentThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME, withDefault: DefaultTheme.SYSTEM_THEME_NAME)
let systemDarkThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SYSTEM_DARK_THEME, withDefault: DefaultTheme.SIMPLEX.themeName)
let currentThemeIdsDefault = CodableDefault<[String: String]>(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME_IDS, withDefault: [:] )
let themeOverridesDefault: CodableDefault<[ThemeOverrides]> = CodableDefault(defaults: UserDefaults.standard, forKey: DEFAULT_THEME_OVERRIDES, withDefault: [])
func setGroupDefaults() {
privacyAcceptImagesGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES))
}
public class StringDefault {
var defaults: UserDefaults
var key: String
var defaultValue: String
public init(defaults: UserDefaults = UserDefaults.standard, forKey: String, withDefault: String) {
self.defaults = defaults
self.key = forKey
self.defaultValue = withDefault
}
public func get() -> String {
defaults.string(forKey: key) ?? defaultValue
}
public func set(_ value: String) {
defaults.set(value, forKey: key)
defaults.synchronize()
}
}
public class CodableDefault<T: Codable> {
var defaults: UserDefaults
var key: String
var defaultValue: T
public init(defaults: UserDefaults = UserDefaults.standard, forKey: String, withDefault: T) {
self.defaults = defaults
self.key = forKey
self.defaultValue = withDefault
}
public func get() -> T {
if let value = defaults.string(forKey: key) {
return decodeJSON(value) ?? defaultValue
}
return defaultValue
}
public func set(_ value: T) {
defaults.set(encodeJSON(value), forKey: key)
defaults.synchronize()
}
}
struct SettingsView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var sceneDelegate: SceneDelegate
@EnvironmentObject var theme: AppTheme
@Binding var showSettings: Bool
@State private var showProgress: Bool = false
@@ -180,6 +242,7 @@ struct SettingsView: View {
NavigationLink {
UserProfile()
.navigationTitle("Your current profile")
.modifier(ThemedBackground())
} label: {
ProfilePreview(profileOf: user)
.padding(.leading, -8)
@@ -197,6 +260,7 @@ struct SettingsView: View {
NavigationLink {
UserAddressView(shareViaProfile: user.addressShared)
.navigationTitle("SimpleX address")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("qrcode") { Text("Your SimpleX address") }
@@ -205,6 +269,7 @@ struct SettingsView: View {
NavigationLink {
PreferencesView(profile: user.profile, preferences: user.fullPreferences, currentPreferences: user.fullPreferences)
.navigationTitle("Your preferences")
.modifier(ThemedBackground())
} label: {
settingsRow("switch.2") { Text("Chat preferences") }
}
@@ -219,6 +284,7 @@ struct SettingsView: View {
NavigationLink {
MigrateFromDevice(showSettings: $showSettings, showProgressOnSettings: $showProgress)
.navigationTitle("Migrate device")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("tray.and.arrow.up") { Text("Migrate to another device") }
@@ -230,6 +296,7 @@ struct SettingsView: View {
NavigationLink {
NotificationsView()
.navigationTitle("Notifications")
.modifier(ThemedBackground())
} label: {
HStack {
notificationsIcon()
@@ -241,6 +308,7 @@ struct SettingsView: View {
NavigationLink {
NetworkAndServers()
.navigationTitle("Network & servers")
.modifier(ThemedBackground())
} label: {
settingsRow("externaldrive.connected.to.line.below") { Text("Network & servers") }
}
@@ -249,6 +317,7 @@ struct SettingsView: View {
NavigationLink {
CallSettings()
.navigationTitle("Your calls")
.modifier(ThemedBackground())
} label: {
settingsRow("video") { Text("Audio & video calls") }
}
@@ -257,6 +326,7 @@ struct SettingsView: View {
NavigationLink {
PrivacySettings()
.navigationTitle("Your privacy")
.modifier(ThemedBackground())
} label: {
settingsRow("lock") { Text("Privacy & security") }
}
@@ -266,6 +336,7 @@ struct SettingsView: View {
NavigationLink {
AppearanceSettings()
.navigationTitle("Appearance")
.modifier(ThemedBackground())
} label: {
settingsRow("sun.max") { Text("Appearance") }
}
@@ -280,6 +351,7 @@ struct SettingsView: View {
NavigationLink {
ChatHelp(showSettings: $showSettings)
.navigationTitle("Welcome \(user.displayName)!")
.modifier(ThemedBackground())
.frame(maxHeight: .infinity, alignment: .top)
} label: {
settingsRow("questionmark") { Text("How to use it") }
@@ -287,6 +359,7 @@ struct SettingsView: View {
}
NavigationLink {
WhatsNewView(viaSettings: true)
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.inline)
} label: {
settingsRow("plus") { Text("What's new") }
@@ -294,6 +367,7 @@ struct SettingsView: View {
NavigationLink {
SimpleXInfo(onboarding: false)
.navigationBarTitle("", displayMode: .inline)
.modifier(ThemedBackground())
.frame(maxHeight: .infinity, alignment: .top)
} label: {
settingsRow("info") { Text("About SimpleX Chat") }
@@ -333,18 +407,21 @@ struct SettingsView: View {
NavigationLink {
DeveloperView()
.navigationTitle("Developer tools")
.modifier(ThemedBackground())
} label: {
settingsRow("chevron.left.forwardslash.chevron.right") { Text("Developer tools") }
}
NavigationLink {
VersionView()
.navigationBarTitle("App version")
.modifier(ThemedBackground())
} label: {
Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))")
}
}
}
.navigationTitle("Your settings")
.modifier(ThemedBackground())
}
.onDisappear {
chatModel.showingTerminal = false
@@ -356,6 +433,7 @@ struct SettingsView: View {
NavigationLink {
DatabaseView(showSettings: $showSettings, chatItemTTL: chatModel.chatItemTTL)
.navigationTitle("Your chat database")
.modifier(ThemedBackground())
} label: {
let color: Color = chatModel.chatDbEncrypted == false ? .orange : .secondary
settingsRow("internaldrive", color: color) {
@@ -300,6 +300,7 @@ struct UserAddressView: View {
NavigationLink {
UserAddressLearnMore()
.navigationTitle("SimpleX address")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("info.circle") {
@@ -8,6 +8,7 @@ import SimpleXChat
struct UserProfilesView: View {
@EnvironmentObject private var m: ChatModel
@EnvironmentObject private var theme: AppTheme
@Binding var showSettings: Bool
@Environment(\.editMode) private var editMode
@AppStorage(DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE) private var showHiddenProfilesNotice = true
@@ -111,6 +112,7 @@ struct UserProfilesView: View {
}
}
.navigationTitle("Your chat profiles")
.modifier(ThemedBackground())
.searchable(text: $searchTextOrPassword, placement: .navigationBarDrawer(displayMode: .always))
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
@@ -237,6 +239,7 @@ struct UserProfilesView: View {
}
}
}
.modifier(ThemedBackground())
}
@ViewBuilder func actionHeader(_ title: LocalizedStringKey, _ user: User) -> some View {
@@ -356,7 +359,7 @@ struct UserProfilesView: View {
}
}
}
.tint(.accentColor)
.tint(theme.colors.primary)
}
}
if #available(iOS 16, *) {
@@ -188,7 +188,12 @@
8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */; };
8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */; };
8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */; };
8C7E3CE42C0DEAC400BFF63A /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7E3CE32C0DEAC400BFF63A /* Theme.swift */; };
8C7F8F0E2C19C0C100D16888 /* ViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7F8F0D2C19C0C100D16888 /* ViewModifiers.swift */; };
8C804B1E2C11F966007A63C8 /* ChatWallpaper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C804B1D2C11F966007A63C8 /* ChatWallpaper.swift */; };
8C81482C2BD91CD4002CBEC3 /* AudioDevicePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */; };
8C852B082C1086D100BA61E8 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C852B072C1086D100BA61E8 /* Color.swift */; };
8C86EBE52C0DAE4F00E12243 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C86EBE42C0DAE4F00E12243 /* ThemeManager.swift */; };
8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; };
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; };
@@ -485,7 +490,12 @@
8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToDevice.swift; sourceTree = "<group>"; };
8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateFromDevice.swift; sourceTree = "<group>"; };
8C7E3CE32C0DEAC400BFF63A /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
8C7F8F0D2C19C0C100D16888 /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = "<group>"; };
8C804B1D2C11F966007A63C8 /* ChatWallpaper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWallpaper.swift; sourceTree = "<group>"; };
8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioDevicePicker.swift; sourceTree = "<group>"; };
8C852B072C1086D100BA61E8 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; };
8C86EBE42C0DAE4F00E12243 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = "<group>"; };
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = "<group>"; };
@@ -660,6 +670,8 @@
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */,
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */,
8C05382D2B39887E006436DC /* VideoUtils.swift */,
8C804B1D2C11F966007A63C8 /* ChatWallpaper.swift */,
8C7F8F0D2C19C0C100D16888 /* ViewModifiers.swift */,
);
path = Helpers;
sourceTree = "<group>";
@@ -684,6 +696,7 @@
5CA059C2279559F40002BEB4 /* Shared */ = {
isa = PBXGroup;
children = (
8C86EBE22C0DAE0A00E12243 /* UI */,
5CA059C3279559F40002BEB4 /* SimpleXApp.swift */,
5C36027227F47AD5009F19D9 /* AppDelegate.swift */,
5CA059C4279559F40002BEB4 /* ContentView.swift */,
@@ -922,6 +935,24 @@
path = Migration;
sourceTree = "<group>";
};
8C86EBE22C0DAE0A00E12243 /* UI */ = {
isa = PBXGroup;
children = (
8C86EBE32C0DAE3700E12243 /* Theme */,
);
path = UI;
sourceTree = "<group>";
};
8C86EBE32C0DAE3700E12243 /* Theme */ = {
isa = PBXGroup;
children = (
8C86EBE42C0DAE4F00E12243 /* ThemeManager.swift */,
8C7E3CE32C0DEAC400BFF63A /* Theme.swift */,
8C852B072C1086D100BA61E8 /* Color.swift */,
);
path = Theme;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -1159,6 +1190,7 @@
5C029EAA283942EA004A9677 /* CallController.swift in Sources */,
5CBE6C142944CC12002D9531 /* ScanCodeView.swift in Sources */,
5CC036E029C488D500C0EF20 /* HiddenProfileView.swift in Sources */,
8C7E3CE42C0DEAC400BFF63A /* Theme.swift in Sources */,
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */,
5CB634A829E437960066AD6B /* PasscodeEntry.swift in Sources */,
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */,
@@ -1184,6 +1216,7 @@
5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */,
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */,
5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */,
8C7F8F0E2C19C0C100D16888 /* ViewModifiers.swift in Sources */,
5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */,
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */,
5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */,
@@ -1196,6 +1229,7 @@
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */,
5CC1C99227A6C7F5000D9FF6 /* QRCode.swift in Sources */,
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */,
8C86EBE52C0DAE4F00E12243 /* ThemeManager.swift in Sources */,
5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */,
649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */,
5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */,
@@ -1226,6 +1260,7 @@
5C5DB70E289ABDD200730FFF /* AppearanceSettings.swift in Sources */,
5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */,
5C3CCFCC2AE6BD3100C3F0C3 /* ConnectDesktopView.swift in Sources */,
8C804B1E2C11F966007A63C8 /* ChatWallpaper.swift in Sources */,
5C9C2DA92899DA6F00CC63B1 /* NetworkAndServers.swift in Sources */,
5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */,
5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */,
@@ -1252,6 +1287,7 @@
5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */,
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */,
6440CA00288857A10062C672 /* CIEventView.swift in Sources */,
8C852B082C1086D100BA61E8 /* Color.swift in Sources */,
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */,
+5 -4
View File
@@ -26,6 +26,7 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat {
public var sendRcptsContacts: Bool
public var sendRcptsSmallGroups: Bool
public var viewPwdHash: UserPwdHash?
//public var uiThemes: ThemeModeOverrides
public var id: Int64 { userId }
@@ -2653,8 +2654,8 @@ public struct CIMeta: Decodable {
return false
}
public func statusIcon(_ metaColor: Color = .secondary) -> (String, Color)? {
itemStatus.statusIcon(metaColor)
public func statusIcon(_ metaColor: Color = .secondary, _ primaryColor: Color = .accentColor) -> (String, Color)? {
itemStatus.statusIcon(metaColor, primaryColor)
}
public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, deletable: Bool = true, editable: Bool = true) -> CIMeta {
@@ -2742,7 +2743,7 @@ public enum CIStatus: Decodable {
}
}
public func statusIcon(_ metaColor: Color = .secondary) -> (String, Color)? {
public func statusIcon(_ metaColor: Color = .secondary, _ primaryColor: Color = .accentColor) -> (String, Color)? {
switch self {
case .sndNew: return nil
case .sndSent: return ("checkmark", metaColor)
@@ -2754,7 +2755,7 @@ public enum CIStatus: Decodable {
case .sndErrorAuth: return ("multiply", .red)
case .sndError: return ("multiply", .red)
case .sndWarning: return ("exclamationmark.triangle.fill", .orange)
case .rcvNew: return ("circlebadge.fill", Color.accentColor)
case .rcvNew: return ("circlebadge.fill", primaryColor)
case .rcvRead: return nil
case .invalid: return ("questionmark", metaColor)
}
@@ -32,7 +32,6 @@ enum class DefaultTheme {
val mode: DefaultThemeMode get() = if (this == LIGHT) DefaultThemeMode.LIGHT else DefaultThemeMode.DARK
// Call it only with base theme, not SYSTEM
fun hasChangedAnyColor(overrides: ThemeOverrides?): Boolean {
if (overrides == null) return false
return overrides.colors != ThemeColors() ||