feat: added list overview and detail view
This commit is contained in:
@@ -2,16 +2,16 @@ import SwiftUI
|
|||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
NavigationStack {
|
||||||
Image(systemName: "globe")
|
ListsOverviewView()
|
||||||
.imageScale(.large)
|
.navigationDestination(for: UUID.self) { listID in
|
||||||
.foregroundStyle(.tint)
|
ListDetailView(listID: listID)
|
||||||
Text("Hello, world!")
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
ContentView()
|
ContentView()
|
||||||
|
.environment(ListStore())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import SwiftUI
|
|||||||
|
|
||||||
@main
|
@main
|
||||||
struct MindDumpApp: App {
|
struct MindDumpApp: App {
|
||||||
|
@State private var store = ListStore()
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView()
|
||||||
|
.environment(store)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
MindDump/Models/TodoItem.swift
Normal file
15
MindDump/Models/TodoItem.swift
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct TodoItem: Identifiable {
|
||||||
|
let id: UUID
|
||||||
|
var title: String
|
||||||
|
var isCompleted: Bool
|
||||||
|
let createdAt: Date
|
||||||
|
|
||||||
|
init(id: UUID = UUID(), title: String, isCompleted: Bool = false, createdAt: Date = Date()) {
|
||||||
|
self.id = id
|
||||||
|
self.title = title
|
||||||
|
self.isCompleted = isCompleted
|
||||||
|
self.createdAt = createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
15
MindDump/Models/TodoList.swift
Normal file
15
MindDump/Models/TodoList.swift
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct TodoList: Identifiable {
|
||||||
|
let id: UUID
|
||||||
|
var name: String
|
||||||
|
var items: [TodoItem]
|
||||||
|
let isInbox: Bool
|
||||||
|
|
||||||
|
init(id: UUID = UUID(), name: String, items: [TodoItem] = [], isInbox: Bool = false) {
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self.items = items
|
||||||
|
self.isInbox = isInbox
|
||||||
|
}
|
||||||
|
}
|
||||||
44
MindDump/ViewModels/ListStore.swift
Normal file
44
MindDump/ViewModels/ListStore.swift
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import Foundation
|
||||||
|
import Observation
|
||||||
|
|
||||||
|
@Observable
|
||||||
|
class ListStore {
|
||||||
|
var lists: [TodoList]
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.lists = [TodoList(name: "Inbox", isInbox: true)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func addList(name: String) {
|
||||||
|
let list = TodoList(name: name)
|
||||||
|
lists.append(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteList(_ list: TodoList) {
|
||||||
|
guard !list.isInbox else { return }
|
||||||
|
lists.removeAll { $0.id == list.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
func addItem(to listID: UUID, title: String) {
|
||||||
|
guard let index = lists.firstIndex(where: { $0.id == listID }) else { return }
|
||||||
|
let item = TodoItem(title: title)
|
||||||
|
lists[index].items.append(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteItem(_ itemID: UUID, from listID: UUID) {
|
||||||
|
guard let listIndex = lists.firstIndex(where: { $0.id == listID }) else { return }
|
||||||
|
lists[listIndex].items.removeAll { $0.id == itemID }
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateItem(_ itemID: UUID, in listID: UUID, title: String) {
|
||||||
|
guard let listIndex = lists.firstIndex(where: { $0.id == listID }),
|
||||||
|
let itemIndex = lists[listIndex].items.firstIndex(where: { $0.id == itemID }) else { return }
|
||||||
|
lists[listIndex].items[itemIndex].title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggleItemCompleted(_ itemID: UUID, in listID: UUID) {
|
||||||
|
guard let listIndex = lists.firstIndex(where: { $0.id == listID }),
|
||||||
|
let itemIndex = lists[listIndex].items.firstIndex(where: { $0.id == itemID }) else { return }
|
||||||
|
lists[listIndex].items[itemIndex].isCompleted.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
52
MindDump/Views/ListDetailView.swift
Normal file
52
MindDump/Views/ListDetailView.swift
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ListDetailView: View {
|
||||||
|
@Environment(ListStore.self) private var store
|
||||||
|
let listID: UUID
|
||||||
|
@State private var editorItem: TodoItem?
|
||||||
|
@State private var showingEditor = false
|
||||||
|
|
||||||
|
private var todoList: TodoList? {
|
||||||
|
store.lists.first { $0.id == listID }
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if let todoList {
|
||||||
|
if todoList.items.isEmpty {
|
||||||
|
ContentUnavailableView("Keine Einträge", systemImage: "tray")
|
||||||
|
} else {
|
||||||
|
List {
|
||||||
|
ForEach(todoList.items) { item in
|
||||||
|
TodoRowView(item: item, onToggle: {
|
||||||
|
store.toggleItemCompleted(item.id, in: listID)
|
||||||
|
}, onTap: {
|
||||||
|
editorItem = item
|
||||||
|
showingEditor = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.onDelete { offsets in
|
||||||
|
for index in offsets {
|
||||||
|
store.deleteItem(todoList.items[index].id, from: listID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(todoList?.name ?? "")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button(action: {
|
||||||
|
editorItem = nil
|
||||||
|
showingEditor = true
|
||||||
|
}) {
|
||||||
|
Image(systemName: "plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showingEditor) {
|
||||||
|
TodoEditorView(listID: listID, item: editorItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
MindDump/Views/ListsOverviewView.swift
Normal file
50
MindDump/Views/ListsOverviewView.swift
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ListsOverviewView: View {
|
||||||
|
@Environment(ListStore.self) private var store
|
||||||
|
@State private var showingAddList = false
|
||||||
|
@State private var newListName = ""
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
ForEach(store.lists) { list in
|
||||||
|
NavigationLink(value: list.id) {
|
||||||
|
HStack {
|
||||||
|
Text(list.name)
|
||||||
|
Spacer()
|
||||||
|
Text("\(list.items.count)")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete(perform: deleteLists)
|
||||||
|
}
|
||||||
|
.navigationTitle("MindDump")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button(action: { showingAddList = true }) {
|
||||||
|
Image(systemName: "plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert("Neue Liste", isPresented: $showingAddList) {
|
||||||
|
TextField("Name", text: $newListName)
|
||||||
|
Button("Abbrechen", role: .cancel) {
|
||||||
|
newListName = ""
|
||||||
|
}
|
||||||
|
Button("Erstellen") {
|
||||||
|
let name = newListName.trimmingCharacters(in: .whitespaces)
|
||||||
|
if !name.isEmpty {
|
||||||
|
store.addList(name: name)
|
||||||
|
}
|
||||||
|
newListName = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deleteLists(at offsets: IndexSet) {
|
||||||
|
for index in offsets {
|
||||||
|
store.deleteList(store.lists[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
MindDump/Views/TodoEditorView.swift
Normal file
52
MindDump/Views/TodoEditorView.swift
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TodoEditorView: View {
|
||||||
|
@Environment(ListStore.self) private var store
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
let listID: UUID
|
||||||
|
var item: TodoItem?
|
||||||
|
|
||||||
|
@State private var title: String = ""
|
||||||
|
@FocusState private var titleFocused: Bool
|
||||||
|
|
||||||
|
private var isEditing: Bool { item != nil }
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
TextField("Titel", text: $title)
|
||||||
|
.focused($titleFocused)
|
||||||
|
}
|
||||||
|
.navigationTitle(isEditing ? "Bearbeiten" : "Neuer Eintrag")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button("Abbrechen") { dismiss() }
|
||||||
|
}
|
||||||
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
Button("Fertig") { save() }
|
||||||
|
.disabled(title.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if let item {
|
||||||
|
title = item.title
|
||||||
|
}
|
||||||
|
titleFocused = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func save() {
|
||||||
|
let trimmed = title.trimmingCharacters(in: .whitespaces)
|
||||||
|
guard !trimmed.isEmpty else { return }
|
||||||
|
|
||||||
|
if let item {
|
||||||
|
store.updateItem(item.id, in: listID, title: trimmed)
|
||||||
|
} else {
|
||||||
|
store.addItem(to: listID, title: trimmed)
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
27
MindDump/Views/TodoRowView.swift
Normal file
27
MindDump/Views/TodoRowView.swift
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TodoRowView: View {
|
||||||
|
let item: TodoItem
|
||||||
|
let onToggle: () -> Void
|
||||||
|
var onTap: (() -> Void)?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Button(action: onToggle) {
|
||||||
|
Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle")
|
||||||
|
.foregroundStyle(item.isCompleted ? .green : .secondary)
|
||||||
|
.imageScale(.large)
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
|
||||||
|
Button(action: { onTap?() }) {
|
||||||
|
Text(item.title)
|
||||||
|
.strikethrough(item.isCompleted)
|
||||||
|
.foregroundStyle(item.isCompleted ? .secondary : .primary)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user