WIP: Widget-, Sync- & Event-Editor-Änderungen
Zwischenstand vor den Sharing/Gruppen/Import-Export-Features (gesichert, damit die neuen Features sauber darauf aufbauen).
This commit is contained in:
@@ -1,5 +1,16 @@
|
||||
import SwiftUI
|
||||
|
||||
private enum CalEditorContext: Identifiable {
|
||||
case create(Date)
|
||||
case edit(CalEvent)
|
||||
var id: String {
|
||||
switch self {
|
||||
case .create(let d): return "new-\(d.timeIntervalSince1970)"
|
||||
case .edit(let ev): return "edit-\(ev.id)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CalendarHostView: View {
|
||||
let api: CalendarrAPI
|
||||
@Binding var showMenu: Bool
|
||||
@@ -14,9 +25,7 @@ struct CalendarHostView: View {
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
@State private var store = CalendarStore()
|
||||
@State private var showEditor = false
|
||||
@State private var editorDate: Date = .now
|
||||
@State private var editingEvent: CalEvent? = nil
|
||||
@State private var editorContext: CalEditorContext? = nil
|
||||
@State private var selectedEvent: CalEvent? = nil
|
||||
@State private var visibleMonth: Date = .now
|
||||
@State private var showFilter = false
|
||||
@@ -40,6 +49,19 @@ struct CalendarHostView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Loading indicator
|
||||
|
||||
@ViewBuilder
|
||||
private var loadingIndicator: some View {
|
||||
if store.isLoading || store.isCachingBackground {
|
||||
ProgressView()
|
||||
.padding(14)
|
||||
.background(.regularMaterial, in: Circle())
|
||||
.shadow(color: .black.opacity(0.12), radius: 6, y: 2)
|
||||
.transition(.opacity.combined(with: .scale(scale: 0.85)))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Flat variant
|
||||
|
||||
private var flatVariant: some View {
|
||||
@@ -51,22 +73,11 @@ struct CalendarHostView: View {
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color(hex: bgHex))
|
||||
.overlay(alignment: .top) {
|
||||
if store.isLoading {
|
||||
ProgressView().padding(.top, 10).transition(.opacity)
|
||||
}
|
||||
loadingIndicator.padding(.top, 12)
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.2), value: store.isLoading || store.isCachingBackground)
|
||||
}
|
||||
.overlay(alignment: .bottomTrailing) { solidFAB }
|
||||
// Subtle background cache indicator (top-leading)
|
||||
.overlay(alignment: .topLeading) {
|
||||
if store.isCachingBackground {
|
||||
Image(systemName: "arrow.triangle.2.circlepath")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(6)
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
.modifier(calendarSheets)
|
||||
.task { await startup() }
|
||||
.onChange(of: store.currentDate) { _, _ in Task { await onNavigate() } }
|
||||
@@ -93,10 +104,9 @@ struct CalendarHostView: View {
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color(hex: bgHex))
|
||||
.overlay(alignment: .top) {
|
||||
if store.isLoading {
|
||||
ProgressView().padding(.top, 10).transition(.opacity)
|
||||
}
|
||||
loadingIndicator.padding(.top, 12)
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.2), value: store.isLoading || store.isCachingBackground)
|
||||
.overlay(alignment: .top) {
|
||||
if let err = store.lastError { errorBannerView(err).padding(.top, 8) }
|
||||
}
|
||||
@@ -247,13 +257,9 @@ struct CalendarHostView: View {
|
||||
case .month:
|
||||
// Month view uses vertical scroll – no horizontal swipe.
|
||||
MonthView(store: store,
|
||||
onDayTap: { editorDate = $0 },
|
||||
onDayTap: { store.currentDate = $0 },
|
||||
onEventTap: { selectedEvent = $0 },
|
||||
onCreateEvent: { day in
|
||||
editingEvent = nil
|
||||
editorDate = day
|
||||
showEditor = true
|
||||
},
|
||||
onCreateEvent: { day in editorContext = .create(day) },
|
||||
onShowWeek: { day in
|
||||
store.currentDate = day
|
||||
store.viewType = .week
|
||||
@@ -266,11 +272,7 @@ struct CalendarHostView: View {
|
||||
case .week:
|
||||
WeekView(store: store,
|
||||
onEventTap: { selectedEvent = $0 },
|
||||
onCreateEvent: { date in
|
||||
editingEvent = nil
|
||||
editorDate = date
|
||||
showEditor = true
|
||||
},
|
||||
onCreateEvent: { date in editorContext = .create(date) },
|
||||
onShowMonth: { date in
|
||||
store.currentDate = date
|
||||
store.viewType = .month
|
||||
@@ -283,11 +285,7 @@ struct CalendarHostView: View {
|
||||
case .day:
|
||||
DayView(store: store,
|
||||
onEventTap: { selectedEvent = $0 },
|
||||
onCreateEvent: { date in
|
||||
editingEvent = nil
|
||||
editorDate = date
|
||||
showEditor = true
|
||||
})
|
||||
onCreateEvent: { date in editorContext = .create(date) })
|
||||
.simultaneousGesture(swipe)
|
||||
case .quarter:
|
||||
QuarterView(store: store, onEventTap: { selectedEvent = $0 })
|
||||
@@ -302,7 +300,7 @@ struct CalendarHostView: View {
|
||||
/// Standard solid FAB (flat mode)
|
||||
private var solidFAB: some View {
|
||||
Button {
|
||||
editingEvent = nil; editorDate = .now; showEditor = true
|
||||
editorContext = .create(.now)
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
@@ -320,7 +318,7 @@ struct CalendarHostView: View {
|
||||
private var glassFAB: some View {
|
||||
if #available(iOS 26, *) {
|
||||
Button {
|
||||
editingEvent = nil; editorDate = .now; showEditor = true
|
||||
editorContext = .create(.now)
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
@@ -338,8 +336,7 @@ struct CalendarHostView: View {
|
||||
// MARK: – Sheets modifier
|
||||
|
||||
private var calendarSheets: CalendarSheets {
|
||||
CalendarSheets(store: store, showEditor: $showEditor,
|
||||
editorDate: $editorDate, editingEvent: $editingEvent,
|
||||
CalendarSheets(store: store, editorContext: $editorContext,
|
||||
selectedEvent: $selectedEvent, showFilter: $showFilter,
|
||||
api: api,
|
||||
reload: { await onNavigate() },
|
||||
@@ -437,9 +434,7 @@ struct CalendarHostView: View {
|
||||
|
||||
private struct CalendarSheets: ViewModifier {
|
||||
let store: CalendarStore
|
||||
@Binding var showEditor: Bool
|
||||
@Binding var editorDate: Date
|
||||
@Binding var editingEvent: CalEvent?
|
||||
@Binding var editorContext: CalEditorContext?
|
||||
@Binding var selectedEvent: CalEvent?
|
||||
@Binding var showFilter: Bool
|
||||
let api: CalendarrAPI
|
||||
@@ -448,21 +443,23 @@ private struct CalendarSheets: ViewModifier {
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.sheet(isPresented: $showEditor) {
|
||||
// Use sheet(item:) so the editing event is captured atomically –
|
||||
// avoiding the race where sheet(isPresented:) evaluates its content
|
||||
// before the editingEvent state update propagates.
|
||||
.sheet(item: $editorContext) { ctx in
|
||||
let editingEv: CalEvent? = { if case .edit(let ev) = ctx { return ev }; return nil }()
|
||||
let date: Date = { if case .create(let d) = ctx { return d }; return .now }()
|
||||
EventEditorSheet(api: api, store: store,
|
||||
initialDate: editorDate, editingEvent: editingEvent) {
|
||||
// Create/edit changed server state → bust the cache so the
|
||||
// new/updated event appears without a manual sync.
|
||||
editingEvent = nil; await reloadForce()
|
||||
initialDate: date, editingEvent: editingEv) {
|
||||
editorContext = nil
|
||||
await reloadForce()
|
||||
}
|
||||
}
|
||||
.sheet(item: $selectedEvent) { ev in
|
||||
EventDetailSheet(event: ev, api: api, store: store) { updated in
|
||||
EventDetailSheet(event: ev, api: api, store: store) { updated, needsForce in
|
||||
selectedEvent = nil
|
||||
if let u = updated { editingEvent = u; showEditor = true }
|
||||
// Delete already removed the event from the cache optimistically;
|
||||
// a light cache refresh is enough here.
|
||||
await reload()
|
||||
if let u = updated { editorContext = .edit(u) }
|
||||
if needsForce { await reloadForce() } else { await reload() }
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showFilter) {
|
||||
|
||||
@@ -42,14 +42,10 @@ struct EventDetailSheet: View {
|
||||
}
|
||||
|
||||
private var canEdit: Bool {
|
||||
event.source == "local" || event.source == "caldav"
|
||||
event.source == "local" || event.source == "caldav" || event.source == "homeassistant"
|
||||
}
|
||||
|
||||
/// Home Assistant events can't be edited in-app (no editor support), but
|
||||
/// the server does support deleting them.
|
||||
private var canDelete: Bool {
|
||||
canEdit || event.source == "homeassistant"
|
||||
}
|
||||
private var canDelete: Bool { canEdit }
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
|
||||
@@ -5,7 +5,7 @@ struct EventEditorSheet: View {
|
||||
let store: CalendarStore
|
||||
let initialDate: Date
|
||||
let editingEvent: CalEvent?
|
||||
let copyFrom: CalEvent?
|
||||
var copyFrom: CalEvent? = nil
|
||||
let onSaved: () async -> Void
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@@ -133,16 +133,27 @@ struct EventEditorSheet: View {
|
||||
title = ev.title
|
||||
isAllDay = ev.isAllDay
|
||||
startDate = ev.startDate
|
||||
endDate = ev.endDate
|
||||
// All-day end dates are stored as exclusive (day after last); subtract 1 for the picker.
|
||||
endDate = ev.isAllDay
|
||||
? Calendar.current.date(byAdding: .day, value: -1, to: ev.endDate) ?? ev.endDate
|
||||
: ev.endDate
|
||||
location = ev.location
|
||||
notes = ev.notes
|
||||
color = ev.color ?? ""
|
||||
selectedCalendarId = ev.calendarId
|
||||
// HA events use "homeassistant-42" in CalEvent but "ha-42" in WritableCalendar
|
||||
if ev.source == "homeassistant" {
|
||||
let num = ev.calendarId.replacingOccurrences(of: "homeassistant-", with: "")
|
||||
selectedCalendarId = "ha-\(num)"
|
||||
} else {
|
||||
selectedCalendarId = ev.calendarId
|
||||
}
|
||||
} else if let ev = copyFrom {
|
||||
title = ev.title
|
||||
isAllDay = ev.isAllDay
|
||||
startDate = ev.startDate
|
||||
endDate = ev.endDate
|
||||
endDate = ev.isAllDay
|
||||
? Calendar.current.date(byAdding: .day, value: -1, to: ev.endDate) ?? ev.endDate
|
||||
: ev.endDate
|
||||
location = ev.location
|
||||
notes = ev.notes
|
||||
color = ev.color ?? ""
|
||||
@@ -168,10 +179,19 @@ struct EventEditorSheet: View {
|
||||
|
||||
do {
|
||||
if let ev = editingEvent {
|
||||
if ev.source == "local" {
|
||||
switch ev.source {
|
||||
case "local":
|
||||
try await api.updateLocalEvent(uid: ev.id, title: title, start: start, end: end,
|
||||
isAllDay: isAllDay, location: location, description: notes, color: colorVal)
|
||||
} else {
|
||||
case "homeassistant":
|
||||
// No update API exists – delete the old event and recreate with new data.
|
||||
let rawId = ev.calendarId.replacingOccurrences(of: "homeassistant-", with: "")
|
||||
let haCalId = Int(rawId) ?? 0
|
||||
try await api.deleteHAEvent(calendarId: haCalId, uid: ev.id)
|
||||
try await api.createHAEvent(calendarId: haCalId, title: title,
|
||||
start: start, end: end, isAllDay: isAllDay,
|
||||
location: location, description: notes)
|
||||
default: // caldav
|
||||
let calId = Int(ev.calendarId)
|
||||
try await api.updateCalDAVEvent(uid: ev.id, url: ev.url, calendarId: calId,
|
||||
title: title, start: start, end: end, isAllDay: isAllDay,
|
||||
|
||||
@@ -53,8 +53,7 @@ struct MonthView: View {
|
||||
headerRow
|
||||
Divider()
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
ForEach(weekStarts, id: \.self) { ws in
|
||||
LazyVStack(spacing: 0) { ForEach(weekStarts, id: \.self) { ws in
|
||||
WeekRow(weekStart: ws,
|
||||
store: store,
|
||||
dividerColor: Color(hex: dividerHex),
|
||||
@@ -72,6 +71,7 @@ struct MonthView: View {
|
||||
}
|
||||
.scrollTargetLayout()
|
||||
}
|
||||
.scrollIndicators(.hidden)
|
||||
.scrollPosition(id: $scrolledWeek, anchor: .top)
|
||||
.onAppear {
|
||||
if !didInitialScroll {
|
||||
|
||||
Reference in New Issue
Block a user