From 29719b7499b0ba35e2bfea6b39b7f11d068559c5 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 12 Feb 2026 22:30:47 +0100 Subject: [PATCH] feat: extract reusable TodoListView and add tappable filter screens --- MindDump/ContentView.swift | 22 +++++++++--- MindDump/Models/Destination.swift | 6 ++++ MindDump/Models/TodoFilter.swift | 29 +++++++++++++++ MindDump/ViewModels/ListStore.swift | 30 +++++++++++++--- MindDump/Views/ListDetailView.swift | 30 +++++++--------- MindDump/Views/ListsOverviewView.swift | 16 ++++++--- MindDump/Views/TodoListScreen.swift | 50 ++++++++++++++++++++++++++ MindDump/Views/TodoListView.swift | 40 +++++++++++++++++++++ MindDump/Views/TodoRowView.swift | 7 ++++ 9 files changed, 199 insertions(+), 31 deletions(-) create mode 100644 MindDump/Models/Destination.swift create mode 100644 MindDump/Models/TodoFilter.swift create mode 100644 MindDump/Views/TodoListScreen.swift create mode 100644 MindDump/Views/TodoListView.swift diff --git a/MindDump/ContentView.swift b/MindDump/ContentView.swift index 13a0e6d..32cbca4 100644 --- a/MindDump/ContentView.swift +++ b/MindDump/ContentView.swift @@ -2,17 +2,29 @@ import SwiftUI import SwiftData struct ContentView: View { - @State private var navigationPath: [UUID] = [] + @State private var navigationPath: [Destination] = [] + + private var activeListID: UUID? { + if case .list(let id) = navigationPath.last { + return id + } + return nil + } var body: some View { ZStack(alignment: .bottomTrailing) { NavigationStack(path: $navigationPath) { - ListsOverviewView() - .navigationDestination(for: UUID.self) { listID in - ListDetailView(listID: listID) + ListsOverviewView(navigationPath: $navigationPath) + .navigationDestination(for: Destination.self) { destination in + switch destination { + case .list(let listID): + ListDetailView(listID: listID) + case .filter(let filter): + TodoListScreen(filter: filter) + } } } - MindDumpButton(activeListID: navigationPath.last) + MindDumpButton(activeListID: activeListID) } } } diff --git a/MindDump/Models/Destination.swift b/MindDump/Models/Destination.swift new file mode 100644 index 0000000..2ed80b1 --- /dev/null +++ b/MindDump/Models/Destination.swift @@ -0,0 +1,6 @@ +import Foundation + +enum Destination: Hashable { + case list(UUID) + case filter(TodoFilter) +} diff --git a/MindDump/Models/TodoFilter.swift b/MindDump/Models/TodoFilter.swift new file mode 100644 index 0000000..a62e983 --- /dev/null +++ b/MindDump/Models/TodoFilter.swift @@ -0,0 +1,29 @@ +import Foundation + +enum TodoFilter: Hashable { + case open, urgent, due + + var title: String { + switch self { + case .open: "Offene Aufgaben" + case .urgent: "Dringende Aufgaben" + case .due: "Fällige Aufgaben" + } + } + + var emptyMessage: String { + switch self { + case .open: "Keine offenen Aufgaben" + case .urgent: "Keine dringenden Aufgaben" + case .due: "Keine fälligen Aufgaben" + } + } + + var emptyIcon: String { + switch self { + case .open: "checkmark.circle" + case .urgent: "exclamationmark.triangle" + case .due: "calendar.badge.checkmark" + } + } +} diff --git a/MindDump/ViewModels/ListStore.swift b/MindDump/ViewModels/ListStore.swift index feaf7d0..299273c 100644 --- a/MindDump/ViewModels/ListStore.swift +++ b/MindDump/ViewModels/ListStore.swift @@ -18,18 +18,30 @@ class ListStore { var openCount: Int { allOpenItems.count } - var urgentCount: Int { + var urgentItems: [TodoItem] { let threeDaysFromNow = Calendar.current.date(byAdding: .day, value: 3, to: Date())! return allOpenItems.filter { item in item.priority == .high || (item.deadline != nil && item.deadline! <= threeDaysFromNow) - }.count + } } - var dueCount: Int { + var urgentCount: Int { urgentItems.count } + + var dueItems: [TodoItem] { let startOfTomorrow = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!) return allOpenItems.filter { item in item.deadline != nil && item.deadline! < startOfTomorrow - }.count + } + } + + var dueCount: Int { dueItems.count } + + func items(for filter: TodoFilter) -> [TodoItem] { + switch filter { + case .open: allOpenItems + case .urgent: urgentItems + case .due: dueItems + } } init(modelContext: ModelContext) { @@ -106,6 +118,16 @@ class ListStore { fetchLists() } + func toggleItemCompleted(_ item: TodoItem) { + guard let listID = item.list?.id else { return } + toggleItemCompleted(item.id, in: listID) + } + + func deleteItem(_ item: TodoItem) { + guard let listID = item.list?.id else { return } + deleteItem(item.id, from: listID) + } + func moveItem(_ itemID: UUID, from sourceListID: UUID, to targetListID: UUID) { guard sourceListID != targetListID, let sourceList = lists.first(where: { $0.id == sourceListID }), diff --git a/MindDump/Views/ListDetailView.swift b/MindDump/Views/ListDetailView.swift index 57d3a06..15e1239 100644 --- a/MindDump/Views/ListDetailView.swift +++ b/MindDump/Views/ListDetailView.swift @@ -17,27 +17,21 @@ struct ListDetailView: View { ContentUnavailableView("Keine Einträge", systemImage: "tray") } else { List { - ForEach(todoList.sortedItems) { item in - TodoRowView(item: item, onToggle: { + TodoListView( + items: todoList.sortedItems, + onToggle: { item in store.toggleItemCompleted(item.id, in: listID) - }, onTap: { + }, + onTap: { item in editorItem = item - }) - .swipeActions(edge: .leading) { - Button { - moveItem = item - } label: { - Label("Verschieben", systemImage: "folder") - } - .tint(.blue) + }, + onDelete: { item in + store.deleteItem(item.id, from: listID) + }, + onMove: { item in + moveItem = item } - } - .onDelete { offsets in - let sorted = todoList.sortedItems - for index in offsets { - store.deleteItem(sorted[index].id, from: listID) - } - } + ) } } } diff --git a/MindDump/Views/ListsOverviewView.swift b/MindDump/Views/ListsOverviewView.swift index 97ae023..a1e2716 100644 --- a/MindDump/Views/ListsOverviewView.swift +++ b/MindDump/Views/ListsOverviewView.swift @@ -2,6 +2,7 @@ import SwiftUI struct ListsOverviewView: View { @Environment(ListStore.self) private var store + @Binding var navigationPath: [Destination] @State private var newListName = "" @FocusState private var isFieldFocused: Bool @@ -9,16 +10,23 @@ struct ListsOverviewView: View { List { Section { HStack(spacing: 12) { - StatsCardView(icon: "checklist", count: store.openCount, label: "Offen", color: .blue) - StatsCardView(icon: "exclamationmark.triangle", count: store.urgentCount, label: "Dringend", color: .orange) - StatsCardView(icon: "calendar.badge.clock", count: store.dueCount, label: "Fällig", color: .red) + Button { navigationPath.append(.filter(.open)) } label: { + StatsCardView(icon: "checklist", count: store.openCount, label: "Offen", color: .blue) + } + Button { navigationPath.append(.filter(.urgent)) } label: { + StatsCardView(icon: "exclamationmark.triangle", count: store.urgentCount, label: "Dringend", color: .orange) + } + Button { navigationPath.append(.filter(.due)) } label: { + StatsCardView(icon: "calendar.badge.clock", count: store.dueCount, label: "Fällig", color: .red) + } } + .buttonStyle(.plain) .listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0)) .listRowBackground(Color.clear) } ForEach(store.lists) { list in - NavigationLink(value: list.id) { + NavigationLink(value: Destination.list(list.id)) { HStack { Text(list.name) Spacer() diff --git a/MindDump/Views/TodoListScreen.swift b/MindDump/Views/TodoListScreen.swift new file mode 100644 index 0000000..296c1fc --- /dev/null +++ b/MindDump/Views/TodoListScreen.swift @@ -0,0 +1,50 @@ +import SwiftUI + +struct TodoListScreen: View { + @Environment(ListStore.self) private var store + let filter: TodoFilter + @State private var editorItem: TodoItem? + @State private var moveItem: TodoItem? + + private var items: [TodoItem] { + store.items(for: filter) + } + + var body: some View { + Group { + if items.isEmpty { + ContentUnavailableView(filter.emptyMessage, systemImage: filter.emptyIcon) + } else { + List { + TodoListView( + items: items, + showListName: true, + onToggle: { item in + store.toggleItemCompleted(item) + }, + onTap: { item in + editorItem = item + }, + onDelete: { item in + store.deleteItem(item) + }, + onMove: { item in + moveItem = item + } + ) + } + } + } + .navigationTitle(filter.title) + .sheet(item: $editorItem) { item in + if let listID = item.list?.id { + TodoEditorView(listID: listID, item: item) + } + } + .sheet(item: $moveItem) { item in + if let listID = item.list?.id { + MoveToListView(itemID: item.id, currentListID: listID) + } + } + } +} diff --git a/MindDump/Views/TodoListView.swift b/MindDump/Views/TodoListView.swift new file mode 100644 index 0000000..17abd28 --- /dev/null +++ b/MindDump/Views/TodoListView.swift @@ -0,0 +1,40 @@ +import SwiftUI + +struct TodoListView: View { + let items: [TodoItem] + var showListName: Bool = false + var onToggle: (TodoItem) -> Void + var onTap: (TodoItem) -> Void + var onDelete: ((TodoItem) -> Void)? + var onMove: ((TodoItem) -> Void)? + + var body: some View { + ForEach(items) { item in + TodoRowView( + item: item, + onToggle: { onToggle(item) }, + subtitle: showListName ? item.list?.name : nil, + onTap: { onTap(item) } + ) + .swipeActions(edge: .leading) { + if let onMove { + Button { + onMove(item) + } label: { + Label("Verschieben", systemImage: "folder") + } + .tint(.blue) + } + } + .swipeActions(edge: .trailing) { + if let onDelete { + Button(role: .destructive) { + onDelete(item) + } label: { + Label("Löschen", systemImage: "trash") + } + } + } + } + } +} diff --git a/MindDump/Views/TodoRowView.swift b/MindDump/Views/TodoRowView.swift index 384200a..bbb9652 100644 --- a/MindDump/Views/TodoRowView.swift +++ b/MindDump/Views/TodoRowView.swift @@ -3,6 +3,7 @@ import SwiftUI struct TodoRowView: View { let item: TodoItem let onToggle: () -> Void + var subtitle: String? var onTap: (() -> Void)? private var isOverdue: Bool { @@ -33,6 +34,12 @@ struct TodoRowView: View { .foregroundStyle(.secondary) .lineLimit(1) } + + if let subtitle { + Text(subtitle) + .font(.caption2) + .foregroundStyle(.tertiary) + } } .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle())