IceCubesApp/Packages/DesignSystem/Sources/DesignSystem/Theme.swift

364 lines
10 KiB
Swift
Raw Normal View History

import Combine
2022-12-24 13:55:04 +00:00
import SwiftUI
2024-03-11 07:59:29 +00:00
@MainActor
2024-03-11 08:05:52 +00:00
@Observable
2024-03-11 07:59:29 +00:00
public final class Theme {
final class ThemeStorage {
2023-09-18 19:03:52 +00:00
enum ThemeKey: String {
case colorScheme, tint, label, primaryBackground, secondaryBackground
case avatarPosition2, avatarShape2, statusActionsDisplay, statusDisplayStyle
2023-09-18 19:03:52 +00:00
case selectedSet, selectedScheme
case followSystemColorSchme
case displayFullUsernameTimeline
case lineSpacing
case statusActionSecondary
2023-09-18 19:03:52 +00:00
}
@AppStorage("is_previously_set") public var isThemePreviouslySet: Bool = false
@AppStorage(ThemeKey.selectedScheme.rawValue) public var selectedScheme: ColorScheme = .dark
@AppStorage(ThemeKey.tint.rawValue) public var tintColor: Color = .black
@AppStorage(ThemeKey.primaryBackground.rawValue) public var primaryBackgroundColor: Color = .white
@AppStorage(ThemeKey.secondaryBackground.rawValue) public var secondaryBackgroundColor: Color = .gray
@AppStorage(ThemeKey.label.rawValue) public var labelColor: Color = .black
2023-12-27 15:07:16 +00:00
@AppStorage(ThemeKey.avatarPosition2.rawValue) var avatarPosition: AvatarPosition = .leading
@AppStorage(ThemeKey.avatarShape2.rawValue) var avatarShape: AvatarShape = .circle
2023-09-18 19:03:52 +00:00
@AppStorage(ThemeKey.selectedSet.rawValue) var storedSet: ColorSetName = .iceCubeDark
@AppStorage(ThemeKey.statusActionsDisplay.rawValue) public var statusActionsDisplay: StatusActionsDisplay = .full
@AppStorage(ThemeKey.statusDisplayStyle.rawValue) public var statusDisplayStyle: StatusDisplayStyle = .large
@AppStorage(ThemeKey.followSystemColorSchme.rawValue) public var followSystemColorScheme: Bool = true
2023-12-27 15:07:16 +00:00
@AppStorage(ThemeKey.displayFullUsernameTimeline.rawValue) public var displayFullUsername: Bool = false
@AppStorage(ThemeKey.lineSpacing.rawValue) public var lineSpacing: Double = 1.2
@AppStorage(ThemeKey.statusActionSecondary.rawValue) public var statusActionSecondary: StatusActionSecondary = .share
2023-09-18 19:03:52 +00:00
@AppStorage("font_size_scale") public var fontSizeScale: Double = 1
@AppStorage("chosen_font") public var chosenFontData: Data?
init() {}
}
2023-01-30 06:27:06 +00:00
public enum FontState: Int, CaseIterable {
case system
case openDyslexic
case hyperLegible
case SFRounded
2023-01-30 06:27:06 +00:00
case custom
public var title: LocalizedStringKey {
switch self {
case .system:
2023-09-16 12:15:03 +00:00
"settings.display.font.system"
case .openDyslexic:
2023-09-16 12:15:03 +00:00
"Open Dyslexic"
case .hyperLegible:
2023-09-16 12:15:03 +00:00
"Hyper Legible"
case .SFRounded:
2023-09-16 12:15:03 +00:00
"SF Rounded"
2023-01-30 06:27:06 +00:00
case .custom:
2023-09-16 12:15:03 +00:00
"settings.display.font.custom"
2023-01-30 06:27:06 +00:00
}
}
2023-01-30 06:27:06 +00:00
}
2023-01-17 10:36:01 +00:00
public enum AvatarPosition: String, CaseIterable {
case leading, top
2023-01-17 10:36:01 +00:00
public var description: LocalizedStringKey {
switch self {
case .leading:
2023-09-16 12:15:03 +00:00
"enum.avatar-position.leading"
case .top:
2023-09-16 12:15:03 +00:00
"enum.avatar-position.top"
2022-12-31 05:48:09 +00:00
}
}
2022-12-31 05:48:09 +00:00
}
2024-02-14 11:48:14 +00:00
public enum StatusActionSecondary: String, CaseIterable {
case share, bookmark
2024-02-14 11:48:14 +00:00
public var description: LocalizedStringKey {
switch self {
case .share:
"status.action.share-title"
case .bookmark:
"status.action.bookmark"
}
}
}
public enum AvatarShape: String, CaseIterable {
case circle, rounded
public var description: LocalizedStringKey {
switch self {
case .circle:
2023-09-16 12:15:03 +00:00
"enum.avatar-shape.circle"
case .rounded:
2023-09-16 12:15:03 +00:00
"enum.avatar-shape.rounded"
}
}
}
2023-01-17 10:36:01 +00:00
2023-01-06 16:14:34 +00:00
public enum StatusActionsDisplay: String, CaseIterable {
case full, discret, none
public var description: LocalizedStringKey {
switch self {
case .full:
2023-09-16 12:15:03 +00:00
"enum.status-actions-display.all"
2023-01-06 16:14:34 +00:00
case .discret:
2023-09-16 12:15:03 +00:00
"enum.status-actions-display.only-buttons"
2023-01-06 16:14:34 +00:00
case .none:
2023-09-16 12:15:03 +00:00
"enum.status-actions-display.no-buttons"
2023-01-06 16:14:34 +00:00
}
}
}
2023-01-17 10:36:01 +00:00
2023-01-07 16:44:25 +00:00
public enum StatusDisplayStyle: String, CaseIterable {
2023-02-22 06:26:32 +00:00
case large, medium, compact
2023-01-07 16:44:25 +00:00
public var description: LocalizedStringKey {
switch self {
case .large:
2023-09-16 12:15:03 +00:00
"enum.status-display-style.large"
2023-02-22 06:26:32 +00:00
case .medium:
2023-09-16 12:15:03 +00:00
"enum.status-display-style.medium"
2023-01-07 16:44:25 +00:00
case .compact:
2023-09-16 12:15:03 +00:00
"enum.status-display-style.compact"
2023-01-07 16:44:25 +00:00
}
}
}
2023-02-21 06:23:42 +00:00
2023-02-21 06:37:16 +00:00
private var _cachedChoosenFont: UIFont?
public var chosenFont: UIFont? {
get {
2023-02-21 06:37:16 +00:00
if let _cachedChoosenFont {
return _cachedChoosenFont
}
guard let chosenFontData,
let font = try? NSKeyedUnarchiver.unarchivedObject(ofClass: UIFont.self, from: chosenFontData) else { return nil }
2023-02-21 06:37:16 +00:00
_cachedChoosenFont = font
return font
}
set {
if let font = newValue,
let data = try? NSKeyedArchiver.archivedData(withRootObject: font, requiringSecureCoding: false)
{
chosenFontData = data
} else {
chosenFontData = nil
}
2023-02-21 06:37:16 +00:00
_cachedChoosenFont = nil
}
}
2023-01-17 10:36:01 +00:00
2023-09-18 19:03:52 +00:00
let themeStorage = ThemeStorage()
2023-09-18 19:03:52 +00:00
public var isThemePreviouslySet: Bool {
didSet {
themeStorage.isThemePreviouslySet = isThemePreviouslySet
}
}
2023-09-18 19:03:52 +00:00
public var selectedScheme: ColorScheme {
didSet {
themeStorage.selectedScheme = selectedScheme
}
}
public var tintColor: Color {
didSet {
themeStorage.tintColor = tintColor
computeContrastingTintColor()
2023-09-18 19:03:52 +00:00
}
}
public var primaryBackgroundColor: Color {
didSet {
themeStorage.primaryBackgroundColor = primaryBackgroundColor
computeContrastingTintColor()
2023-09-18 19:03:52 +00:00
}
}
public var secondaryBackgroundColor: Color {
didSet {
themeStorage.secondaryBackgroundColor = secondaryBackgroundColor
}
}
public var labelColor: Color {
didSet {
themeStorage.labelColor = labelColor
computeContrastingTintColor()
}
}
public private(set) var contrastingTintColor: Color
// set contrastingTintColor to either labelColor or primaryBackgroundColor, whichever contrasts
// better against the tintColor
private func computeContrastingTintColor() {
func luminance(_ color: Color.Resolved) -> Float {
2024-02-14 11:48:14 +00:00
return 0.299 * color.red + 0.587 * color.green + 0.114 * color.blue
}
2024-02-13 16:21:33 +00:00
let resolvedTintColor = tintColor.resolve(in: .init())
let resolvedLabelColor = labelColor.resolve(in: .init())
let resolvedPrimaryBackgroundColor = primaryBackgroundColor.resolve(in: .init())
let tintLuminance = luminance(resolvedTintColor)
let labelLuminance = luminance(resolvedLabelColor)
let primaryBackgroundLuminance = luminance(resolvedPrimaryBackgroundColor)
if abs(tintLuminance - labelLuminance) > abs(tintLuminance - primaryBackgroundLuminance) {
contrastingTintColor = labelColor
} else {
contrastingTintColor = primaryBackgroundColor
2023-09-18 19:03:52 +00:00
}
}
public var avatarPosition: AvatarPosition {
2023-09-18 19:03:52 +00:00
didSet {
themeStorage.avatarPosition = avatarPosition
2023-09-18 19:03:52 +00:00
}
}
public var avatarShape: AvatarShape {
2023-09-18 19:03:52 +00:00
didSet {
themeStorage.avatarShape = avatarShape
2023-09-18 19:03:52 +00:00
}
}
private var storedSet: ColorSetName {
didSet {
themeStorage.storedSet = storedSet
}
}
public var statusActionsDisplay: StatusActionsDisplay {
didSet {
themeStorage.statusActionsDisplay = statusActionsDisplay
}
}
public var statusDisplayStyle: StatusDisplayStyle {
didSet {
themeStorage.statusDisplayStyle = statusDisplayStyle
}
}
2024-02-14 11:48:14 +00:00
public var statusActionSecondary: StatusActionSecondary {
didSet {
themeStorage.statusActionSecondary = statusActionSecondary
}
}
2023-09-18 19:03:52 +00:00
public var followSystemColorScheme: Bool {
didSet {
themeStorage.followSystemColorScheme = followSystemColorScheme
}
}
public var displayFullUsername: Bool {
didSet {
themeStorage.displayFullUsername = displayFullUsername
}
}
public var lineSpacing: Double {
didSet {
themeStorage.lineSpacing = lineSpacing
}
}
public var fontSizeScale: Double {
didSet {
themeStorage.fontSizeScale = fontSizeScale
}
}
public private(set) var chosenFontData: Data? {
didSet {
themeStorage.chosenFontData = chosenFontData
}
}
2023-09-20 05:28:04 +00:00
public var selectedSet: ColorSetName = .iceCubeDark
2023-01-17 10:36:01 +00:00
public static let shared = Theme()
2023-01-17 10:36:01 +00:00
2024-01-13 07:48:29 +00:00
public func restoreDefault() {
applySet(set: themeStorage.selectedScheme == .dark ? .iceCubeDark : .iceCubeLight)
isThemePreviouslySet = true
avatarPosition = .leading
avatarShape = .circle
storedSet = selectedSet
statusActionsDisplay = .full
statusDisplayStyle = .large
followSystemColorScheme = true
displayFullUsername = false
lineSpacing = 1.2
fontSizeScale = 1
chosenFontData = nil
statusActionSecondary = .share
}
2024-02-14 11:48:14 +00:00
private init() {
2023-09-18 19:03:52 +00:00
isThemePreviouslySet = themeStorage.isThemePreviouslySet
selectedScheme = themeStorage.selectedScheme
tintColor = themeStorage.tintColor
primaryBackgroundColor = themeStorage.primaryBackgroundColor
secondaryBackgroundColor = themeStorage.secondaryBackgroundColor
labelColor = themeStorage.labelColor
contrastingTintColor = .red // real work done in computeContrastingTintColor()
avatarPosition = themeStorage.avatarPosition
avatarShape = themeStorage.avatarShape
2023-09-18 19:03:52 +00:00
storedSet = themeStorage.storedSet
statusActionsDisplay = themeStorage.statusActionsDisplay
statusDisplayStyle = themeStorage.statusDisplayStyle
followSystemColorScheme = themeStorage.followSystemColorScheme
displayFullUsername = themeStorage.displayFullUsername
lineSpacing = themeStorage.lineSpacing
fontSizeScale = themeStorage.fontSizeScale
chosenFontData = themeStorage.chosenFontData
statusActionSecondary = themeStorage.statusActionSecondary
selectedSet = storedSet
computeContrastingTintColor()
}
2023-01-17 10:36:01 +00:00
public static var allColorSet: [ColorSet] {
[
IceCubeDark(),
IceCubeLight(),
2023-01-21 17:40:35 +00:00
IceCubeNeonDark(),
IceCubeNeonLight(),
DesertDark(),
DesertLight(),
NemesisDark(),
2023-01-17 10:36:01 +00:00
NemesisLight(),
2023-01-19 10:58:38 +00:00
MediumLight(),
MediumDark(),
2023-08-30 06:02:38 +00:00
ConstellationLight(),
ConstellationDark(),
2023-12-29 07:01:09 +00:00
ThreadsLight(),
2024-02-14 11:48:14 +00:00
ThreadsDark(),
]
2022-12-31 05:48:09 +00:00
}
2023-09-20 06:20:01 +00:00
public func applySet(set: ColorSetName) {
selectedSet = set
setColor(withName: set)
}
2023-01-17 10:36:01 +00:00
public func setColor(withName name: ColorSetName) {
let colorSet = Theme.allColorSet.filter { $0.name == name }.first ?? IceCubeDark()
2023-01-17 10:36:01 +00:00
selectedScheme = colorSet.scheme
tintColor = colorSet.tintColor
primaryBackgroundColor = colorSet.primaryBackgroundColor
secondaryBackgroundColor = colorSet.secondaryBackgroundColor
labelColor = colorSet.labelColor
storedSet = name
2022-12-31 05:48:09 +00:00
}
2022-12-24 13:55:04 +00:00
}