commit f2e04bfab4caed167b734f75b70efad61c49d42c
parent 05fdfc90b8b38bc08a37461bed6244674839228e
Author: Michael Camilleri <[email protected]>
Date: Thu, 19 Mar 2026 22:37:04 +0900
Add Collaroy colour theme
Diffstat:
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