refactor: extract TodoListBodyView to eliminate duplication between ListDetailView and FilteredTodoListView
This commit is contained in:
@@ -1,147 +1,25 @@
|
||||
import SwiftUI
|
||||
|
||||
// Displays todos filtered globally (across all lists) by a TodoFilter.
|
||||
// Delegates the shared list structure to TodoListBodyView and only configures the filter-specific data source.
|
||||
|
||||
struct FilteredTodoListView: View {
|
||||
@Environment(ListStore.self) private var store
|
||||
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
|
||||
|
||||
init(filter: TodoFilter) {
|
||||
self.filter = filter
|
||||
_filterUrgent = State(initialValue: filter == .urgent)
|
||||
_filterDue = State(initialValue: filter == .due)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TodoListBodyView(
|
||||
showListName: true,
|
||||
isEmpty: store.allOpenItems.isEmpty && store.allCompletedItems.isEmpty,
|
||||
initialFilterUrgent: filter == .urgent,
|
||||
initialFilterDue: filter == .due,
|
||||
openItemsProvider: { urgent, due, sort in
|
||||
store.filteredOpenItems(urgent: urgent, due: due, sort: sort)
|
||||
},
|
||||
completedItems: store.allCompletedItems,
|
||||
onToggle: { store.toggleItemCompleted($0) },
|
||||
onDelete: { store.deleteItem($0) }
|
||||
)
|
||||
.navigationTitle("Aufgaben")
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import SwiftUI
|
||||
|
||||
// Displays the entries of a single TodoList. Delegates the shared list structure
|
||||
// to TodoListBodyView and only configures the list-specific data source.
|
||||
|
||||
struct ListDetailView: View {
|
||||
@Environment(ListStore.self) private var store
|
||||
let listID: UUID
|
||||
@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,125 +14,18 @@ struct ListDetailView: View {
|
||||
var body: some View {
|
||||
Group {
|
||||
if let todoList {
|
||||
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 {
|
||||
FilterChipBar(
|
||||
filterUrgent: $filterUrgent,
|
||||
filterDue: $filterDue,
|
||||
selectedSort: $selectedSort
|
||||
)
|
||||
}
|
||||
|
||||
Section {
|
||||
if filteredItems.isEmpty {
|
||||
emptyState
|
||||
} else {
|
||||
TodoListView(
|
||||
items: filteredItems,
|
||||
onToggle: { item in
|
||||
store.toggleItemCompleted(item.id, in: listID)
|
||||
},
|
||||
onTap: { item in
|
||||
editorItem = item
|
||||
},
|
||||
onDelete: { item in
|
||||
store.deleteItem(item.id, from: listID)
|
||||
},
|
||||
onMove: { item in
|
||||
moveItem = item
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if !todoList.completedItems.isEmpty {
|
||||
Section {
|
||||
if showCompleted {
|
||||
TodoListView(
|
||||
items: todoList.completedItems,
|
||||
onToggle: { item in
|
||||
store.toggleItemCompleted(item.id, in: listID)
|
||||
},
|
||||
onTap: { item in
|
||||
editorItem = item
|
||||
},
|
||||
onDelete: { item in
|
||||
store.deleteItem(item.id, from: listID)
|
||||
},
|
||||
onMove: { item in
|
||||
moveItem = item
|
||||
}
|
||||
)
|
||||
}
|
||||
} header: {
|
||||
Button {
|
||||
withAnimation {
|
||||
showCompleted.toggle()
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Erledigt (\(todoList.completedItems.count))")
|
||||
Spacer()
|
||||
Image(systemName: showCompleted ? "chevron.down" : "chevron.right")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TodoListBodyView(
|
||||
showListName: false,
|
||||
isEmpty: todoList.items.isEmpty,
|
||||
openItemsProvider: { urgent, due, sort in
|
||||
store.filteredOpenItems(for: todoList, urgent: urgent, due: due, sort: sort)
|
||||
},
|
||||
completedItems: todoList.completedItems,
|
||||
onToggle: { store.toggleItemCompleted($0) },
|
||||
onDelete: { store.deleteItem($0) }
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationTitle(todoList?.name ?? "")
|
||||
.sheet(item: $editorItem) { item in
|
||||
TodoEditorView(listID: listID, item: item)
|
||||
}
|
||||
.sheet(item: $moveItem) { item in
|
||||
MoveToListView(itemID: item.id, currentListID: listID)
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
150
MindDump/Views/TodoListBodyView.swift
Normal file
150
MindDump/Views/TodoListBodyView.swift
Normal file
@@ -0,0 +1,150 @@
|
||||
import SwiftUI
|
||||
|
||||
// Shared list body used by both ListDetailView (single list)
|
||||
// and FilteredTodoListView (global filter) to eliminate code duplication.
|
||||
|
||||
struct TodoListBodyView: View {
|
||||
let showListName: Bool
|
||||
let isEmpty: Bool
|
||||
let openItemsProvider: (Bool, Bool, ListDetailSort) -> [TodoItem]
|
||||
let completedItems: [TodoItem]
|
||||
let onToggle: (TodoItem) -> Void
|
||||
let onDelete: (TodoItem) -> Void
|
||||
|
||||
@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
|
||||
|
||||
init(
|
||||
showListName: Bool,
|
||||
isEmpty: Bool,
|
||||
initialFilterUrgent: Bool = false,
|
||||
initialFilterDue: Bool = false,
|
||||
openItemsProvider: @escaping (Bool, Bool, ListDetailSort) -> [TodoItem],
|
||||
completedItems: [TodoItem],
|
||||
onToggle: @escaping (TodoItem) -> Void,
|
||||
onDelete: @escaping (TodoItem) -> Void
|
||||
) {
|
||||
self.showListName = showListName
|
||||
self.isEmpty = isEmpty
|
||||
self.openItemsProvider = openItemsProvider
|
||||
self.completedItems = completedItems
|
||||
self.onToggle = onToggle
|
||||
self.onDelete = onDelete
|
||||
_filterUrgent = State(initialValue: initialFilterUrgent)
|
||||
_filterDue = State(initialValue: initialFilterDue)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if isEmpty {
|
||||
ContentUnavailableView("Keine Einträge", systemImage: "tray")
|
||||
} else {
|
||||
let filteredItems = openItemsProvider(filterUrgent, filterDue, selectedSort)
|
||||
|
||||
List {
|
||||
Section {
|
||||
FilterChipBar(
|
||||
filterUrgent: $filterUrgent,
|
||||
filterDue: $filterDue,
|
||||
selectedSort: $selectedSort
|
||||
)
|
||||
}
|
||||
|
||||
Section {
|
||||
if filteredItems.isEmpty {
|
||||
emptyState
|
||||
} else {
|
||||
TodoListView(
|
||||
items: filteredItems,
|
||||
showListName: showListName,
|
||||
onToggle: onToggle,
|
||||
onTap: { item in editorItem = item },
|
||||
onDelete: onDelete,
|
||||
onMove: { item in moveItem = item }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if !completedItems.isEmpty {
|
||||
Section {
|
||||
if showCompleted {
|
||||
TodoListView(
|
||||
items: completedItems,
|
||||
showListName: showListName,
|
||||
onToggle: onToggle,
|
||||
onTap: { item in editorItem = item },
|
||||
onDelete: onDelete,
|
||||
onMove: { item in moveItem = item }
|
||||
)
|
||||
}
|
||||
} header: {
|
||||
Button {
|
||||
withAnimation {
|
||||
showCompleted.toggle()
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Erledigt (\(completedItems.count))")
|
||||
Spacer()
|
||||
Image(systemName: showCompleted ? "chevron.down" : "chevron.right")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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