diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index 760ca29..48a552c 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -81,6 +81,10 @@ F89992CE296D92E7005994BF /* AttachmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89992CD296D92E7005994BF /* AttachmentViewModel.swift */; }; F89A46DC296EAACE0062125F /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89A46DB296EAACE0062125F /* SettingsView.swift */; }; F89A46DE296EABA20062125F /* StatusPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89A46DD296EABA20062125F /* StatusPlaceholder.swift */; }; + F89D6C3F29716E41001DA3D4 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C3E29716E41001DA3D4 /* Theme.swift */; }; + F89D6C4229717FDC001DA3D4 /* AccountsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C4129717FDC001DA3D4 /* AccountsSection.swift */; }; + F89D6C4429718092001DA3D4 /* AccentsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C4329718092001DA3D4 /* AccentsSection.swift */; }; + F89D6C4629718193001DA3D4 /* ThemeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C4529718193001DA3D4 /* ThemeSection.swift */; }; F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.swift */; }; F8A93D802965FED4001D8331 /* AccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7F2965FED4001D8331 /* AccountService.swift */; }; F8C14392296AF0B3001FE31D /* String+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C14391296AF0B3001FE31D /* String+Exif.swift */; }; @@ -163,6 +167,11 @@ F89992CD296D92E7005994BF /* AttachmentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentViewModel.swift; sourceTree = ""; }; F89A46DB296EAACE0062125F /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; F89A46DD296EABA20062125F /* StatusPlaceholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPlaceholder.swift; sourceTree = ""; }; + F89D6C3B29716DBC001DA3D4 /* Vernissage20230113-001.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage20230113-001.xcdatamodel"; sourceTree = ""; }; + F89D6C3E29716E41001DA3D4 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; + F89D6C4129717FDC001DA3D4 /* AccountsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsSection.swift; sourceTree = ""; }; + F89D6C4329718092001DA3D4 /* AccentsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccentsSection.swift; sourceTree = ""; }; + F89D6C4529718193001DA3D4 /* ThemeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeSection.swift; sourceTree = ""; }; F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = ""; }; F8A93D7F2965FED4001D8331 /* AccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountService.swift; sourceTree = ""; }; F8AF2A61297073FE00D2DA3F /* Vernissage20230112-001.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage20230112-001.xcdatamodel"; sourceTree = ""; }; @@ -234,6 +243,7 @@ F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */, F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */, F8CC95CD2970761D00C9C2AC /* TintColor.swift */, + F89D6C3E29716E41001DA3D4 /* Theme.swift */, ); path = Models; sourceTree = ""; @@ -273,6 +283,7 @@ F83901A2295D863B00456AE2 /* Widgets */ = { isa = PBXGroup; children = ( + F89D6C4029717FC0001DA3D4 /* SettingsView */, F83901A5295D8EC000456AE2 /* LabelIcon.swift */, F85D4972296406E700751DF7 /* BottomRight.swift */, F85D497629640A5200751DF7 /* ImageRow.swift */, @@ -399,6 +410,16 @@ path = ViewModels; sourceTree = ""; }; + F89D6C4029717FC0001DA3D4 /* SettingsView */ = { + isa = PBXGroup; + children = ( + F89D6C4129717FDC001DA3D4 /* AccountsSection.swift */, + F89D6C4329718092001DA3D4 /* AccentsSection.swift */, + F89D6C4529718193001DA3D4 /* ThemeSection.swift */, + ); + path = SettingsView; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -480,6 +501,7 @@ files = ( F85D497729640A5200751DF7 /* ImageRow.swift in Sources */, F8210DDF2966CFC7001D9973 /* AttachmentData+Attachment.swift in Sources */, + F89D6C4229717FDC001DA3D4 /* AccountsSection.swift in Sources */, F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */, F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */, F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */, @@ -502,7 +524,9 @@ F8210DCF2966B600001D9973 /* ImageRowAsync.swift in Sources */, F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */, F85D497B29640C8200751DF7 /* UsernameRow.swift in Sources */, + F89D6C4429718092001DA3D4 /* AccentsSection.swift in Sources */, F85D497929640B9D00751DF7 /* ImagesCarousel.swift in Sources */, + F89D6C3F29716E41001DA3D4 /* Theme.swift in Sources */, F8CC95CE2970761D00C9C2AC /* TintColor.swift in Sources */, F89992CC296D9231005994BF /* StatusViewModel.swift in Sources */, F80048052961850500E6868A /* StatusData+CoreDataClass.swift in Sources */, @@ -523,6 +547,7 @@ F89A46DE296EABA20062125F /* StatusPlaceholder.swift in Sources */, F88C2482295C3A4F0006098B /* StatusView.swift in Sources */, F866F6A329604161002E8F88 /* AccountDataHandler.swift in Sources */, + F89D6C4629718193001DA3D4 /* ThemeSection.swift in Sources */, F85D497F296416C800751DF7 /* CommentsSection.swift in Sources */, F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */, F866F6A529604194002E8F88 /* ApplicationSettingsHandler.swift in Sources */, @@ -799,10 +824,11 @@ F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + F89D6C3B29716DBC001DA3D4 /* Vernissage20230113-001.xcdatamodel */, F8AF2A61297073FE00D2DA3F /* Vernissage20230112-001.xcdatamodel */, F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */, ); - currentVersion = F8AF2A61297073FE00D2DA3F /* Vernissage20230112-001.xcdatamodel */; + currentVersion = F89D6C3B29716DBC001DA3D4 /* Vernissage20230113-001.xcdatamodel */; path = Vernissage.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Vernissage/CoreData/ApplicationSettings+CoreDataProperties.swift b/Vernissage/CoreData/ApplicationSettings+CoreDataProperties.swift index a762e48..4b04946 100644 --- a/Vernissage/CoreData/ApplicationSettings+CoreDataProperties.swift +++ b/Vernissage/CoreData/ApplicationSettings+CoreDataProperties.swift @@ -3,13 +3,10 @@ // Copyright © 2023 Marcin Czachurski and the repository contributors. // Licensed under the MIT License. // - -// import Foundation import CoreData - extension ApplicationSettings { @nonobjc public class func fetchRequest() -> NSFetchRequest { @@ -17,6 +14,11 @@ extension ApplicationSettings { } @NSManaged public var currentAccount: String? + @NSManaged public var theme: Int32 @NSManaged public var tintColor: Int32 } + +extension ApplicationSettings : Identifiable { + +} diff --git a/Vernissage/CoreData/ApplicationSettingsHandler.swift b/Vernissage/CoreData/ApplicationSettingsHandler.swift index 2ae294a..d9ac916 100644 --- a/Vernissage/CoreData/ApplicationSettingsHandler.swift +++ b/Vernissage/CoreData/ApplicationSettingsHandler.swift @@ -43,6 +43,12 @@ class ApplicationSettingsHandler { defaultSettings.tintColor = Int32(tintColor.rawValue) CoreDataHandler.shared.save() } + + func setDefaultTheme(theme: Theme) { + let defaultSettings = self.getDefaultSettings() + defaultSettings.theme = Int32(theme.rawValue) + CoreDataHandler.shared.save() + } private func createApplicationSettingsEntity() -> ApplicationSettings { let context = CoreDataHandler.shared.container.viewContext diff --git a/Vernissage/Models/ApplicationState.swift b/Vernissage/Models/ApplicationState.swift index fadac96..f4c9da1 100644 --- a/Vernissage/Models/ApplicationState.swift +++ b/Vernissage/Models/ApplicationState.swift @@ -14,6 +14,7 @@ public class ApplicationState: ObservableObject { @Published var accountData: AccountData? @Published var tintColor = TintColor.accentColor2 + @Published var theme = Theme.system @Published var showInteractionStatusId = "" } diff --git a/Vernissage/Models/Theme.swift b/Vernissage/Models/Theme.swift new file mode 100644 index 0000000..9a60b24 --- /dev/null +++ b/Vernissage/Models/Theme.swift @@ -0,0 +1,22 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI + +public enum Theme: Int { + case system, light, dark + + public func colorScheme() -> ColorScheme? { + switch self { + case .system: + return nil + case .light: + return ColorScheme.light + case .dark: + return ColorScheme.dark + } + } +} diff --git a/Vernissage/Vernissage.xcdatamodeld/.xccurrentversion b/Vernissage/Vernissage.xcdatamodeld/.xccurrentversion index d4bfb6d..c5fbcc6 100644 --- a/Vernissage/Vernissage.xcdatamodeld/.xccurrentversion +++ b/Vernissage/Vernissage.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Vernissage20230112-001.xcdatamodel + Vernissage20230113-001.xcdatamodel diff --git a/Vernissage/Vernissage.xcdatamodeld/Vernissage20230113-001.xcdatamodel/contents b/Vernissage/Vernissage.xcdatamodeld/Vernissage20230113-001.xcdatamodel/contents new file mode 100644 index 0000000..428615e --- /dev/null +++ b/Vernissage/Vernissage.xcdatamodeld/Vernissage20230113-001.xcdatamodel/contents @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Vernissage/VernissageApp.swift b/Vernissage/VernissageApp.swift index 0b82b27..2760353 100644 --- a/Vernissage/VernissageApp.swift +++ b/Vernissage/VernissageApp.swift @@ -15,6 +15,7 @@ struct VernissageApp: App { @State var applicationViewMode: ApplicationViewMode = .loading @State var tintColor = ApplicationState.shared.tintColor.color() + @State var theme = ApplicationState.shared.theme.colorScheme() var body: some Scene { WindowGroup { @@ -31,19 +32,28 @@ struct VernissageApp: App { case .mainView: MainView { color in self.tintColor = color.color() + } onThemeChange: { theme in + self.theme = theme.colorScheme() } .environment(\.managedObjectContext, coreDataHandler.container.viewContext) .environmentObject(applicationState) } } .tint(self.tintColor) + .preferredColorScheme(self.theme) .task { let defaultSettings = ApplicationSettingsHandler.shared.getDefaultSettings() + if let tintColor = TintColor(rawValue: Int(defaultSettings.tintColor)) { self.applicationState.tintColor = tintColor self.tintColor = tintColor.color() } + if let theme = Theme(rawValue: Int(defaultSettings.theme)) { + self.applicationState.theme = theme + self.theme = theme.colorScheme() + } + await AuthorizationService.shared.verifyAccount({ accountData in guard let accountData = accountData else { self.applicationViewMode = .signIn diff --git a/Vernissage/Views/MainView.swift b/Vernissage/Views/MainView.swift index 3cd7da8..66d2cf2 100644 --- a/Vernissage/Views/MainView.swift +++ b/Vernissage/Views/MainView.swift @@ -19,6 +19,7 @@ struct MainView: View { @EnvironmentObject var applicationState: ApplicationState var onTintChange: ((TintColor) -> Void)? + var onThemeChange: ((Theme) -> Void)? @State private var showSettings = false @State private var sheet: Sheet? @@ -45,6 +46,8 @@ struct MainView: View { case .settings: SettingsView { color in self.onTintChange?(color) + } onThemeChange: { theme in + self.onThemeChange?(theme) } case .compose: ComposeView(statusViewModel: .constant(nil)) diff --git a/Vernissage/Views/SettingsView.swift b/Vernissage/Views/SettingsView.swift index 0ff9876..68263cb 100644 --- a/Vernissage/Views/SettingsView.swift +++ b/Vernissage/Views/SettingsView.swift @@ -8,105 +8,46 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject var applicationState: ApplicationState + @Environment(\.colorScheme) var colorScheme @Environment(\.dismiss) private var dismiss - @State var accounts: [AccountData] = [] - @State private var matchSystemTheme = true + @State private var theme: ColorScheme? + @State private var appVersion: String? var onTintChange: ((TintColor) -> Void)? - - let accentColors1: [TintColor] = [.accentColor1, .accentColor2, .accentColor3, .accentColor4, .accentColor5] - let accentColors2: [TintColor] = [.accentColor6, .accentColor7, .accentColor8, .accentColor9, .accentColor10] - + var onThemeChange: ((Theme) -> Void)? + var body: some View { NavigationView { List { - Section("Accounts") { - ForEach(self.accounts) { account in - UsernameRow(accountAvatar: account.avatar, - accountDisplayName: account.displayName, - accountUsername: account.username, - cachedAvatar: CacheAvatarService.shared.getImage(for: account.id)) - } - - NavigationLink(destination: SignInView()) { - HStack { - Text("New account") - Spacer() - Image(systemName: "person.crop.circle.badge.plus") - } - } - } - - Section("Theme") { - Toggle("Match system", isOn: $matchSystemTheme) - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("Light") - Text("Dark") + // Accounts. + AccountsSection() + + // Themes. + ThemeSection { theme in + changeTheme(theme: theme) } - Section("Accent") { - VStack(alignment: .leading) { - HStack(alignment: .center) { - ForEach(accentColors1, id: \.self) { color in - ZStack { - Circle() - .fill(color.color()) - .frame(width: 36, height: 36) - .onTapGesture { - self.applicationState.tintColor = color - ApplicationSettingsHandler.shared.setDefaultTintColor(tintColor: color) - self.onTintChange?(color) - } - if color == self.applicationState.tintColor { - Image(systemName: "checkmark") - .tint(Color.mainTextColor) - .fontWeight(.bold) - } - } - - if color != accentColors1.last { - Spacer() - } - } - } - .padding(.vertical, 8) - - HStack(alignment: .center) { - ForEach(accentColors2, id: \.self) { color in - ZStack { - Circle() - .fill(color.color()) - .frame(width: 36, height: 36) - .onTapGesture { - self.applicationState.tintColor = color - ApplicationSettingsHandler.shared.setDefaultTintColor(tintColor: color) - self.onTintChange?(color) - } - if color == self.applicationState.tintColor { - Image(systemName: "checkmark") - .tint(Color.mainTextColor) - .fontWeight(.bold) - } - } - - if color != accentColors2.last { - Spacer() - } - } - } - .padding(.vertical, 8) - } + // Accents. + AccentsSection { color in + self.onTintChange?(color) } + // Other. Section("Other") { Text("Third party") // Link to dependeinces Text("Report a bug") Text("Follow me on Mastodon") } + // Version. Section() { - Text("Version") // Link to dependeinces + HStack { + Text("Version") + Spacer() + Text(appVersion ?? "") + .foregroundColor(.accentColor) + } } } .frame(alignment: .topLeading) @@ -118,11 +59,28 @@ struct SettingsView: View { } } .task { - self.accounts = AccountDataHandler.shared.getAccountsData() + self.appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String } + .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification), perform: { _ in + self.theme = applicationState.theme.colorScheme() ?? self.getSystemColorScheme() + }) .navigationBarTitle(Text("Settings"), displayMode: .inline) + .preferredColorScheme(self.theme) } } + + private func changeTheme(theme: Theme) { + // Change theme of current modal screen (unformtunatelly it's not changed autmatically_ + self.theme = theme.colorScheme() ?? self.getSystemColorScheme() + + self.applicationState.theme = theme + ApplicationSettingsHandler.shared.setDefaultTheme(theme: theme) + onThemeChange?(theme) + } + + func getSystemColorScheme() -> ColorScheme { + return UITraitCollection.current.userInterfaceStyle == .light ? .light : .dark + } } struct SettingsView_Previews: PreviewProvider { diff --git a/Vernissage/Widgets/SettingsView/AccentsSection.swift b/Vernissage/Widgets/SettingsView/AccentsSection.swift new file mode 100644 index 0000000..c7e1c97 --- /dev/null +++ b/Vernissage/Widgets/SettingsView/AccentsSection.swift @@ -0,0 +1,79 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import SwiftUI + +struct AccentsSection: View { + @EnvironmentObject var applicationState: ApplicationState + + var onTintChange: ((TintColor) -> Void)? + + private let accentColors1: [TintColor] = [.accentColor1, .accentColor2, .accentColor3, .accentColor4, .accentColor5] + private let accentColors2: [TintColor] = [.accentColor6, .accentColor7, .accentColor8, .accentColor9, .accentColor10] + + var body: some View { + Section("Accent") { + VStack(alignment: .leading) { + HStack(alignment: .center) { + ForEach(accentColors1, id: \.self) { color in + ZStack { + Circle() + .fill(color.color()) + .frame(width: 36, height: 36) + .onTapGesture { + self.applicationState.tintColor = color + ApplicationSettingsHandler.shared.setDefaultTintColor(tintColor: color) + self.onTintChange?(color) + } + if color == self.applicationState.tintColor { + Image(systemName: "checkmark") + .foregroundColor(Color.white) + .fontWeight(.bold) + } + } + + if color != accentColors1.last { + Spacer() + } + } + } + .padding(.vertical, 8) + + HStack(alignment: .center) { + ForEach(accentColors2, id: \.self) { color in + ZStack { + Circle() + .fill(color.color()) + .frame(width: 36, height: 36) + .onTapGesture { + self.applicationState.tintColor = color + ApplicationSettingsHandler.shared.setDefaultTintColor(tintColor: color) + self.onTintChange?(color) + } + if color == self.applicationState.tintColor { + Image(systemName: "checkmark") + .foregroundColor(Color.white) + .fontWeight(.bold) + } + } + + if color != accentColors2.last { + Spacer() + } + } + } + .padding(.vertical, 8) + } + } + } +} + +struct AccentsSection_Previews: PreviewProvider { + static var previews: some View { + AccentsSection() + } +} diff --git a/Vernissage/Widgets/SettingsView/AccountsSection.swift b/Vernissage/Widgets/SettingsView/AccountsSection.swift new file mode 100644 index 0000000..2677f31 --- /dev/null +++ b/Vernissage/Widgets/SettingsView/AccountsSection.swift @@ -0,0 +1,40 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import SwiftUI + +struct AccountsSection: View { + @State private var accounts: [AccountData] = [] + + var body: some View { + Section("Accounts") { + ForEach(self.accounts) { account in + UsernameRow(accountAvatar: account.avatar, + accountDisplayName: account.displayName, + accountUsername: account.username, + cachedAvatar: CacheAvatarService.shared.getImage(for: account.id)) + } + + NavigationLink(destination: SignInView()) { + HStack { + Text("New account") + Spacer() + Image(systemName: "person.crop.circle.badge.plus") + } + } + } + .task { + self.accounts = AccountDataHandler.shared.getAccountsData() + } + } +} + +struct AccountsSection_Previews: PreviewProvider { + static var previews: some View { + AccountsSection() + } +} diff --git a/Vernissage/Widgets/SettingsView/ThemeSection.swift b/Vernissage/Widgets/SettingsView/ThemeSection.swift new file mode 100644 index 0000000..a7bda18 --- /dev/null +++ b/Vernissage/Widgets/SettingsView/ThemeSection.swift @@ -0,0 +1,63 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI + +struct ThemeSection: View { + @EnvironmentObject var applicationState: ApplicationState + @Environment(\.colorScheme) var colorScheme + + var onThemeChange: ((Theme) -> Void)? + + var body: some View { + Section("Theme") { + Button { + onThemeChange?(.system) + } label: { + HStack { + Text("System") + .foregroundColor(.label) + Spacer() + if self.applicationState.theme == .system { + Image(systemName: "checkmark") + } + } + } + + Button { + onThemeChange?(.light) + } label: { + HStack { + Text("Light") + .foregroundColor(.label) + Spacer() + if self.applicationState.theme == .light { + Image(systemName: "checkmark") + } + } + } + + Button { + onThemeChange?(.dark) + } label: { + HStack { + Text("Dark") + .foregroundColor(.label) + Spacer() + if self.applicationState.theme == .dark { + Image(systemName: "checkmark") + } + } + } + } + } +} + +struct ThemeSection_Previews: PreviewProvider { + static var previews: some View { + ThemeSection() + } +}