refactor: extract TodoListBodyView to eliminate duplication between ListDetailView and FilteredTodoListView
This commit is contained in:
@@ -1,147 +1,25 @@
|
|||||||
import SwiftUI
|
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 {
|
struct FilteredTodoListView: View {
|
||||||
@Environment(ListStore.self) private var store
|
@Environment(ListStore.self) private var store
|
||||||
let filter: TodoFilter
|
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 {
|
var body: some View {
|
||||||
Group {
|
TodoListBodyView(
|
||||||
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,
|
showListName: true,
|
||||||
onToggle: { item in
|
isEmpty: store.allOpenItems.isEmpty && store.allCompletedItems.isEmpty,
|
||||||
store.toggleItemCompleted(item)
|
initialFilterUrgent: filter == .urgent,
|
||||||
|
initialFilterDue: filter == .due,
|
||||||
|
openItemsProvider: { urgent, due, sort in
|
||||||
|
store.filteredOpenItems(urgent: urgent, due: due, sort: sort)
|
||||||
},
|
},
|
||||||
onTap: { item in
|
completedItems: store.allCompletedItems,
|
||||||
editorItem = item
|
onToggle: { store.toggleItemCompleted($0) },
|
||||||
},
|
onDelete: { store.deleteItem($0) }
|
||||||
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("Aufgaben")
|
.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
|
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 {
|
struct ListDetailView: View {
|
||||||
@Environment(ListStore.self) private var store
|
@Environment(ListStore.self) private var store
|
||||||
let listID: UUID
|
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? {
|
private var todoList: TodoList? {
|
||||||
store.lists.first { $0.id == listID }
|
store.lists.first { $0.id == listID }
|
||||||
@@ -17,125 +14,18 @@ struct ListDetailView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if let todoList {
|
if let todoList {
|
||||||
if todoList.items.isEmpty {
|
TodoListBodyView(
|
||||||
ContentUnavailableView("Keine Einträge", systemImage: "tray")
|
showListName: false,
|
||||||
} else {
|
isEmpty: todoList.items.isEmpty,
|
||||||
let filteredItems = store.filteredOpenItems(
|
openItemsProvider: { urgent, due, sort in
|
||||||
for: todoList, urgent: filterUrgent, due: filterDue, sort: selectedSort
|
store.filteredOpenItems(for: todoList, urgent: urgent, due: due, sort: sort)
|
||||||
|
},
|
||||||
|
completedItems: todoList.completedItems,
|
||||||
|
onToggle: { store.toggleItemCompleted($0) },
|
||||||
|
onDelete: { store.deleteItem($0) }
|
||||||
)
|
)
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(todoList?.name ?? "")
|
.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