From 050053a69a4b204a162efe4bcefecc2789ada20b Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Sat, 11 May 2024 11:18:52 -0400 Subject: [PATCH] Color Profiles (#1058) Co-authored-by: Sjmarf <78750526+Sjmarf@users.noreply.github.com> --- CONTRIBUTING.md | 54 ++++++++++++- Mlem.xcodeproj/project.pbxproj | 40 ++++++++-- .../xcshareddata/swiftpm/Package.resolved | 13 ++- Mlem/App/Constants/Colors.swift | 21 ----- .../Constants/Colors/MonochromePalette.swift | 23 ++++++ .../Constants/Colors/StandardPalette.swift | 22 ++++++ .../Notifier/NotificationDisplayer.swift | 5 +- Mlem/App/Globals/Definitions/Palette.swift | 79 +++++++++++++++++++ .../Models/Helpers/Action/BasicAction.swift | 1 + .../Interactable1Providing+Extensions.swift | 6 +- Mlem/App/Views/Root/ContentView.swift | 7 ++ .../App/Views/Root/Tabs/Feeds/FeedsView.swift | 5 +- .../Root/Tabs/Settings/SettingsView.swift | 41 ++++++++++ .../Views/Shared/CustomTabBarController.swift | 3 +- Mlem/App/Views/Shared/CustomTabView.swift | 12 ++- Mlem/App/Views/Shared/Markdown.swift | 5 +- .../Shared/Navigation/NavigationPage.swift | 2 +- 17 files changed, 298 insertions(+), 41 deletions(-) delete mode 100644 Mlem/App/Constants/Colors.swift create mode 100644 Mlem/App/Constants/Colors/MonochromePalette.swift create mode 100644 Mlem/App/Constants/Colors/StandardPalette.swift create mode 100644 Mlem/App/Globals/Definitions/Palette.swift create mode 100644 Mlem/App/Views/Root/Tabs/Settings/SettingsView.swift diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08f9dfa3b..d90d9f559 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ With these steps completed each time you build your code will be linted, and eac ## Getting started -To avoid having multiple in-flight tasks working on the same part of the codebase, we have a set procedure for claiming and performing work. If you don't follow it, your PR will *probably* be rejected (unless it's really *that* good). +To avoid having multiple in-flight tasks working on the same part of the codebase, we have a set procedure for claiming and performing work. If you don't follow it, your PR will _probably_ be rejected (unless it's really _that_ good). 1. Go to our [project board](https://github.com/orgs/mlemgroup/projects/1/views/1). 2. Find an unassigned issue under the "Todo" section that you'd like to work on. @@ -39,10 +39,58 @@ When your code is approved, it can be merged into the `dev` branch by a member o ## Conventions Please develop according to the following principles: -- One View per file. A file containing a View struct must end in "View". We're yet to decide on an official naming scheme for files - feel free to offer your thoughts [here](https://github.com/mlemgroup/mlem/issues/55). + +- Files should be named according to the following patterns: + - All files: `TitleCase`. If the file contains extensions, it should be named `BaseEntity+Extensions`. + - `View` files: file name must end in `View` (e.g., `FeedsView`) +- One View per file. - Within reason, any complex of views that renders a single component of a larger view should be placed in a descriptively named function, computed property or `@ViewBuilder` variable beneath the body of the View. This keeps pyramids from piling up and makes our accessibility experts' work easier. - If you can reuse code, do. Prefer abstracting common components to a generic struct and common logic to a generic function. +## View Structure + +All `View` structs should be organized according to the following template: + +``` +struct SomeView: View { + @AppStorage values + @Environment entities + @Binding variables + @State variables + Normal variables + Computed properties + + // if necessary + init() { ... } + + var body: some View { ... } + + // if necessary + var content: some View { ... } + + Helper views +} +``` + +Further notes: + +- If the view has modifiers that are attached to the entire body, place the view definition in `content` and attach these modifiers to it in `body` (see `ContentView.swift` for an example). +- Prefer `var helper: some View` to `func helper() -> some View` unless the helper view takes in parameters. +- Helper views should always appear lower in the file than the view they help. + +## Global Objects + +There are several objects (e.g., `AppState`) that need to be available anywhere in the app. Normally this is handled with `@Environment`, but this is not available outside of the context of a `View`. To address this, globals that need to be available outside of a `View` define a `static var main: GlobalObject = .init()`, allowing them to be referenced as `GlobalObject.main`. This definition should be placed immediately above the initializer. + +This pattern should be used only where necessary, and should not be blindly applied to any global object. Likewise, if possible, these objects should be referenced via `@Environment(GlobalObject.self) var globalObject`; the static singleton should be considered a last resort. + +## Colors + +Colors are managed using the globally available `Palette` object, which enables color themes. The following conventions apply: + +- Avoid referencing `Color` directly; always use a `Palette` color. +- Prefer semantic over literal colors (e.g., `.upvote` over `.blue`). + ## Testing -We operate a Lemmy Instance at https://test-mlem.jo.wtf/ which you may use for testing purposes. +We operate a Lemmy Instance at https://test-mlem.jo.wtf/ which you may use for testing purposes. Please note that, as of 2024-05-10, it is running Lemmy v17, which is no longer used by any major Lemmy instance and thus we do not bother maintaining compatibility for. You may wish to use a local Lemmy instance instead. diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 966cbbcb8..9a3f4132a 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -50,12 +50,14 @@ CD1446212A5B328E00610EF1 /* Privacy Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1446202A5B328E00610EF1 /* Privacy Policy.swift */; }; CD1446252A5B357900610EF1 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1446242A5B357900610EF1 /* Document.swift */; }; CD1446272A5B36DA00610EF1 /* EULA.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1446262A5B36DA00610EF1 /* EULA.swift */; }; + CD317D4C2BE97FED008F63E2 /* Palette.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD317D4B2BE97FED008F63E2 /* Palette.swift */; }; + CD317D4F2BE983ED008F63E2 /* MonochromePalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD317D4E2BE983ED008F63E2 /* MonochromePalette.swift */; }; CD4368C12AE23FD400BD8BD1 /* Semaphore in Frameworks */ = {isa = PBXBuildFile; productRef = CD4368C02AE23FD400BD8BD1 /* Semaphore */; }; CD4BAD352B4B2C0B00A1E726 /* FeedsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */; }; CD4D583F2B86855F00B82964 /* MlemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D583E2B86855F00B82964 /* MlemTests.swift */; }; CD4D58412B86858100B82964 /* MlemUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58402B86858100B82964 /* MlemUITests.swift */; }; CD4D58742B86B4AA00B82964 /* AccountsTracker+Dependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58732B86B4AA00B82964 /* AccountsTracker+Dependency.swift */; }; - CD4D58932B86BA5C00B82964 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58922B86BA5C00B82964 /* Colors.swift */; }; + CD4D58932B86BA5C00B82964 /* StandardPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58922B86BA5C00B82964 /* StandardPalette.swift */; }; CD4D58972B86BAD900B82964 /* EnvironmentValues+TabReselectionHashValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58962B86BAD900B82964 /* EnvironmentValues+TabReselectionHashValue.swift */; }; CD4D58992B86BB0300B82964 /* EnvironmentValues+TabSelectionHashValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58982B86BB0300B82964 /* EnvironmentValues+TabSelectionHashValue.swift */; }; CD4D58A52B86BD1B00B82964 /* PersistenceRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58A42B86BD1B00B82964 /* PersistenceRepository.swift */; }; @@ -95,6 +97,8 @@ CDA1E8542B952C3D007953EF /* StateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA1E8532B952C3D007953EF /* StateManager.swift */; }; CDC199EA2BE449790077B4F1 /* Interactable1Providing+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC199E92BE449790077B4F1 /* Interactable1Providing+Extensions.swift */; }; CDC199EC2BE449EA0077B4F1 /* Post1Providing+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC199EB2BE449EA0077B4F1 /* Post1Providing+Extensions.swift */; }; + CDC199EE2BE44A200077B4F1 /* Post2Providing+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC199ED2BE44A200077B4F1 /* Post2Providing+Extensions.swift */; }; + CDEB38B42BED7CAE0059FBA7 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEB38B32BED7CAE0059FBA7 /* SettingsView.swift */; }; CDF9EF332AB2845C003F885B /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF9EF322AB2845C003F885B /* Icons.swift */; }; /* End PBXBuildFile section */ @@ -155,11 +159,13 @@ CD1446202A5B328E00610EF1 /* Privacy Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Privacy Policy.swift"; sourceTree = ""; }; CD1446242A5B357900610EF1 /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; CD1446262A5B36DA00610EF1 /* EULA.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EULA.swift; sourceTree = ""; }; + CD317D4B2BE97FED008F63E2 /* Palette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Palette.swift; sourceTree = ""; }; + CD317D4E2BE983ED008F63E2 /* MonochromePalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonochromePalette.swift; sourceTree = ""; }; CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedsView.swift; sourceTree = ""; }; CD4D583E2B86855F00B82964 /* MlemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MlemTests.swift; sourceTree = ""; }; CD4D58402B86858100B82964 /* MlemUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MlemUITests.swift; sourceTree = ""; }; CD4D58732B86B4AA00B82964 /* AccountsTracker+Dependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountsTracker+Dependency.swift"; sourceTree = ""; }; - CD4D58922B86BA5C00B82964 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; + CD4D58922B86BA5C00B82964 /* StandardPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardPalette.swift; sourceTree = ""; }; CD4D58962B86BAD900B82964 /* EnvironmentValues+TabReselectionHashValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+TabReselectionHashValue.swift"; sourceTree = ""; }; CD4D58982B86BB0300B82964 /* EnvironmentValues+TabSelectionHashValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+TabSelectionHashValue.swift"; sourceTree = ""; }; CD4D58A42B86BD1B00B82964 /* PersistenceRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistenceRepository.swift; sourceTree = ""; }; @@ -197,6 +203,8 @@ CDA1E8532B952C3D007953EF /* StateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateManager.swift; sourceTree = ""; }; CDC199E92BE449790077B4F1 /* Interactable1Providing+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Interactable1Providing+Extensions.swift"; sourceTree = ""; }; CDC199EB2BE449EA0077B4F1 /* Post1Providing+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post1Providing+Extensions.swift"; sourceTree = ""; }; + CDC199ED2BE44A200077B4F1 /* Post2Providing+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post2Providing+Extensions.swift"; sourceTree = ""; }; + CDEB38B32BED7CAE0059FBA7 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; CDF9EF322AB2845C003F885B /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icons.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -347,6 +355,7 @@ 6363D5F427EE1BAE00E34822 /* Tabs */ = { isa = PBXGroup; children = ( + CDEB38B22BED7CA30059FBA7 /* Settings */, CD4BAD382B4C6C1B00A1E726 /* Feeds */, 6DE1183A2A4A215F00810C7E /* Profile */, ); @@ -373,6 +382,15 @@ path = Data; sourceTree = ""; }; + CD317D4D2BE97FFB008F63E2 /* Colors */ = { + isa = PBXGroup; + children = ( + CD4D58922B86BA5C00B82964 /* StandardPalette.swift */, + CD317D4E2BE983ED008F63E2 /* MonochromePalette.swift */, + ); + path = Colors; + sourceTree = ""; + }; CD4BAD382B4C6C1B00A1E726 /* Feeds */ = { isa = PBXGroup; children = ( @@ -414,9 +432,9 @@ CD4D583D2B867DA900B82964 /* Constants */ = { isa = PBXGroup; children = ( + CD317D4D2BE97FFB008F63E2 /* Colors */, 63DF71F02A02999C002AC14E /* AppConstants.swift */, CDF9EF322AB2845C003F885B /* Icons.swift */, - CD4D58922B86BA5C00B82964 /* Colors.swift */, ); path = Constants; sourceTree = ""; @@ -578,11 +596,12 @@ isa = PBXGroup; children = ( CD4D58B22B86BFD400B82964 /* AccountsTracker.swift */, + CDA1E8262B90EF24007953EF /* AppFlow.swift */, 036CC3AE2B8145C30098B6A1 /* AppState.swift */, + CD317D4B2BE97FED008F63E2 /* Palette.swift */, CD4D58A42B86BD1B00B82964 /* PersistenceRepository.swift */, CD4D58FB2B87B13B00B82964 /* ErrorHandler */, CD4D59042B87B19100B82964 /* Notifier */, - CDA1E8262B90EF24007953EF /* AppFlow.swift */, ); path = Definitions; sourceTree = ""; @@ -614,6 +633,14 @@ path = "Content Models"; sourceTree = ""; }; + CDEB38B22BED7CA30059FBA7 /* Settings */ = { + isa = PBXGroup; + children = ( + CDEB38B32BED7CAE0059FBA7 /* SettingsView.swift */, + ); + path = Settings; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -821,14 +848,17 @@ CD1446272A5B36DA00610EF1 /* EULA.swift in Sources */, CD4D58FF2B87B15700B82964 /* EquatableError.swift in Sources */, CD4D58A52B86BD1B00B82964 /* PersistenceRepository.swift in Sources */, + CDEB38B42BED7CAE0059FBA7 /* SettingsView.swift in Sources */, CD1446252A5B357900610EF1 /* Document.swift in Sources */, CD4D58972B86BAD900B82964 /* EnvironmentValues+TabReselectionHashValue.swift in Sources */, CD4D591C2B87B43D00B82964 /* Task+Extensions.swift in Sources */, CD4D58AA2B86BE5900B82964 /* AccountListView.swift in Sources */, CD4D58B92B86D9F800B82964 /* AccountButtonView.swift in Sources */, CD4D58B32B86BFD400B82964 /* AccountsTracker.swift in Sources */, + CD317D4F2BE983ED008F63E2 /* MonochromePalette.swift in Sources */, CD4D58B52B86BFFB00B82964 /* PersistenceRepository+Dependency.swift in Sources */, CD4D58EB2B86E63300B82964 /* AssociatedColor.swift in Sources */, + CD317D4C2BE97FED008F63E2 /* Palette.swift in Sources */, CD4D591A2B87B3D100B82964 /* ErrorHandler+Dependency.swift in Sources */, 03D3A1F32BB9D49B009DE55E /* ActionGroup.swift in Sources */, CD4D58FD2B87B14D00B82964 /* ContextualError.swift in Sources */, @@ -870,7 +900,7 @@ CD4D59202B87B63300B82964 /* SettingsOptions.swift in Sources */, 030FF67F2BC8544700F6BFAC /* CustomTabBarController.swift in Sources */, CD4D58F82B87B0D100B82964 /* InternetSpeed.swift in Sources */, - CD4D58932B86BA5C00B82964 /* Colors.swift in Sources */, + CD4D58932B86BA5C00B82964 /* StandardPalette.swift in Sources */, 030FF6812BC859FD00F6BFAC /* CustomTabViewHostingController.swift in Sources */, CD4D58AD2B86BE7100B82964 /* QuickSwitcherView.swift in Sources */, 6363D5C527EE196700E34822 /* MlemApp.swift in Sources */, diff --git a/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a20514d5d..9580c2796 100644 --- a/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -18,13 +18,22 @@ "revision" : "ecb18d8ce4d88277cc4fb103973352d91e18c535" } }, + { + "identity" : "lemmymarkdownui", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mlemgroup/LemmyMarkdownUI", + "state" : { + "branch" : "main", + "revision" : "a5ef8e49e9db253afd49e9e38805fa2e89649b32" + } + }, { "identity" : "mlemmiddleware", "kind" : "remoteSourceControl", "location" : "https://github.com/mlemgroup/MlemMiddleware.git", "state" : { - "branch" : "master", - "revision" : "82bae530b51ad208ae1fce11bab3276ec80c5b76" + "revision" : "3fc9bdebc74909dfdea2fcd51afb645d5d1ed2b6", + "version" : "0.1.0" } }, { diff --git a/Mlem/App/Constants/Colors.swift b/Mlem/App/Constants/Colors.swift deleted file mode 100644 index 4b5226bf0..000000000 --- a/Mlem/App/Constants/Colors.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Color+Colors.swift -// Mlem -// -// Created by David Bureš on 26.03.2022. -// - -import SwiftUI - -enum Colors { - // This is here to give me dynamic light/dark system colors for view backgrounds - // Maybe add more colors down the line if needed? - static let systemBackground = Color(UIColor.systemBackground) - static let secondarySystemBackground = Color(UIColor.secondarySystemBackground) - static let tertiarySystemBackground = Color(UIColor.tertiarySystemBackground) - - // Interaction colors--redundant right now, but this will be nice if we want to change them later - static let upvoteColor = Color.blue - static let downvoteColor = Color.red - static let saveColor = Color.green -} diff --git a/Mlem/App/Constants/Colors/MonochromePalette.swift b/Mlem/App/Constants/Colors/MonochromePalette.swift new file mode 100644 index 000000000..17f869658 --- /dev/null +++ b/Mlem/App/Constants/Colors/MonochromePalette.swift @@ -0,0 +1,23 @@ +// +// MonochromePalette.swift +// Mlem +// +// Created by Eric Andrews on 2024-05-06. +// + +import Foundation +import SwiftUI + +extension ColorPalette { + static let monochrome: ColorPalette = .init( + primary: .primary, + background: Color(UIColor.systemBackground), + secondaryBackground: Color(UIColor.secondarySystemBackground), + tertiaryBackground: Color(UIColor.tertiarySystemBackground), + accent: .primary, + upvote: .primary, + downvote: .primary, + save: .primary, + selectedInteractionBarItem: Color(UIColor.systemBackground) + ) +} diff --git a/Mlem/App/Constants/Colors/StandardPalette.swift b/Mlem/App/Constants/Colors/StandardPalette.swift new file mode 100644 index 000000000..9cac306a2 --- /dev/null +++ b/Mlem/App/Constants/Colors/StandardPalette.swift @@ -0,0 +1,22 @@ +// +// DefaultColors.swift +// Mlem +// +// Created by Eric Andrews on 2024-05-06. +// + +import SwiftUI + +extension ColorPalette { + static let standard: ColorPalette = .init( + primary: .primary, + background: Color(UIColor.systemBackground), + secondaryBackground: Color(UIColor.secondarySystemBackground), + tertiaryBackground: Color(UIColor.tertiarySystemBackground), + accent: .accentColor, + upvote: .blue, + downvote: .red, + save: .green, + selectedInteractionBarItem: .white + ) +} diff --git a/Mlem/App/Globals/Definitions/Notifier/NotificationDisplayer.swift b/Mlem/App/Globals/Definitions/Notifier/NotificationDisplayer.swift index ca2b76da6..15f438ec0 100644 --- a/Mlem/App/Globals/Definitions/Notifier/NotificationDisplayer.swift +++ b/Mlem/App/Globals/Definitions/Notifier/NotificationDisplayer.swift @@ -6,6 +6,7 @@ // // +import Dependencies import SwiftUI /// A class responsible for displaying important notifications to the user @@ -202,6 +203,8 @@ enum NotificationDisplayer { /// A simple toast view /// - Note: This view is private as it should only be created via the notification process private struct Toast: View { + @Environment(Palette.self) var palette + enum Style { case success case error @@ -242,7 +245,7 @@ private struct Toast: View { @ViewBuilder var background: some View { - Colors.secondarySystemBackground + palette.secondaryBackground .clipShape(Capsule()) .overlay(Capsule().stroke(Color.gray.opacity(0.2), lineWidth: 1)) } diff --git a/Mlem/App/Globals/Definitions/Palette.swift b/Mlem/App/Globals/Definitions/Palette.swift new file mode 100644 index 000000000..ad08af22e --- /dev/null +++ b/Mlem/App/Globals/Definitions/Palette.swift @@ -0,0 +1,79 @@ +// +// ColorProvider.swift +// Mlem +// +// Created by Eric Andrews on 2024-05-06. +// + +import Foundation +import SwiftUI + +protocol PaletteProviding { + // basics + var primary: Color { get } + var background: Color { get } + var secondaryBackground: Color { get } + var tertiaryBackground: Color { get } + var accent: Color { get } + + // interactions + var upvote: Color { get } + var downvote: Color { get } + var save: Color { get } +} + +enum PaletteOption: String { + case standard, monochrome + + var palette: ColorPalette { + switch self { + case .standard: ColorPalette.standard + case .monochrome: ColorPalette.monochrome + } + } +} + +struct ColorPalette: PaletteProviding { + // basics + var primary: Color + var background: Color + var secondaryBackground: Color + var tertiaryBackground: Color + var accent: Color + + // interactions + var upvote: Color + var downvote: Color + var save: Color + var selectedInteractionBarItem: Color +} + +@Observable +class Palette: PaletteProviding { + /// Current color palette + private var palette: ColorPalette + + static var main: Palette = .init() + + init() { + @AppStorage("colorPalette") var colorPalette: PaletteOption = .standard + self.palette = colorPalette.palette + } + + /// Updates the current color palette + func changePalette(to newPalette: PaletteOption) { + palette = newPalette.palette + } + + // ColorProviding conformance + var primary: Color { palette.primary } + var background: Color { palette.background } + var secondaryBackground: Color { palette.secondaryBackground } + var tertiaryBackground: Color { palette.tertiaryBackground } + var accent: Color { palette.accent } + + var upvote: Color { palette.upvote } + var downvote: Color { palette.downvote } + var save: Color { palette.save } + var selectedInteractionBarItem: Color { palette.selectedInteractionBarItem } +} diff --git a/Mlem/App/Models/Helpers/Action/BasicAction.swift b/Mlem/App/Models/Helpers/Action/BasicAction.swift index 9a15f44ff..cbc682119 100644 --- a/Mlem/App/Models/Helpers/Action/BasicAction.swift +++ b/Mlem/App/Models/Helpers/Action/BasicAction.swift @@ -5,6 +5,7 @@ // Created by Sjmarf on 31/03/2024. // +import Dependencies import SwiftUI struct BasicAction: Action { diff --git a/Mlem/App/Utility/Extensions/Content Models/Interactable1Providing+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Interactable1Providing+Extensions.swift index 60180e5ab..71d1afc6f 100644 --- a/Mlem/App/Utility/Extensions/Content Models/Interactable1Providing+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/Interactable1Providing+Extensions.swift @@ -16,7 +16,7 @@ extension Interactable1Providing { return .init( isOn: isOn, label: isOn ? "Undo Upvote" : "Upvote", - color: Colors.upvoteColor, + color: Palette.main.upvote, icon: Icons.upvote, menuIcon: isOn ? Icons.upvoteSquareFill : Icons.upvoteSquare, swipeIcon1: isOn ? Icons.resetVoteSquare : Icons.upvoteSquare, @@ -30,7 +30,7 @@ extension Interactable1Providing { return .init( isOn: isOn, label: isOn ? "Undo Downvote" : "Downvote", - color: Colors.downvoteColor, + color: Palette.main.downvote, icon: Icons.downvote, menuIcon: isOn ? Icons.downvoteSquareFill : Icons.downvoteSquare, swipeIcon1: isOn ? Icons.resetVoteSquare : Icons.downvoteSquare, @@ -44,7 +44,7 @@ extension Interactable1Providing { return .init( isOn: isOn, label: isOn ? "Unsave" : "Save", - color: Colors.saveColor, + color: Palette.main.save, icon: isOn ? Icons.saveFill : Icons.save, menuIcon: isOn ? Icons.saveFill : Icons.save, swipeIcon1: isOn ? Icons.unsave : Icons.save, diff --git a/Mlem/App/Views/Root/ContentView.swift b/Mlem/App/Views/Root/ContentView.swift index 03c7193a2..8f711c0ca 100644 --- a/Mlem/App/Views/Root/ContentView.swift +++ b/Mlem/App/Views/Root/ContentView.swift @@ -5,22 +5,29 @@ // Created by David Bureš on 25.03.2022. // +import Dependencies import SwiftUI struct ContentView: View { + @AppStorage("colorPalette") var colorPalette: PaletteOption = .standard + let timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect() + // globals var appState: AppState { .main } + @State var palette: Palette = .main @State var selectedTabIndex: Int = 0 @State var navigationModel: NavigationModel = .init() var body: some View { content + .tint(palette.accent) .onReceive(timer) { _ in appState.cleanCaches() } + .environment(palette) .environment(appState) } diff --git a/Mlem/App/Views/Root/Tabs/Feeds/FeedsView.swift b/Mlem/App/Views/Root/Tabs/Feeds/FeedsView.swift index 5536cc05a..9a7addcd7 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/FeedsView.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/FeedsView.swift @@ -27,6 +27,7 @@ struct MinimalPostFeedView: View { @Dependency(\.errorHandler) var errorHandler @Environment(AppState.self) var appState + @Environment(Palette.self) var palette @State var postTracker: StandardPostFeedLoader @@ -85,7 +86,7 @@ struct MinimalPostFeedView: View { func actionButton(_ action: BasicAction) -> some View { Button(action: action.callback ?? {}) { Image(systemName: action.barIcon) - .foregroundColor(action.isOn ? .white : .primary) + .foregroundColor(action.isOn ? palette.selectedInteractionBarItem : palette.primary) .padding(2) .background( RoundedRectangle(cornerRadius: AppConstants.tinyItemCornerRadius) @@ -113,7 +114,7 @@ struct MinimalPostFeedView: View { .foregroundStyle(post.read ? .secondary : .primary) } .padding(10) - .background(Color(uiColor: .systemBackground)) + .background(palette.background) .contentShape(.rect) .contextMenu { ForEach(post.menuActions.children, id: \.id) { action in diff --git a/Mlem/App/Views/Root/Tabs/Settings/SettingsView.swift b/Mlem/App/Views/Root/Tabs/Settings/SettingsView.swift new file mode 100644 index 000000000..83e3962f4 --- /dev/null +++ b/Mlem/App/Views/Root/Tabs/Settings/SettingsView.swift @@ -0,0 +1,41 @@ +// +// SettingsView.swift +// Mlem +// +// Created by Eric Andrews on 2024-05-09. +// + +import Dependencies +import Foundation +import SwiftUI + +struct SettingsView: View { + @Environment(Palette.self) var palette + + @AppStorage("colorPalette") var colorPalette: PaletteOption = .standard { + didSet { + print("updating palette to \(colorPalette)") + palette.changePalette(to: colorPalette) + } + } + + var body: some View { + Form { + colorSettings + } + } + + var colorSettings: some View { + Section { + Button("Default") { + colorPalette = .standard + } + + Button("Monochrome") { + colorPalette = .monochrome + } + } header: { + Text("Theme") + } + } +} diff --git a/Mlem/App/Views/Shared/CustomTabBarController.swift b/Mlem/App/Views/Shared/CustomTabBarController.swift index 7fa9ee570..9c9349eb3 100644 --- a/Mlem/App/Views/Shared/CustomTabBarController.swift +++ b/Mlem/App/Views/Shared/CustomTabBarController.swift @@ -5,6 +5,7 @@ // Created by Sjmarf on 11/04/2024. // +import Dependencies import Foundation import SwiftUI @@ -34,7 +35,7 @@ class CustomTabBarController: UITabBarController, UITabBarControllerDelegate { delegate = self hidesBottomBarWhenPushed = true - tabBar.tintColor = .systemBlue + tabBar.tintColor = UIColor(Palette.main.accent) let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressGestureTriggered(_:))) tabBar.addGestureRecognizer(longPressRecognizer) diff --git a/Mlem/App/Views/Shared/CustomTabView.swift b/Mlem/App/Views/Shared/CustomTabView.swift index 767ec5876..e91a91dc2 100644 --- a/Mlem/App/Views/Shared/CustomTabView.swift +++ b/Mlem/App/Views/Shared/CustomTabView.swift @@ -9,6 +9,8 @@ import Foundation import SwiftUI struct CustomTabView: UIViewControllerRepresentable { + @Environment(Palette.self) var palette + var viewControllers: [CustomTabViewHostingController] let swipeGestureCallback: () -> Void @@ -35,7 +37,15 @@ struct CustomTabView: UIViewControllerRepresentable { _ uiViewController: UITabBarController, context: UIViewControllerRepresentableContext ) { - // no-op + withObservationTracking { + _ = palette.accent + } onChange: { + if let controller = uiViewController as? CustomTabBarController { + Task { @MainActor in + controller.tabBar.tintColor = UIColor(palette.accent) + } + } + } } func makeCoordinator() -> Coordinator { diff --git a/Mlem/App/Views/Shared/Markdown.swift b/Mlem/App/Views/Shared/Markdown.swift index 32c4d4fc7..f9a8f03b5 100644 --- a/Mlem/App/Views/Shared/Markdown.swift +++ b/Mlem/App/Views/Shared/Markdown.swift @@ -5,11 +5,14 @@ // Created by Sjmarf on 25/04/2024. // +import Dependencies import LemmyMarkdownUI import Nuke import SwiftUI struct Markdown: View { + @Environment(Palette.self) var palette + let markdown: String init(_ markdown: String) { @@ -37,7 +40,7 @@ struct Markdown: View { .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: 8) - .fill(Color(uiColor: .secondarySystemBackground)) + .fill(palette.secondaryBackground) ) ) } diff --git a/Mlem/App/Views/Shared/Navigation/NavigationPage.swift b/Mlem/App/Views/Shared/Navigation/NavigationPage.swift index af72d2419..1353fd15a 100644 --- a/Mlem/App/Views/Shared/Navigation/NavigationPage.swift +++ b/Mlem/App/Views/Shared/Navigation/NavigationPage.swift @@ -24,7 +24,7 @@ extension NavigationPage { case .search: SubscriptionListView() case .settings: - Text("Settings") + SettingsView() case .quickSwitcher: QuickSwitcherView() .presentationDetents([.medium, .large])