← All repositories

listless

A simple list app for Apple platforms

git clone https://code.inqk.net/listless.git


commit f2e04bfab4caed167b734f75b70efad61c49d42c
parent 05fdfc90b8b38bc08a37461bed6244674839228e
Author: Michael Camilleri <[email protected]>
Date:   Thu, 19 Mar 2026 22:37:04 +0900

Add Collaroy colour theme

Diffstat

MListless/Helpers/AccentColor.swift | 55++++++++++++++++++++++++++++++++++++++++++++++---------
MListlessiOS/Views/PullToCreate.swift | 4+++-
MListlessiOS/Views/SettingsView.swift | 31+++++++++++++++++++++++++++++++
MListlessiOS/Views/TaskListView.swift | 6++++--
MListlessiOS/Views/TaskRowView.swift | 7++++++-

5 files changed, 90 insertions(+), 13 deletions(-)

diff --git a/Listless/Helpers/AccentColor.swift b/Listless/Helpers/AccentColor.swift

@@ -1,8 +1,46 @@
 import SwiftUI
 
+enum ColorTheme: Int, CaseIterable, Identifiable {
+    case original = 0
+    case collaroy = 1
+
+    var id: Int { rawValue }
+
+    var displayName: String {
+        switch self {
+        case .original: "Original"
+        case .collaroy: "Collaroy"
+        }
+    }
+
+    fileprivate typealias HSB = (h: Double, s: Double, b: Double)
+
+    fileprivate var top: HSB {
+        switch self {
+        case .original: (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 .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 .collaroy: (h: 0.44, s: 0.50, b: 0.80)
+        }
+    }
+}
+
 private struct TaskAccentColorKey: Hashable {
     let index: Int
     let total: Int
+    let theme: ColorTheme
 }
 
 @MainActor
@@ -10,14 +48,13 @@ private enum TaskAccentColorCache {
     static var colors: [TaskAccentColorKey: Color] = [:]
 }
 
-func taskColor(forIndex index: Int, total: Int) -> Color {
-    guard total > 1 else { return Color(hue: 0.98, saturation: 0.85, brightness: 1.0) }
+func taskColor(forIndex index: Int, total: Int, theme: ColorTheme = .original) -> Color {
+    let top = theme.top
+    guard total > 1 else { return Color(hue: top.h, saturation: top.s, brightness: top.b) }
 
-    // Gradient matches gradient.png: coral/red → pink/magenta → purple/blue
     let progress = Double(index) / Double(total - 1)
-    let top    = (h: 0.98, s: 0.85, b: 1.00)
-    let mid    = (h: 0.88, s: 0.75, b: 0.95)
-    let bottom = (h: 0.72, s: 0.65, b: 0.85)
+    let mid = theme.mid
+    let bottom = theme.bottom
 
     if progress < 0.5 {
         return interpolateHSB(from: top, to: mid, progress: progress * 2.0)
@@ -27,13 +64,13 @@ func taskColor(forIndex index: Int, total: Int) -> Color {
 }
 
 @MainActor
-func cachedTaskColor(forIndex index: Int, total: Int) -> Color {
-    let key = TaskAccentColorKey(index: index, total: total)
+func cachedTaskColor(forIndex index: Int, total: Int, theme: ColorTheme = .original) -> Color {
+    let key = TaskAccentColorKey(index: index, total: total, theme: theme)
     if let cached = TaskAccentColorCache.colors[key] {
         return cached
     }
 
-    let computed = taskColor(forIndex: index, total: total)
+    let computed = taskColor(forIndex: index, total: total, theme: theme)
     TaskAccentColorCache.colors[key] = computed
     return computed
 }

diff --git a/ListlessiOS/Views/PullToCreate.swift b/ListlessiOS/Views/PullToCreate.swift

@@ -4,6 +4,8 @@ struct PullToCreateIndicator: View {
     let pullOffset: CGFloat
     let threshold: CGFloat
     var hasRowsBelow: Bool = true
+    @AppStorage("colorTheme") private var colorThemeRaw = 0
+    private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .original }
 
     static let indicatorHeight: CGFloat = 50
 
@@ -45,7 +47,7 @@ struct PullToCreateIndicator: View {
         )
         .overlay(alignment: .leading) {
             Rectangle()
-                .fill(taskColor(forIndex: 0, total: 1))
+                .fill(taskColor(forIndex: 0, total: 1, theme: colorTheme))
                 .frame(width: TaskRowMetrics.accentBarWidth)
         }
         .frame(height: Self.indicatorHeight, alignment: .top)

diff --git a/ListlessiOS/Views/SettingsView.swift b/ListlessiOS/Views/SettingsView.swift

@@ -5,6 +5,7 @@ struct SettingsView: View {
     @Environment(\.dismiss) private var dismiss
     @AppStorage("headingText") private var headingText = "Items"
     @AppStorage("appearanceMode") private var appearanceMode = 0
+    @AppStorage("colorTheme") private var colorThemeRaw = 0
 
     var body: some View {
         NavigationStack {
@@ -13,6 +14,36 @@ struct SettingsView: View {
                     TextField("List Title", text: $headingText)
                 }
 
+                Section("Theme") {
+                    ForEach(ColorTheme.allCases) { theme in
+                        Button {
+                            colorThemeRaw = theme.rawValue
+                        } label: {
+                            HStack {
+                                Image(systemName: "checkmark")
+                                    .fontWeight(.semibold)
+                                    .foregroundStyle(.blue)
+                                    .opacity(colorThemeRaw == theme.rawValue ? 1 : 0)
+                                Text(theme.displayName)
+                                Spacer()
+                                LinearGradient(
+                                    colors: [
+                                        taskColor(forIndex: 0, total: 5, theme: theme),
+                                        taskColor(forIndex: 2, total: 5, theme: theme),
+                                        taskColor(forIndex: 4, total: 5, theme: theme),
+                                    ],
+                                    startPoint: .leading,
+                                    endPoint: .trailing
+                                )
+                                .frame(width: 80, height: 24)
+                                .clipShape(RoundedRectangle(cornerRadius: 6))
+                            }
+                            .contentShape(Rectangle())
+                        }
+                        .buttonStyle(.plain)
+                    }
+                }
+
                 Section("Appearance") {
                     Picker("Appearance", selection: $appearanceMode) {
                         Text("System").tag(0)

diff --git a/ListlessiOS/Views/TaskListView.swift b/ListlessiOS/Views/TaskListView.swift

@@ -20,6 +20,8 @@ struct TaskListView: View, TaskListViewProtocol {
     }
 
     @AppStorage("headingText") var headingText = "Items"
+    @AppStorage("colorTheme") private var colorThemeRaw = 0
+    private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .original }
     @Environment(\.undoManager) var undoManager
     @Environment(\.managedObjectContext) var managedObjectContext
 
@@ -276,7 +278,7 @@ struct TaskListView: View, TaskListViewProtocol {
     /// ZStack in ``pullToCreateIndicatorRow`` rather than its own visibility.
     @ViewBuilder private var phantomEntryRowContent: some View {
         let accentColor = taskColor(
-            forIndex: 0, total: max(1, displayActiveTasks.count + 1)
+            forIndex: 0, total: max(1, displayActiveTasks.count + 1), theme: colorTheme
         )
         let isSelected = fState.selectedTaskID == draftPrependRowID
         HStack(alignment: .center, spacing: TaskRowMetrics.contentSpacing) {
@@ -341,7 +343,7 @@ struct TaskListView: View, TaskListViewProtocol {
         if isAppendDraftVisible {
             let total = max(1, displayActiveTasks.count + 1)
             let index = displayActiveTasks.count
-            let accentColor = taskColor(forIndex: index, total: total)
+            let accentColor = taskColor(forIndex: index, total: total, theme: colorTheme)
             let isSelected = fState.selectedTaskID == draftAppendRowID
             HStack(alignment: .center, spacing: TaskRowMetrics.contentSpacing) {
                 Image(systemName: "circle")

diff --git a/ListlessiOS/Views/TaskRowView.swift b/ListlessiOS/Views/TaskRowView.swift

@@ -17,6 +17,8 @@ struct TaskRowView: View {
     let onEndEdit: (UUID, _ shouldCreateNewTask: Bool) -> Void
     @FocusState.Binding var focusedField: FocusField?
 
+    @AppStorage("colorTheme") private var colorThemeRaw = 0
+    private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .original }
     @State private var swipeOffset: CGFloat = 0
     @State private var swipeDirection: TaskRowSwipeGesture.SwipeDirection = .none
     @State private var isSwipeTriggered: Bool = false
@@ -148,6 +150,9 @@ struct TaskRowView: View {
         .onChange(of: totalTasks) { _, _ in
             cachedAccentColor = computeAccentColor()
         }
+        .onChange(of: colorThemeRaw) { _, _ in
+            cachedAccentColor = computeAccentColor()
+        }
         .taskSwipeGesture(
             isDragging: $isDragging,
             isScrolling: isScrolling,
@@ -187,7 +192,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)
     }
 
     @ViewBuilder