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:
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