listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

commit 9380c94322191d9f818c541c60c719616e9d8356
parent b988b41f1334626252b1c366355685c6bd16c3d8
Author: Michael Camilleri <[email protected]>
Date:   Sun, 22 Mar 2026 10:52:47 +0900

Support themes on macOS version

This commit brings themes to the macOS version. It also renames the
original theme to Pilbara.

Co-Authored-By: Claude 4.6 Opus <[email protected]>

Diffstat:
MListless/Helpers/AccentColor.swift | 18+++++++++++-------
MListlessMac/ListlessMacApp.swift | 22++++++++++++++++++++++
MListlessMac/Views/TaskListView.swift | 5++++-
MListlessMac/Views/TaskRowView.swift | 8+++++++-
MListlessiOS/Views/PullToCreate.swift | 2+-
MListlessiOS/Views/SettingsView.swift | 2+-
MListlessiOS/Views/TaskListView.swift | 2+-
MListlessiOS/Views/TaskRowView.swift | 2+-
8 files changed, 48 insertions(+), 13 deletions(-)

diff --git a/Listless/Helpers/AccentColor.swift b/Listless/Helpers/AccentColor.swift @@ -1,14 +1,18 @@ import SwiftUI enum ColorTheme: Int, CaseIterable, Identifiable { - case original = 0 + case pilbara = 0 case collaroy = 1 var id: Int { rawValue } + static var displayOrder: [ColorTheme] { + allCases.sorted { $0.displayName < $1.displayName } + } + var displayName: String { switch self { - case .original: "Original" + case .pilbara: "Pilbara" case .collaroy: "Collaroy" } } @@ -17,21 +21,21 @@ enum ColorTheme: Int, CaseIterable, Identifiable { fileprivate var top: HSB { switch self { - case .original: (h: 0.98, s: 0.85, b: 1.00) + case .pilbara: (h: 0.98, s: 0.85, b: 1.00) case .collaroy: (h: 0.58, s: 0.88, b: 1.00) } } fileprivate var mid: HSB { switch self { - case .original: (h: 0.88, s: 0.75, b: 0.95) + case .pilbara: (h: 0.88, s: 0.75, b: 0.95) case .collaroy: (h: 0.51, s: 0.69, b: 0.90) } } fileprivate var bottom: HSB { switch self { - case .original: (h: 0.72, s: 0.65, b: 0.85) + case .pilbara: (h: 0.72, s: 0.65, b: 0.85) case .collaroy: (h: 0.44, s: 0.50, b: 0.80) } } @@ -48,7 +52,7 @@ private enum TaskAccentColorCache { static var colors: [TaskAccentColorKey: Color] = [:] } -func taskColor(forIndex index: Int, total: Int, theme: ColorTheme = .original) -> Color { +func taskColor(forIndex index: Int, total: Int, theme: ColorTheme = .pilbara) -> Color { let top = theme.top guard total > 1 else { return Color(hue: top.h, saturation: top.s, brightness: top.b) } @@ -64,7 +68,7 @@ func taskColor(forIndex index: Int, total: Int, theme: ColorTheme = .original) - } @MainActor -func cachedTaskColor(forIndex index: Int, total: Int, theme: ColorTheme = .original) -> Color { +func cachedTaskColor(forIndex index: Int, total: Int, theme: ColorTheme = .pilbara) -> Color { let key = TaskAccentColorKey(index: index, total: total, theme: theme) if let cached = TaskAccentColorCache.colors[key] { return cached diff --git a/ListlessMac/ListlessMacApp.swift b/ListlessMac/ListlessMacApp.swift @@ -14,6 +14,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation { private var syncDiagnosticsWindow: NSWindow? private let coordinators = NSMapTable<NSWindow, WindowCoordinator>.weakToStrongObjects() private static let appearanceModeKey = "appearanceMode" + private static let colorThemeKey = "colorTheme" private var keyWindowCoordinator: WindowCoordinator? { guard let window = NSApp.keyWindow else { return nil } @@ -66,6 +67,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation { } menuItem.state = (currentMode == itemMode) ? .on : .off return true + case #selector(handleThemeSelection(_:)): + let currentTheme = UserDefaults.standard.integer(forKey: Self.colorThemeKey) + menuItem.state = (currentTheme == menuItem.tag) ? .on : .off + return true default: break } @@ -147,6 +152,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation { @objc private func handleAppearanceLight() { setAppearanceMode(1) } @objc private func handleAppearanceDark() { setAppearanceMode(2) } + @objc private func handleThemeSelection(_ sender: NSMenuItem) { + UserDefaults.standard.set(sender.tag, forKey: Self.colorThemeKey) + } + private func setAppearanceMode(_ mode: Int) { UserDefaults.standard.set(mode, forKey: Self.appearanceModeKey) applyAppearanceMode(mode) @@ -366,6 +375,19 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation { viewMenu.addItem(appearanceMenuItem) viewMenu.addItem(NSMenuItem.separator()) + let themeMenu = NSMenu(title: "Theme") + for theme in ColorTheme.displayOrder { + let item = NSMenuItem(title: theme.displayName, action: #selector(handleThemeSelection(_:)), keyEquivalent: "") + item.tag = theme.rawValue + item.target = self + themeMenu.addItem(item) + } + let themeMenuItem = NSMenuItem(title: "Theme", action: nil, keyEquivalent: "") + themeMenuItem.submenu = themeMenu + viewMenu.addItem(themeMenuItem) + + viewMenu.addItem(NSMenuItem.separator()) + let fullScreenItem = NSMenuItem(title: "Enter Full Screen", action: #selector(NSWindow.toggleFullScreen(_:)), keyEquivalent: "f") fullScreenItem.keyEquivalentModifierMask = [.command, .control] viewMenu.addItem(fullScreenItem) diff --git a/ListlessMac/Views/TaskListView.swift b/ListlessMac/Views/TaskListView.swift @@ -9,6 +9,9 @@ struct TaskListView: View, TaskListViewProtocol { var draftTitle: String = "" } + @AppStorage("colorTheme") private var colorThemeRaw = 0 + private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .pilbara } + @Environment(\.undoManager) var undoManager @Environment(\.managedObjectContext) var managedObjectContext @@ -275,7 +278,7 @@ struct TaskListView: View, TaskListViewProtocol { let total = max(1, displayActiveTasks.count + 1) let index = displayActiveTasks.count let accentColor = cachedTaskColor( - forIndex: index, total: total + forIndex: index, total: total, theme: colorTheme ) let isSelected = fState.isTaskSelected(draftAppendRowID) HStack(alignment: .firstTextBaseline, spacing: 12) { diff --git a/ListlessMac/Views/TaskRowView.swift b/ListlessMac/Views/TaskRowView.swift @@ -15,6 +15,9 @@ struct TaskRowView: View { let onPaste: (String) -> Void @FocusState.Binding var focusedField: FocusField? + @AppStorage("colorTheme") private var colorThemeRaw = 0 + private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .pilbara } + @State private var editingTitle: String = "" @State private var isCurrentlyEditing: Bool = false @State private var cachedAccentColor: Color = .clear @@ -30,7 +33,7 @@ struct TaskRowView: View { @MainActor private func computeAccentColor() -> Color { guard !task.isCompleted else { return .clear } - return cachedTaskColor(forIndex: index, total: totalTasks) + return cachedTaskColor(forIndex: index, total: totalTasks, theme: colorTheme) } init( @@ -160,6 +163,9 @@ struct TaskRowView: View { editingTitle = newValue } } + .onChange(of: colorThemeRaw) { _, _ in + cachedAccentColor = computeAccentColor() + } .onChange(of: index) { _, _ in cachedAccentColor = computeAccentColor() } diff --git a/ListlessiOS/Views/PullToCreate.swift b/ListlessiOS/Views/PullToCreate.swift @@ -5,7 +5,7 @@ struct PullToCreateIndicator: View { let threshold: CGFloat var hasRowsBelow: Bool = true @AppStorage("colorTheme") private var colorThemeRaw = 0 - private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .original } + private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .pilbara } static let indicatorHeight: CGFloat = 50 diff --git a/ListlessiOS/Views/SettingsView.swift b/ListlessiOS/Views/SettingsView.swift @@ -15,7 +15,7 @@ struct SettingsView: View { } Section("Theme") { - ForEach(ColorTheme.allCases) { theme in + ForEach(ColorTheme.displayOrder) { theme in Button { colorThemeRaw = theme.rawValue } label: { diff --git a/ListlessiOS/Views/TaskListView.swift b/ListlessiOS/Views/TaskListView.swift @@ -26,7 +26,7 @@ struct TaskListView: View, TaskListViewProtocol { @AppStorage("headingText") var headingText = "Items" @AppStorage("colorTheme") private var colorThemeRaw = 0 - private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .original } + private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .pilbara } @Environment(\.undoManager) var undoManager @Environment(\.managedObjectContext) var managedObjectContext diff --git a/ListlessiOS/Views/TaskRowView.swift b/ListlessiOS/Views/TaskRowView.swift @@ -18,7 +18,7 @@ struct TaskRowView: View { @FocusState.Binding var focusedField: FocusField? @AppStorage("colorTheme") private var colorThemeRaw = 0 - private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .original } + private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .pilbara } @State private var swipeOffset: CGFloat = 0 @State private var swipeDirection: TaskRowSwipeGesture.SwipeDirection = .none @State private var isSwipeTriggered: Bool = false