From 36859a4f4505ec76f39904c388d5bdeb1e1ed359 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 6 Mar 2026 18:34:28 +0100 Subject: [PATCH] refactor: extract TodoListBodyView to eliminate duplication between ListDetailView and FilteredTodoListView --- MindDump/Views/FilteredTodoListView.swift | 152 +++------------------- MindDump/Views/ListDetailView.swift | 136 ++----------------- MindDump/Views/TodoListBodyView.swift | 150 +++++++++++++++++++++ 3 files changed, 178 insertions(+), 260 deletions(-) create mode 100644 MindDump/Views/TodoListBodyView.swift diff --git a/MindDump/Views/FilteredTodoListView.swift b/MindDump/Views/FilteredTodoListView.swift index 1bd560b..1a55d75 100644 --- a/MindDump/Views/FilteredTodoListView.swift +++ b/MindDump/Views/FilteredTodoListView.swift @@ -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" - } } } diff --git a/MindDump/Views/ListDetailView.swift b/MindDump/Views/ListDetailView.swift index 412f1df..50b3bbe 100644 --- a/MindDump/Views/ListDetailView.swift +++ b/MindDump/Views/ListDetailView.swift @@ -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" - } } } diff --git a/MindDump/Views/TodoListBodyView.swift b/MindDump/Views/TodoListBodyView.swift new file mode 100644 index 0000000..2793f19 --- /dev/null +++ b/MindDump/Views/TodoListBodyView.swift @@ -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" + } + } +}