Settings sync, calendar visibility sync, event refresh & week-view fixes

- Add two-way settings sync (SettingsSync) with toggle, app-start/foreground/
  10-min pull and debounced push; server wins; view/week-start/dim-past always
  sync. Wire previously-ignored settings (hour height, contrasts, week start,
  default view, dim past) into the actual UI.
- Make AppSettings decoding resilient (decodeIfPresent) so getSettings no longer
  fails on iOS-only fields the server omits; keep text/bg/line colors local-only;
  month divider/label colors now sync.
- Auto-refresh after create/edit (cache-busting) and optimistic removal on
  delete; switch delete confirm to a centered alert. Add HA event deletion.
- Calendar visibility: fix inverted hide/show toggle; normalize calendar keys so
  local filtering works for all sources; sync banish with server sidebar_hidden
  (CalDAV/Google/HA), refetch on un-banish.
- Manual "sync with server" button in the menu.
- Upcoming widget shows next 5 days (renamed).
- Week/Day view: route multi-day timed events to the all-day strip so they no
  longer render as a full-height block.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-05-27 20:44:14 +02:00
parent 07a9e9eb7f
commit 4125bfc728
16 changed files with 616 additions and 156 deletions

View File

@@ -1,10 +1,54 @@
import SwiftUI
// Shared constants used by WeekView, DayView, EventEditorSheet
let hourHeight: CGFloat = 60
let timeColumnWidth: CGFloat = 44
let hours = Array(0..<24)
/// Live hour-row height, driven by the synced `hourHeight` setting.
/// Falls back to 60 when unset (fresh install / value 0). Views that lay out
/// against this also observe `@AppStorage("hourHeight")` so their body
/// re-renders when it changes.
var hourHeight: CGFloat {
let v = UserDefaults.standard.integer(forKey: "hourHeight")
return v > 0 ? CGFloat(v) : 60
}
/// Opacity for secondary text (weekday headers, time labels, "+N"/"KW"),
/// mapped from the 14 `textContrast` level. Level 3 the previous hard-coded
/// look so existing installs are visually unchanged.
func secondaryTextOpacity(_ level: Int) -> Double {
switch level {
case 1: return 0.4
case 2: return 0.55
case 4: return 1.0
default: return 0.75
}
}
/// Opacity for grid lines / separators, mapped from the 14 `lineContrast`
/// level. Level 3 the previous hard-coded ~0.4 look.
func gridLineOpacity(_ level: Int) -> Double {
switch level {
case 1: return 0.15
case 2: return 0.3
case 4: return 0.8
default: return 0.5
}
}
/// A timed (non-all-day) event that crosses a day boundary. Such events must
/// NOT be placed in the hourly grid their height would be `duration ×
/// hourHeight`, i.e. taller than the whole day, rendering as a giant block
/// (and, sharing one id across days, only drawing on the first day). They are
/// shown in the all-day strip instead, like all-day events.
func eventSpansMultipleDays(_ ev: CalEvent) -> Bool {
guard !ev.isAllDay, ev.endDate > ev.startDate else { return false }
let cal = Calendar.current
// End is exclusive: an event ending exactly at midnight is still single-day.
let lastInstant = ev.endDate.addingTimeInterval(-1)
return !cal.isDate(ev.startDate, inSameDayAs: lastInstant)
}
// Position helpers
func eventTop(_ ev: CalEvent) -> CGFloat {
let cal = Calendar.current
@@ -21,6 +65,9 @@ func eventHeight(_ ev: CalEvent) -> CGFloat {
// Shared event block used in WeekView and DayView
struct EventBlock: View {
let event: CalEvent
@AppStorage("dimPastEvents") private var dimPast = false
private var isPast: Bool { event.endDate < .now }
var body: some View {
RoundedRectangle(cornerRadius: 4)
@@ -41,5 +88,6 @@ struct EventBlock: View {
.padding(4)
}
.padding(.horizontal, 1)
.opacity(dimPast && isPast ? 0.5 : 1.0)
}
}