listless

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

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