2022-12-31 11:29:19 +00:00
|
|
|
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
|
2023-09-20 19:19:45 +00:00
|
|
|
case avatarPosition2, avatarShape2, statusActionsDisplay, statusDisplayStyle
|
2023-09-18 19:03:52 +00:00
|
|
|
case selectedSet, selectedScheme
|
|
|
|
case followSystemColorSchme
|
|
|
|
case displayFullUsernameTimeline
|
|
|
|
case lineSpacing
|
2023-12-28 06:48:35 +00:00
|
|
|
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
|
2023-12-28 06:48:35 +00:00
|
|
|
@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() {}
|
2022-12-31 11:29:19 +00:00
|
|
|
}
|
2023-01-30 06:27:06 +00:00
|
|
|
|
|
|
|
public enum FontState: Int, CaseIterable {
|
|
|
|
case system
|
2023-02-06 17:15:08 +00:00
|
|
|
case openDyslexic
|
|
|
|
case hyperLegible
|
2023-02-21 06:08:32 +00:00
|
|
|
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"
|
2023-02-06 17:15:08 +00:00
|
|
|
case .openDyslexic:
|
2023-09-16 12:15:03 +00:00
|
|
|
"Open Dyslexic"
|
2023-02-06 17:15:08 +00:00
|
|
|
case .hyperLegible:
|
2023-09-16 12:15:03 +00:00
|
|
|
"Hyper Legible"
|
2023-02-21 06:08:32 +00:00
|
|
|
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:25:55 +00:00
|
|
|
}
|
2023-01-30 06:27:06 +00:00
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2022-12-31 11:29:19 +00:00
|
|
|
public enum AvatarPosition: String, CaseIterable {
|
|
|
|
case leading, top
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2022-12-31 11:29:19 +00:00
|
|
|
public var description: LocalizedStringKey {
|
|
|
|
switch self {
|
2023-01-02 16:18:16 +00:00
|
|
|
case .leading:
|
2023-09-16 12:15:03 +00:00
|
|
|
"enum.avatar-position.leading"
|
2023-01-02 16:18:16 +00:00
|
|
|
case .top:
|
2023-09-16 12:15:03 +00:00
|
|
|
"enum.avatar-position.top"
|
2022-12-31 05:48:09 +00:00
|
|
|
}
|
2022-12-30 19:55:23 +00:00
|
|
|
}
|
2022-12-31 05:48:09 +00:00
|
|
|
}
|
2024-02-14 11:48:14 +00:00
|
|
|
|
2023-12-28 06:48:35 +00:00
|
|
|
public enum StatusActionSecondary: String, CaseIterable {
|
|
|
|
case share, bookmark
|
2024-02-14 11:48:14 +00:00
|
|
|
|
2023-12-28 06:48:35 +00:00
|
|
|
public var description: LocalizedStringKey {
|
|
|
|
switch self {
|
|
|
|
case .share:
|
|
|
|
"status.action.share-title"
|
|
|
|
case .bookmark:
|
|
|
|
"status.action.bookmark"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-01-04 16:48:02 +00:00
|
|
|
|
|
|
|
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"
|
2023-01-04 16:48:02 +00:00
|
|
|
case .rounded:
|
2023-09-16 12:15:03 +00:00
|
|
|
"enum.avatar-shape.rounded"
|
2023-01-04 16:48:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
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?
|
2023-02-20 12:00:50 +00:00
|
|
|
public var chosenFont: UIFont? {
|
|
|
|
get {
|
2023-02-21 06:37:16 +00:00
|
|
|
if let _cachedChoosenFont {
|
|
|
|
return _cachedChoosenFont
|
|
|
|
}
|
2023-02-20 12:00:50 +00:00
|
|
|
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
|
2023-02-20 12:00:50 +00:00
|
|
|
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-02-20 12:00:50 +00:00
|
|
|
}
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-09-18 19:03:52 +00:00
|
|
|
let themeStorage = ThemeStorage()
|
2023-01-04 16:48:02 +00:00
|
|
|
|
2023-09-18 19:03:52 +00:00
|
|
|
public var isThemePreviouslySet: Bool {
|
|
|
|
didSet {
|
|
|
|
themeStorage.isThemePreviouslySet = isThemePreviouslySet
|
|
|
|
}
|
|
|
|
}
|
2023-01-04 16:48:02 +00:00
|
|
|
|
2023-09-18 19:03:52 +00:00
|
|
|
public var selectedScheme: ColorScheme {
|
|
|
|
didSet {
|
|
|
|
themeStorage.selectedScheme = selectedScheme
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public var tintColor: Color {
|
|
|
|
didSet {
|
|
|
|
themeStorage.tintColor = tintColor
|
2024-02-13 10:33:59 +00:00
|
|
|
computeContrastingTintColor()
|
2023-09-18 19:03:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public var primaryBackgroundColor: Color {
|
|
|
|
didSet {
|
|
|
|
themeStorage.primaryBackgroundColor = primaryBackgroundColor
|
2024-02-13 10:33:59 +00:00
|
|
|
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
|
2024-02-13 10:33:59 +00:00
|
|
|
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 10:33:59 +00:00
|
|
|
}
|
|
|
|
|
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())
|
2024-02-13 10:33:59 +00:00
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-20 19:19:45 +00:00
|
|
|
public var avatarPosition: AvatarPosition {
|
2023-09-18 19:03:52 +00:00
|
|
|
didSet {
|
2023-09-20 19:19:45 +00:00
|
|
|
themeStorage.avatarPosition = avatarPosition
|
2023-09-18 19:03:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-20 19:19:45 +00:00
|
|
|
public var avatarShape: AvatarShape {
|
2023-09-18 19:03:52 +00:00
|
|
|
didSet {
|
2023-09-20 19:19:45 +00:00
|
|
|
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
|
|
|
|
2023-12-28 06:48:35 +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
|
|
|
|
2023-01-17 06:39:13 +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
|
|
|
|
2023-01-17 06:39:13 +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
|
2024-02-13 10:33:59 +00:00
|
|
|
contrastingTintColor = .red // real work done in computeContrastingTintColor()
|
2023-09-20 19:19:45 +00:00
|
|
|
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
|
2023-12-28 06:48:35 +00:00
|
|
|
statusActionSecondary = themeStorage.statusActionSecondary
|
2023-01-02 16:18:16 +00:00
|
|
|
selectedSet = storedSet
|
2024-02-13 10:33:59 +00:00
|
|
|
|
|
|
|
computeContrastingTintColor()
|
2023-01-02 16:18:16 +00:00
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-02 16:18:16 +00:00
|
|
|
public static var allColorSet: [ColorSet] {
|
|
|
|
[
|
|
|
|
IceCubeDark(),
|
|
|
|
IceCubeLight(),
|
2023-01-21 17:40:35 +00:00
|
|
|
IceCubeNeonDark(),
|
|
|
|
IceCubeNeonLight(),
|
2023-01-02 16:18:16 +00:00
|
|
|
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(),
|
2023-01-02 16:18:16 +00:00
|
|
|
]
|
2022-12-31 05:48:09 +00:00
|
|
|
}
|
2023-10-01 07:37: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
|
|
|
|
2023-01-02 16:18:16 +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
|
|
|
}
|