feat: extract reusable TodoListView and add tappable filter screens
This commit is contained in:
@@ -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
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
MindDump/Models/Destination.swift
Normal file
6
MindDump/Models/Destination.swift
Normal file
@@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
enum Destination: Hashable {
|
||||
case list(UUID)
|
||||
case filter(TodoFilter)
|
||||
}
|
||||
29
MindDump/Models/TodoFilter.swift
Normal file
29
MindDump/Models/TodoFilter.swift
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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 {
|
||||
},
|
||||
onDelete: { item in
|
||||
store.deleteItem(item.id, from: listID)
|
||||
},
|
||||
onMove: { item in
|
||||
moveItem = item
|
||||
} label: {
|
||||
Label("Verschieben", systemImage: "folder")
|
||||
}
|
||||
.tint(.blue)
|
||||
}
|
||||
}
|
||||
.onDelete { offsets in
|
||||
let sorted = todoList.sortedItems
|
||||
for index in offsets {
|
||||
store.deleteItem(sorted[index].id, from: listID)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
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()
|
||||
|
||||
50
MindDump/Views/TodoListScreen.swift
Normal file
50
MindDump/Views/TodoListScreen.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
MindDump/Views/TodoListView.swift
Normal file
40
MindDump/Views/TodoListView.swift
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user