refactor: unify filter chips and sections across list and global views

This commit is contained in:
2026-02-16 14:14:24 +01:00
parent 308b9b56ee
commit edde291059
4 changed files with 143 additions and 122 deletions

View File

@@ -2,28 +2,4 @@ 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"
}
}
}

View File

@@ -19,29 +19,21 @@ class ListStore {
var openCount: Int { allOpenItems.count }
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)
}
allOpenItems.filter { isUrgent($0) }
}
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
}
allOpenItems.filter { isDue($0) }
}
var dueCount: Int { dueItems.count }
func items(for filter: TodoFilter) -> [TodoItem] {
switch filter {
case .open: allOpenItems
case .urgent: urgentItems
case .due: dueItems
}
var allCompletedItems: [TodoItem] {
lists.flatMap { $0.items }
.filter { $0.isCompleted }
.sorted { ($0.modifiedAt ?? $0.createdAt) > ($1.modifiedAt ?? $1.createdAt) }
}
init(modelContext: ModelContext) {
@@ -128,21 +120,14 @@ class ListStore {
deleteItem(item.id, from: listID)
}
func filteredOpenItems(for list: TodoList, urgent: Bool, due: Bool, sort: ListDetailSort) -> [TodoItem] {
var items = list.openItems
func filteredOpenItems(for list: TodoList? = nil, urgent: Bool, due: Bool, sort: ListDetailSort) -> [TodoItem] {
var items = list?.openItems ?? allOpenItems
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
if urgent && due { return isUrgent(item) && isDue(item) }
if urgent { return isUrgent(item) }
return isDue(item)
}
}
@@ -203,6 +188,16 @@ class ListStore {
lists = inbox + rest
}
private func isUrgent(_ item: TodoItem) -> Bool {
let threeDaysFromNow = Calendar.current.date(byAdding: .day, value: 3, to: Date())!
return item.priority == .high || (item.deadline != nil && item.deadline! <= threeDaysFromNow)
}
private func isDue(_ item: TodoItem) -> Bool {
let startOfTomorrow = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!)
return item.deadline != nil && item.deadline! < startOfTomorrow
}
private func save() {
try? modelContext.save()
}

View File

@@ -26,18 +26,11 @@ struct ListDetailView: View {
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)
FilterChipBar(
filterUrgent: $filterUrgent,
filterDue: $filterDue,
selectedSort: $selectedSort
)
}
Section {
@@ -108,46 +101,6 @@ struct ListDetailView: View {
}
}
// 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 {

View File

@@ -5,37 +5,96 @@ struct TodoListScreen: View {
let filter: TodoFilter
@State private var editorItem: TodoItem?
@State private var moveItem: TodoItem?
@State private var showCompleted = true
@State private var filterUrgent: Bool
@State private var filterDue: Bool
@State private var selectedSort: ListDetailSort = .oldest
private var items: [TodoItem] {
store.items(for: filter)
init(filter: TodoFilter) {
self.filter = filter
_filterUrgent = State(initialValue: filter == .urgent)
_filterDue = State(initialValue: filter == .due)
}
var body: some View {
Group {
if items.isEmpty {
ContentUnavailableView(filter.emptyMessage, systemImage: filter.emptyIcon)
let filteredItems = store.filteredOpenItems(
urgent: filterUrgent, due: filterDue, sort: selectedSort
)
if store.allOpenItems.isEmpty && store.allCompletedItems.isEmpty {
ContentUnavailableView("Keine Einträge", systemImage: "tray")
} 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
Section {
FilterChipBar(
filterUrgent: $filterUrgent,
filterDue: $filterDue,
selectedSort: $selectedSort
)
}
Section {
if filteredItems.isEmpty {
emptyState
} else {
TodoListView(
items: filteredItems,
showListName: true,
onToggle: { item in
store.toggleItemCompleted(item)
},
onTap: { item in
editorItem = item
},
onDelete: { item in
store.deleteItem(item)
},
onMove: { item in
moveItem = item
}
)
}
)
}
if !store.allCompletedItems.isEmpty {
Section {
if showCompleted {
TodoListView(
items: store.allCompletedItems,
showListName: true,
onToggle: { item in
store.toggleItemCompleted(item)
},
onTap: { item in
editorItem = item
},
onDelete: { item in
store.deleteItem(item)
},
onMove: { item in
moveItem = item
}
)
}
} header: {
Button {
withAnimation {
showCompleted.toggle()
}
} label: {
HStack {
Text("Erledigt (\(store.allCompletedItems.count))")
Spacer()
Image(systemName: showCompleted ? "chevron.down" : "chevron.right")
}
}
}
}
}
}
}
.navigationTitle(filter.title)
.navigationTitle("Aufgaben")
.sheet(item: $editorItem) { item in
if let listID = item.list?.id {
TodoEditorView(listID: listID, item: item)
@@ -47,4 +106,42 @@ struct TodoListScreen: View {
}
}
}
// 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"
}
}
}