feat: add visible filter chips and sort menu to list detail view
This commit is contained in:
23
MindDump/Models/ListDetailConfig.swift
Normal file
23
MindDump/Models/ListDetailConfig.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
import Foundation
|
||||
|
||||
enum ListDetailSort: CaseIterable {
|
||||
case oldest, newest, dueDate, priority
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .oldest: "Älteste zuerst"
|
||||
case .newest: "Neueste zuerst"
|
||||
case .dueDate: "Deadline zuerst"
|
||||
case .priority: "Priorität zuerst"
|
||||
}
|
||||
}
|
||||
|
||||
var shortLabel: String {
|
||||
switch self {
|
||||
case .oldest: "Älteste"
|
||||
case .newest: "Neueste"
|
||||
case .dueDate: "Deadline"
|
||||
case .priority: "Priorität"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,50 @@ class ListStore {
|
||||
deleteItem(item.id, from: listID)
|
||||
}
|
||||
|
||||
func filteredOpenItems(for list: TodoList, urgent: Bool, due: Bool, sort: ListDetailSort) -> [TodoItem] {
|
||||
var items = list.openItems
|
||||
|
||||
if urgent || due {
|
||||
let now = Date()
|
||||
let threeDaysFromNow = Calendar.current.date(byAdding: .day, value: 3, to: now)!
|
||||
let startOfTomorrow = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: now)!)
|
||||
|
||||
items = items.filter { item in
|
||||
let isUrgent = item.priority == .high || (item.deadline != nil && item.deadline! <= threeDaysFromNow)
|
||||
let isDue = item.deadline != nil && item.deadline! < startOfTomorrow
|
||||
|
||||
if urgent && due { return isUrgent && isDue }
|
||||
if urgent { return isUrgent }
|
||||
return isDue
|
||||
}
|
||||
}
|
||||
|
||||
switch sort {
|
||||
case .oldest:
|
||||
items.sort { $0.createdAt < $1.createdAt }
|
||||
case .newest:
|
||||
items.sort { $0.createdAt > $1.createdAt }
|
||||
case .dueDate:
|
||||
items.sort { lhs, rhs in
|
||||
switch (lhs.deadline, rhs.deadline) {
|
||||
case let (l?, r?): l < r
|
||||
case (_?, nil): true
|
||||
case (nil, _?): false
|
||||
case (nil, nil): lhs.createdAt < rhs.createdAt
|
||||
}
|
||||
}
|
||||
case .priority:
|
||||
items.sort { lhs, rhs in
|
||||
let lp = lhs.priority?.rawValue ?? -1
|
||||
let rp = rhs.priority?.rawValue ?? -1
|
||||
if lp != rp { return lp > rp }
|
||||
return lhs.createdAt < rhs.createdAt
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func moveItem(_ itemID: UUID, from sourceListID: UUID, to targetListID: UUID) {
|
||||
guard sourceListID != targetListID,
|
||||
let sourceList = lists.first(where: { $0.id == sourceListID }),
|
||||
|
||||
@@ -6,6 +6,9 @@ struct ListDetailView: View {
|
||||
@State private var editorItem: TodoItem?
|
||||
@State private var moveItem: TodoItem?
|
||||
@State private var showCompleted = true
|
||||
@State private var filterUrgent = false
|
||||
@State private var filterDue = false
|
||||
@State private var selectedSort: ListDetailSort = .oldest
|
||||
|
||||
private var todoList: TodoList? {
|
||||
store.lists.first { $0.id == listID }
|
||||
@@ -17,10 +20,32 @@ struct ListDetailView: View {
|
||||
if todoList.items.isEmpty {
|
||||
ContentUnavailableView("Keine Einträge", systemImage: "tray")
|
||||
} else {
|
||||
let filteredItems = store.filteredOpenItems(
|
||||
for: todoList, urgent: filterUrgent, due: filterDue, sort: selectedSort
|
||||
)
|
||||
|
||||
List {
|
||||
Section {
|
||||
HStack(spacing: 12) {
|
||||
chipButton("Dringend", icon: "exclamationmark.triangle", isActive: filterUrgent, color: .orange) {
|
||||
withAnimation { filterUrgent.toggle() }
|
||||
}
|
||||
chipButton("Fällig", icon: "calendar.badge.clock", isActive: filterDue, color: .red) {
|
||||
withAnimation { filterDue.toggle() }
|
||||
}
|
||||
sortMenu
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
|
||||
Section {
|
||||
if filteredItems.isEmpty {
|
||||
emptyState
|
||||
} else {
|
||||
TodoListView(
|
||||
items: todoList.openItems,
|
||||
items: filteredItems,
|
||||
onToggle: { item in
|
||||
store.toggleItemCompleted(item.id, in: listID)
|
||||
},
|
||||
@@ -35,6 +60,7 @@ struct ListDetailView: View {
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if !todoList.completedItems.isEmpty {
|
||||
Section {
|
||||
@@ -81,4 +107,82 @@ struct ListDetailView: View {
|
||||
MoveToListView(itemID: item.id, currentListID: listID)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Filter Chip
|
||||
|
||||
@ViewBuilder
|
||||
private func chipButton(_ label: String, icon: String, isActive: Bool, color: Color, action: @escaping () -> Void) -> some View {
|
||||
Button(action: action) {
|
||||
Image(systemName: icon)
|
||||
.font(.subheadline.weight(.medium))
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(isActive ? color.opacity(0.15) : Color(.systemGray6))
|
||||
.foregroundStyle(isActive ? color : .secondary)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
// MARK: - Sort Menu
|
||||
|
||||
private var sortMenu: some View {
|
||||
Menu {
|
||||
Picker("Sortierung", selection: $selectedSort) {
|
||||
ForEach(ListDetailSort.allCases, id: \.self) { sort in
|
||||
Text(sort.label).tag(sort)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "arrow.up.arrow.down")
|
||||
ZStack(alignment: .leading) {
|
||||
ForEach(ListDetailSort.allCases, id: \.self) { sort in
|
||||
Text(sort.shortLabel)
|
||||
.opacity(sort == selectedSort ? 1 : 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Empty State
|
||||
|
||||
private var emptyState: some View {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: emptyIcon)
|
||||
.font(.title2)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(emptyMessage)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 24)
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
|
||||
private var emptyMessage: String {
|
||||
if filterUrgent && filterDue {
|
||||
"Keine dringenden oder fälligen Aufgaben"
|
||||
} else if filterUrgent {
|
||||
"Keine dringenden Aufgaben"
|
||||
} else if filterDue {
|
||||
"Keine fälligen Aufgaben"
|
||||
} else {
|
||||
"Keine offenen Aufgaben"
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyIcon: String {
|
||||
if filterUrgent {
|
||||
"exclamationmark.triangle"
|
||||
} else if filterDue {
|
||||
"calendar.badge.checkmark"
|
||||
} else {
|
||||
"checkmark.circle"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user