feat: extract reusable TodoListView and add tappable filter screens
This commit is contained in:
@@ -2,17 +2,29 @@ import SwiftUI
|
|||||||
import SwiftData
|
import SwiftData
|
||||||
|
|
||||||
struct ContentView: View {
|
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 {
|
var body: some View {
|
||||||
ZStack(alignment: .bottomTrailing) {
|
ZStack(alignment: .bottomTrailing) {
|
||||||
NavigationStack(path: $navigationPath) {
|
NavigationStack(path: $navigationPath) {
|
||||||
ListsOverviewView()
|
ListsOverviewView(navigationPath: $navigationPath)
|
||||||
.navigationDestination(for: UUID.self) { listID in
|
.navigationDestination(for: Destination.self) { destination in
|
||||||
|
switch destination {
|
||||||
|
case .list(let listID):
|
||||||
ListDetailView(listID: 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 openCount: Int { allOpenItems.count }
|
||||||
|
|
||||||
var urgentCount: Int {
|
var urgentItems: [TodoItem] {
|
||||||
let threeDaysFromNow = Calendar.current.date(byAdding: .day, value: 3, to: Date())!
|
let threeDaysFromNow = Calendar.current.date(byAdding: .day, value: 3, to: Date())!
|
||||||
return allOpenItems.filter { item in
|
return allOpenItems.filter { item in
|
||||||
item.priority == .high || (item.deadline != nil && item.deadline! <= threeDaysFromNow)
|
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())!)
|
let startOfTomorrow = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!)
|
||||||
return allOpenItems.filter { item in
|
return allOpenItems.filter { item in
|
||||||
item.deadline != nil && item.deadline! < startOfTomorrow
|
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) {
|
init(modelContext: ModelContext) {
|
||||||
@@ -106,6 +118,16 @@ class ListStore {
|
|||||||
fetchLists()
|
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) {
|
func moveItem(_ itemID: UUID, from sourceListID: UUID, to targetListID: UUID) {
|
||||||
guard sourceListID != targetListID,
|
guard sourceListID != targetListID,
|
||||||
let sourceList = lists.first(where: { $0.id == sourceListID }),
|
let sourceList = lists.first(where: { $0.id == sourceListID }),
|
||||||
|
|||||||
@@ -17,27 +17,21 @@ struct ListDetailView: View {
|
|||||||
ContentUnavailableView("Keine Einträge", systemImage: "tray")
|
ContentUnavailableView("Keine Einträge", systemImage: "tray")
|
||||||
} else {
|
} else {
|
||||||
List {
|
List {
|
||||||
ForEach(todoList.sortedItems) { item in
|
TodoListView(
|
||||||
TodoRowView(item: item, onToggle: {
|
items: todoList.sortedItems,
|
||||||
|
onToggle: { item in
|
||||||
store.toggleItemCompleted(item.id, in: listID)
|
store.toggleItemCompleted(item.id, in: listID)
|
||||||
}, onTap: {
|
},
|
||||||
|
onTap: { item in
|
||||||
editorItem = item
|
editorItem = item
|
||||||
})
|
},
|
||||||
.swipeActions(edge: .leading) {
|
onDelete: { item in
|
||||||
Button {
|
store.deleteItem(item.id, from: listID)
|
||||||
|
},
|
||||||
|
onMove: { item in
|
||||||
moveItem = item
|
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 {
|
struct ListsOverviewView: View {
|
||||||
@Environment(ListStore.self) private var store
|
@Environment(ListStore.self) private var store
|
||||||
|
@Binding var navigationPath: [Destination]
|
||||||
@State private var newListName = ""
|
@State private var newListName = ""
|
||||||
@FocusState private var isFieldFocused: Bool
|
@FocusState private var isFieldFocused: Bool
|
||||||
|
|
||||||
@@ -9,16 +10,23 @@ struct ListsOverviewView: View {
|
|||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
|
Button { navigationPath.append(.filter(.open)) } label: {
|
||||||
StatsCardView(icon: "checklist", count: store.openCount, label: "Offen", color: .blue)
|
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)
|
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)
|
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))
|
.listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0))
|
||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
}
|
}
|
||||||
|
|
||||||
ForEach(store.lists) { list in
|
ForEach(store.lists) { list in
|
||||||
NavigationLink(value: list.id) {
|
NavigationLink(value: Destination.list(list.id)) {
|
||||||
HStack {
|
HStack {
|
||||||
Text(list.name)
|
Text(list.name)
|
||||||
Spacer()
|
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 {
|
struct TodoRowView: View {
|
||||||
let item: TodoItem
|
let item: TodoItem
|
||||||
let onToggle: () -> Void
|
let onToggle: () -> Void
|
||||||
|
var subtitle: String?
|
||||||
var onTap: (() -> Void)?
|
var onTap: (() -> Void)?
|
||||||
|
|
||||||
private var isOverdue: Bool {
|
private var isOverdue: Bool {
|
||||||
@@ -33,6 +34,12 @@ struct TodoRowView: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let subtitle {
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
|
|||||||
Reference in New Issue
Block a user