feat: add extended fields (notes, deadline, priority) to todo editor
This commit is contained in:
23
MindDump/Models/Priority.swift
Normal file
23
MindDump/Models/Priority.swift
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum Priority: Int, CaseIterable {
|
||||||
|
case low = 0
|
||||||
|
case medium = 1
|
||||||
|
case high = 2
|
||||||
|
|
||||||
|
var label: String {
|
||||||
|
switch self {
|
||||||
|
case .low: "Niedrig"
|
||||||
|
case .medium: "Mittel"
|
||||||
|
case .high: "Hoch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var color: Color {
|
||||||
|
switch self {
|
||||||
|
case .low: .green
|
||||||
|
case .medium: .yellow
|
||||||
|
case .high: .red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,11 +5,28 @@ struct TodoItem: Identifiable {
|
|||||||
var title: String
|
var title: String
|
||||||
var isCompleted: Bool
|
var isCompleted: Bool
|
||||||
let createdAt: Date
|
let createdAt: Date
|
||||||
|
var notes: String?
|
||||||
|
var deadline: Date?
|
||||||
|
var priority: Priority?
|
||||||
|
var modifiedAt: Date?
|
||||||
|
|
||||||
init(id: UUID = UUID(), title: String, isCompleted: Bool = false, createdAt: Date = Date()) {
|
init(
|
||||||
|
id: UUID = UUID(),
|
||||||
|
title: String,
|
||||||
|
isCompleted: Bool = false,
|
||||||
|
createdAt: Date = Date(),
|
||||||
|
notes: String? = nil,
|
||||||
|
deadline: Date? = nil,
|
||||||
|
priority: Priority? = nil,
|
||||||
|
modifiedAt: Date? = nil
|
||||||
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.title = title
|
self.title = title
|
||||||
self.isCompleted = isCompleted
|
self.isCompleted = isCompleted
|
||||||
self.createdAt = createdAt
|
self.createdAt = createdAt
|
||||||
|
self.notes = notes
|
||||||
|
self.deadline = deadline
|
||||||
|
self.priority = priority
|
||||||
|
self.modifiedAt = modifiedAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,15 @@ class ListStore {
|
|||||||
lists.removeAll { $0.id == list.id }
|
lists.removeAll { $0.id == list.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
func addItem(to listID: UUID, title: String) {
|
func addItem(
|
||||||
|
to listID: UUID,
|
||||||
|
title: String,
|
||||||
|
notes: String? = nil,
|
||||||
|
deadline: Date? = nil,
|
||||||
|
priority: Priority? = nil
|
||||||
|
) {
|
||||||
guard let index = lists.firstIndex(where: { $0.id == listID }) else { return }
|
guard let index = lists.firstIndex(where: { $0.id == listID }) else { return }
|
||||||
let item = TodoItem(title: title)
|
let item = TodoItem(title: title, notes: notes, deadline: deadline, priority: priority)
|
||||||
lists[index].items.append(item)
|
lists[index].items.append(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,15 +40,31 @@ class ListStore {
|
|||||||
lists[listIndex].items.removeAll { $0.id == itemID }
|
lists[listIndex].items.removeAll { $0.id == itemID }
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateItem(_ itemID: UUID, in listID: UUID, title: String) {
|
func updateItem(
|
||||||
|
_ itemID: UUID,
|
||||||
|
in listID: UUID,
|
||||||
|
title: String,
|
||||||
|
notes: String? = nil,
|
||||||
|
deadline: Date? = nil,
|
||||||
|
priority: Priority? = nil,
|
||||||
|
isCompleted: Bool? = nil
|
||||||
|
) {
|
||||||
guard let listIndex = lists.firstIndex(where: { $0.id == listID }),
|
guard let listIndex = lists.firstIndex(where: { $0.id == listID }),
|
||||||
let itemIndex = lists[listIndex].items.firstIndex(where: { $0.id == itemID }) else { return }
|
let itemIndex = lists[listIndex].items.firstIndex(where: { $0.id == itemID }) else { return }
|
||||||
lists[listIndex].items[itemIndex].title = title
|
lists[listIndex].items[itemIndex].title = title
|
||||||
|
lists[listIndex].items[itemIndex].notes = notes
|
||||||
|
lists[listIndex].items[itemIndex].deadline = deadline
|
||||||
|
lists[listIndex].items[itemIndex].priority = priority
|
||||||
|
if let isCompleted {
|
||||||
|
lists[listIndex].items[itemIndex].isCompleted = isCompleted
|
||||||
|
}
|
||||||
|
lists[listIndex].items[itemIndex].modifiedAt = Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleItemCompleted(_ itemID: UUID, in listID: UUID) {
|
func toggleItemCompleted(_ itemID: UUID, in listID: UUID) {
|
||||||
guard let listIndex = lists.firstIndex(where: { $0.id == listID }),
|
guard let listIndex = lists.firstIndex(where: { $0.id == listID }),
|
||||||
let itemIndex = lists[listIndex].items.firstIndex(where: { $0.id == itemID }) else { return }
|
let itemIndex = lists[listIndex].items.firstIndex(where: { $0.id == itemID }) else { return }
|
||||||
lists[listIndex].items[itemIndex].isCompleted.toggle()
|
lists[listIndex].items[itemIndex].isCompleted.toggle()
|
||||||
|
lists[listIndex].items[itemIndex].modifiedAt = Date()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,94 @@ struct TodoEditorView: View {
|
|||||||
var item: TodoItem?
|
var item: TodoItem?
|
||||||
|
|
||||||
@State private var title: String = ""
|
@State private var title: String = ""
|
||||||
|
@State private var notes: String = ""
|
||||||
|
@State private var deadline: Date = Date()
|
||||||
|
@State private var priority: Priority = .medium
|
||||||
|
|
||||||
|
@State private var showNotes: Bool = false
|
||||||
|
@State private var showDeadline: Bool = false
|
||||||
|
@State private var showPriority: Bool = false
|
||||||
|
|
||||||
@FocusState private var titleFocused: Bool
|
@FocusState private var titleFocused: Bool
|
||||||
|
|
||||||
private var isEditing: Bool { item != nil }
|
private var isEditing: Bool { item != nil }
|
||||||
|
|
||||||
|
private var hasHiddenFields: Bool {
|
||||||
|
!showNotes || !showDeadline || !showPriority
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
|
// Titel
|
||||||
|
Section {
|
||||||
TextField("Titel", text: $title)
|
TextField("Titel", text: $title)
|
||||||
.focused($titleFocused)
|
.focused($titleFocused)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notiz
|
||||||
|
if showNotes {
|
||||||
|
Section {
|
||||||
|
TextField("Notiz hinzufügen...", text: $notes, axis: .vertical)
|
||||||
|
.lineLimit(3...6)
|
||||||
|
} header: {
|
||||||
|
sectionHeader("Notiz", show: $showNotes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deadline
|
||||||
|
if showDeadline {
|
||||||
|
Section {
|
||||||
|
DatePicker("Fällig am", selection: $deadline, displayedComponents: [.date, .hourAndMinute])
|
||||||
|
} header: {
|
||||||
|
sectionHeader("Deadline", show: $showDeadline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prioritaet
|
||||||
|
if showPriority {
|
||||||
|
Section {
|
||||||
|
Picker("Priorität", selection: $priority) {
|
||||||
|
ForEach(Priority.allCases, id: \.self) { p in
|
||||||
|
Label(p.label, systemImage: "circle.fill")
|
||||||
|
.foregroundStyle(p.color)
|
||||||
|
.tag(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
sectionHeader("Priorität", show: $showPriority)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hinzufuegen
|
||||||
|
if hasHiddenFields {
|
||||||
|
Section {
|
||||||
|
if !showNotes {
|
||||||
|
addFieldButton("Notiz", systemImage: "note.text") { showNotes = true }
|
||||||
|
}
|
||||||
|
if !showDeadline {
|
||||||
|
addFieldButton("Deadline", systemImage: "calendar") { showDeadline = true }
|
||||||
|
}
|
||||||
|
if !showPriority {
|
||||||
|
addFieldButton("Priorität", systemImage: "flag") { showPriority = true }
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Hinzufügen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meta
|
||||||
|
if isEditing, let item {
|
||||||
|
Section {
|
||||||
|
LabeledContent("Erstellt am", value: item.createdAt.formatted(date: .abbreviated, time: .shortened))
|
||||||
|
if let modifiedAt = item.modifiedAt {
|
||||||
|
LabeledContent("Geändert am", value: modifiedAt.formatted(date: .abbreviated, time: .shortened))
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Info")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.navigationTitle(isEditing ? "Bearbeiten" : "Neuer Eintrag")
|
.navigationTitle(isEditing ? "Bearbeiten" : "Neuer Eintrag")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
@@ -32,20 +110,76 @@ struct TodoEditorView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
if let item {
|
if let item {
|
||||||
title = item.title
|
title = item.title
|
||||||
|
|
||||||
|
if let n = item.notes, !n.isEmpty {
|
||||||
|
notes = n
|
||||||
|
showNotes = true
|
||||||
|
}
|
||||||
|
if let d = item.deadline {
|
||||||
|
deadline = d
|
||||||
|
showDeadline = true
|
||||||
|
}
|
||||||
|
if let p = item.priority {
|
||||||
|
priority = p
|
||||||
|
showPriority = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
titleFocused = true
|
titleFocused = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
private func sectionHeader(_ label: String, show: Binding<Bool>) -> some View {
|
||||||
|
HStack {
|
||||||
|
Text(label)
|
||||||
|
Spacer()
|
||||||
|
Button {
|
||||||
|
withAnimation { show.wrappedValue = false }
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "xmark.circle.fill")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addFieldButton(_ label: String, systemImage: String, action: @escaping () -> Void) -> some View {
|
||||||
|
Button {
|
||||||
|
withAnimation { action() }
|
||||||
|
} label: {
|
||||||
|
Label(label, systemImage: systemImage)
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func save() {
|
private func save() {
|
||||||
let trimmed = title.trimmingCharacters(in: .whitespaces)
|
let trimmed = title.trimmingCharacters(in: .whitespaces)
|
||||||
guard !trimmed.isEmpty else { return }
|
guard !trimmed.isEmpty else { return }
|
||||||
|
|
||||||
|
let finalNotes: String? = showNotes && !notes.trimmingCharacters(in: .whitespaces).isEmpty
|
||||||
|
? notes.trimmingCharacters(in: .whitespaces)
|
||||||
|
: nil
|
||||||
|
let finalDeadline: Date? = showDeadline ? deadline : nil
|
||||||
|
let finalPriority: Priority? = showPriority ? priority : nil
|
||||||
|
|
||||||
if let item {
|
if let item {
|
||||||
store.updateItem(item.id, in: listID, title: trimmed)
|
store.updateItem(
|
||||||
|
item.id,
|
||||||
|
in: listID,
|
||||||
|
title: trimmed,
|
||||||
|
notes: finalNotes,
|
||||||
|
deadline: finalDeadline,
|
||||||
|
priority: finalPriority
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
store.addItem(to: listID, title: trimmed)
|
store.addItem(
|
||||||
|
to: listID,
|
||||||
|
title: trimmed,
|
||||||
|
notes: finalNotes,
|
||||||
|
deadline: finalDeadline,
|
||||||
|
priority: finalPriority
|
||||||
|
)
|
||||||
}
|
}
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,35 @@ struct TodoRowView: View {
|
|||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
|
|
||||||
Button(action: { onTap?() }) {
|
Button(action: { onTap?() }) {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(item.title)
|
Text(item.title)
|
||||||
.strikethrough(item.isCompleted)
|
.strikethrough(item.isCompleted)
|
||||||
.foregroundStyle(item.isCompleted ? .secondary : .primary)
|
.foregroundStyle(item.isCompleted ? .secondary : .primary)
|
||||||
|
|
||||||
|
if let notes = item.notes, !notes.isEmpty {
|
||||||
|
Text(noteSnippet(notes))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
if let priority = item.priority {
|
||||||
|
Image(systemName: "circle.fill")
|
||||||
|
.font(.system(size: 8))
|
||||||
|
.foregroundStyle(priority.color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func noteSnippet(_ text: String) -> String {
|
||||||
|
if text.count > 50 {
|
||||||
|
return String(text.prefix(50)) + "..."
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user