- 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>
116 lines
4.5 KiB
Swift
116 lines
4.5 KiB
Swift
import SwiftUI
|
|
|
|
struct MenuSheet: View {
|
|
let api: CalendarrAPI
|
|
@Environment(AppState.self) var appState
|
|
@Environment(\.dismiss) var dismiss
|
|
@AppStorage("appLanguage") private var appLang = "system"
|
|
@State private var isSyncing = false
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
List {
|
|
// User info header
|
|
Section {
|
|
HStack(spacing: 12) {
|
|
Circle()
|
|
.fill(Color.accentColor)
|
|
.frame(width: 44, height: 44)
|
|
.overlay {
|
|
Text(appState.username.prefix(1).uppercased())
|
|
.font(.title3.bold())
|
|
.foregroundStyle(.white)
|
|
}
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(appState.username).font(.headline)
|
|
Text(appState.serverURL
|
|
.replacingOccurrences(of: "https://", with: "")
|
|
.replacingOccurrences(of: "http://", with: ""))
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
if appState.isAdmin {
|
|
Spacer()
|
|
Text(L10n.t("menu.admin", appLang))
|
|
.font(.caption2.weight(.semibold))
|
|
.padding(.horizontal, 8).padding(.vertical, 3)
|
|
.background(Color.accentColor.opacity(0.15))
|
|
.foregroundStyle(Color.accentColor)
|
|
.clipShape(Capsule())
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
|
|
Section(L10n.t("menu.section.settings", appLang)) {
|
|
NavigationLink {
|
|
ProfileView(api: api)
|
|
} label: {
|
|
Label(L10n.t("menu.profile", appLang), systemImage: "person.circle")
|
|
}
|
|
|
|
NavigationLink {
|
|
SettingsView(api: api)
|
|
} label: {
|
|
Label(L10n.t("menu.appearance", appLang), systemImage: "paintpalette")
|
|
}
|
|
|
|
NavigationLink {
|
|
AccountsView(api: api)
|
|
} label: {
|
|
Label(L10n.t("menu.accounts", appLang), systemImage: "tray.2")
|
|
}
|
|
|
|
NavigationLink {
|
|
ServerView()
|
|
} label: {
|
|
Label(L10n.t("menu.server", appLang), systemImage: "server.rack")
|
|
}
|
|
}
|
|
|
|
Section(L10n.t("menu.sync.section", appLang)) {
|
|
Button {
|
|
Task { await syncNow() }
|
|
} label: {
|
|
HStack {
|
|
Label(L10n.t("menu.sync", appLang), systemImage: "arrow.triangle.2.circlepath")
|
|
Spacer()
|
|
if isSyncing { ProgressView() }
|
|
}
|
|
}
|
|
.disabled(isSyncing)
|
|
}
|
|
|
|
Section {
|
|
Button(role: .destructive) {
|
|
dismiss()
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
appState.logout()
|
|
}
|
|
} label: {
|
|
Label(L10n.t("menu.logout", appLang), systemImage: "rectangle.portrait.and.arrow.right")
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle(L10n.t("nav.menu", appLang))
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .primaryAction) {
|
|
Button(L10n.t("nav.done", appLang)) { dismiss() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Manual sync: pull appearance/behaviour settings from the server, then
|
|
/// ask the calendar host to re-fetch events (cache-busting).
|
|
private func syncNow() async {
|
|
isSyncing = true
|
|
await SettingsSync.pull(api: api)
|
|
NotificationCenter.default.post(name: .manualSyncRequested, object: nil)
|
|
isSyncing = false
|
|
dismiss()
|
|
}
|
|
}
|