Widget anpassung vorbereitung
This commit is contained in:
10
Calendarr iOS/Calendarr iOS.entitlements
Normal file
10
Calendarr iOS/Calendarr iOS.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.scarriffleservices.calendarr</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,6 +1,13 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
extension Notification.Name {
|
||||
/// Posted whenever the persistent "banished calendars" set is mutated from
|
||||
/// outside the active `CalendarStore` (e.g. by `AccountsView`). The store
|
||||
/// listens for this in `CalendarHostView` and refreshes its filter.
|
||||
static let banishedCalendarsChanged = Notification.Name("banishedCalendarsChanged")
|
||||
}
|
||||
|
||||
enum CalViewType: String, CaseIterable {
|
||||
case month, week, day, quarter, agenda
|
||||
|
||||
@@ -45,11 +52,112 @@ class CalendarStore {
|
||||
var weekStartsOnMonday = true
|
||||
var writableCalendars: [WritableCalendar] = []
|
||||
|
||||
/// Set of `"source:calendarId"` keys the user has chosen to hide from the
|
||||
/// calendar views. Persisted in UserDefaults as a JSON array. Events whose
|
||||
/// key matches one of these are filtered out before being rendered.
|
||||
var hiddenCalendarKeys: Set<String> = CalendarStore.loadHiddenKeys()
|
||||
|
||||
/// "Banished" calendars – like `hiddenCalendarKeys` but expressing a
|
||||
/// stronger user intent: the calendar should not even appear in the quick
|
||||
/// show/hide list. Re-activation happens in AccountsView.
|
||||
var banishedCalendarKeys: Set<String> = CalendarStore.loadBanishedKeys()
|
||||
|
||||
// Cache bookkeeping
|
||||
private var cachedStart: Date? = nil
|
||||
private var cachedEnd: Date? = nil
|
||||
private var allCachedEvents: [CalEvent] = []
|
||||
|
||||
// MARK: – Hidden-calendar persistence
|
||||
|
||||
private static let hiddenKeysDefaultsKey = "hiddenCalendarKeys"
|
||||
|
||||
private static func loadHiddenKeys() -> Set<String> {
|
||||
guard let raw = UserDefaults.standard.string(forKey: hiddenKeysDefaultsKey),
|
||||
let data = raw.data(using: .utf8),
|
||||
let arr = try? JSONDecoder().decode([String].self, from: data)
|
||||
else { return [] }
|
||||
return Set(arr)
|
||||
}
|
||||
|
||||
private func saveHiddenKeys() {
|
||||
let arr = Array(hiddenCalendarKeys)
|
||||
if let data = try? JSONEncoder().encode(arr),
|
||||
let s = String(data: data, encoding: .utf8) {
|
||||
UserDefaults.standard.set(s, forKey: Self.hiddenKeysDefaultsKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle visibility of a single calendar and immediately refresh the
|
||||
/// visible event list + widget snapshot.
|
||||
func setCalendarHidden(_ key: String, hidden: Bool) {
|
||||
if hidden { hiddenCalendarKeys.insert(key) } else { hiddenCalendarKeys.remove(key) }
|
||||
saveHiddenKeys()
|
||||
let (s, e) = rangeForCurrentView()
|
||||
refreshFromCache(start: s, end: e)
|
||||
publishWidgetSnapshot()
|
||||
}
|
||||
|
||||
/// Replace the entire set (used by the filter sheet's bulk show/hide).
|
||||
func setHiddenCalendars(_ keys: Set<String>) {
|
||||
hiddenCalendarKeys = keys
|
||||
saveHiddenKeys()
|
||||
let (s, e) = rangeForCurrentView()
|
||||
refreshFromCache(start: s, end: e)
|
||||
publishWidgetSnapshot()
|
||||
}
|
||||
|
||||
static func calendarKey(source: String, calendarId: String) -> String {
|
||||
"\(source):\(calendarId)"
|
||||
}
|
||||
|
||||
// MARK: – Banished-calendar persistence
|
||||
|
||||
private static let banishedKeysDefaultsKey = "banishedCalendarKeys"
|
||||
|
||||
static func loadBanishedKeys() -> Set<String> {
|
||||
guard let raw = UserDefaults.standard.string(forKey: banishedKeysDefaultsKey),
|
||||
let data = raw.data(using: .utf8),
|
||||
let arr = try? JSONDecoder().decode([String].self, from: data)
|
||||
else { return [] }
|
||||
return Set(arr)
|
||||
}
|
||||
|
||||
static func saveBanishedKeys(_ keys: Set<String>) {
|
||||
let arr = Array(keys)
|
||||
if let data = try? JSONEncoder().encode(arr),
|
||||
let s = String(data: data, encoding: .utf8) {
|
||||
UserDefaults.standard.set(s, forKey: banishedKeysDefaultsKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// Move a calendar to / out of the banished set. Also clears any quick
|
||||
/// hidden flag for that key – once banished, the dual state is redundant.
|
||||
/// Posts `.banishedCalendarsChanged` so other views in the navigation
|
||||
/// stack (e.g. AccountsView) stay in sync.
|
||||
func setCalendarBanished(_ key: String, banished: Bool) {
|
||||
if banished {
|
||||
banishedCalendarKeys.insert(key)
|
||||
hiddenCalendarKeys.remove(key)
|
||||
} else {
|
||||
banishedCalendarKeys.remove(key)
|
||||
}
|
||||
Self.saveBanishedKeys(banishedCalendarKeys)
|
||||
saveHiddenKeys()
|
||||
NotificationCenter.default.post(name: .banishedCalendarsChanged, object: nil)
|
||||
let (s, e) = rangeForCurrentView()
|
||||
refreshFromCache(start: s, end: e)
|
||||
publishWidgetSnapshot()
|
||||
}
|
||||
|
||||
/// Re-read the banished set from UserDefaults – called when an external
|
||||
/// view (AccountsView) mutated it. Refreshes visible events + widgets.
|
||||
func syncBanishedFromDefaults() {
|
||||
banishedCalendarKeys = Self.loadBanishedKeys()
|
||||
let (s, e) = rangeForCurrentView()
|
||||
refreshFromCache(start: s, end: e)
|
||||
publishWidgetSnapshot()
|
||||
}
|
||||
|
||||
var userCalendar: Calendar {
|
||||
var cal = Calendar.current
|
||||
cal.firstWeekday = weekStartsOnMonday ? 2 : 1
|
||||
@@ -67,7 +175,10 @@ class CalendarStore {
|
||||
/// Call this after navigation without hitting the network.
|
||||
func refreshFromCache(start: Date, end: Date) {
|
||||
events = allCachedEvents.filter { ev in
|
||||
ev.startDate < end && ev.endDate > start
|
||||
let key = Self.calendarKey(source: ev.source, calendarId: ev.calendarId)
|
||||
return ev.startDate < end && ev.endDate > start
|
||||
&& !hiddenCalendarKeys.contains(key)
|
||||
&& !banishedCalendarKeys.contains(key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +246,53 @@ class CalendarStore {
|
||||
cachedStart = rangeStart
|
||||
cachedEnd = rangeEnd
|
||||
}
|
||||
|
||||
publishWidgetSnapshot()
|
||||
}
|
||||
|
||||
/// Write a slim snapshot of the next ~6 weeks into the App-Group container
|
||||
/// so the widget extension can render without a network call. 42 days
|
||||
/// covers the worst-case month grid (6 rows × 7 cols) for the calendar
|
||||
/// widget. Also asks the system to refresh the widget timeline.
|
||||
private func publishWidgetSnapshot() {
|
||||
let cal = userCalendar
|
||||
let now = Date()
|
||||
// Include the week before today so widgets that show the current week
|
||||
// (e.g. "This Week", "Up Next + Calendar") have data for Monday–today.
|
||||
let from = cal.date(byAdding: .day, value: -7, to: cal.startOfDay(for: now)) ?? now
|
||||
let to = cal.date(byAdding: .day, value: 42, to: cal.startOfDay(for: now)) ?? from
|
||||
let visible = allCachedEvents
|
||||
.filter { ev in
|
||||
let key = Self.calendarKey(source: ev.source, calendarId: ev.calendarId)
|
||||
return ev.startDate < to && ev.endDate > from
|
||||
&& !hiddenCalendarKeys.contains(key)
|
||||
&& !banishedCalendarKeys.contains(key)
|
||||
}
|
||||
.sorted { $0.startDate < $1.startDate }
|
||||
.prefix(500)
|
||||
.map { ev in
|
||||
WidgetEvent(id: ev.id,
|
||||
title: ev.title,
|
||||
start: ev.startDate,
|
||||
end: ev.endDate,
|
||||
isAllDay: ev.isAllDay,
|
||||
colorHex: ev.effectiveColor,
|
||||
location: ev.location)
|
||||
}
|
||||
let defaults = UserDefaults.standard
|
||||
let snap = WidgetSnapshot(
|
||||
writtenAt: now,
|
||||
events: Array(visible),
|
||||
todayColorHex: defaults.string(forKey: "todayColor") ?? "#4285f4",
|
||||
textColorHex: defaults.string(forKey: "textColor") ?? "#FFFFFF",
|
||||
backgroundColorHex: defaults.string(forKey: "backgroundColor") ?? "#000000",
|
||||
lineColorHex: defaults.string(forKey: "lineColor") ?? "#3A3A3C",
|
||||
primaryColorHex: defaults.string(forKey: "primaryColor") ?? "#4285f4",
|
||||
accentColorHex: defaults.string(forKey: "accentColor") ?? "#ea4335",
|
||||
language: defaults.string(forKey: "appLanguage") ?? "system"
|
||||
)
|
||||
WidgetStore.write(snap)
|
||||
WidgetTimelineNotifier.reload()
|
||||
}
|
||||
|
||||
// MARK: – Writable calendars
|
||||
|
||||
@@ -234,6 +234,20 @@ private let strings: [String: [String: String]] = [
|
||||
"accounts.ha.header": "Home Assistant",
|
||||
"accounts.ha.empty": "Keine Home Assistant-Konten",
|
||||
"accounts.ha.add": "Home Assistant hinzufügen",
|
||||
"profile.admin_note": "Hinweis: Die Benutzerverwaltung – sowohl das Erstellen als auch das Löschen von Benutzerkonten – erfolgt ausschließlich durch den Administrator des Servers.",
|
||||
|
||||
// Kalender-Filter (Sidebar)
|
||||
"filter.title": "Kalender",
|
||||
"filter.loading": "Lade Kalender…",
|
||||
"filter.empty": "Keine Kalender vorhanden",
|
||||
"filter.show_all": "Alle anzeigen",
|
||||
"filter.hide_all": "Alle ausblenden",
|
||||
"filter.button": "Kalender ein-/ausblenden",
|
||||
"filter.banish": "Dauerhaft ausblenden",
|
||||
"filter.banished_footer": "Dauerhaft ausgeblendete Kalender erscheinen unter »Konten & Kalender« und können dort wieder eingeblendet werden.",
|
||||
"accounts.banished_header": "Ausgeblendete Kalender",
|
||||
"accounts.banished_unhide": "Wieder einblenden",
|
||||
"accounts.banished_unknown": "Unbekannter Kalender",
|
||||
|
||||
// CalDAV add sheet
|
||||
"caldav.section": "Konto-Details",
|
||||
@@ -474,6 +488,20 @@ private let strings: [String: [String: String]] = [
|
||||
"accounts.ha.header": "Home Assistant",
|
||||
"accounts.ha.empty": "No Home Assistant accounts",
|
||||
"accounts.ha.add": "Add Home Assistant",
|
||||
"profile.admin_note": "Note: User management — both the creation and deletion of user accounts — is handled exclusively by the server administrator.",
|
||||
|
||||
// Calendar filter (sidebar)
|
||||
"filter.title": "Calendars",
|
||||
"filter.loading": "Loading calendars…",
|
||||
"filter.empty": "No calendars available",
|
||||
"filter.show_all": "Show all",
|
||||
"filter.hide_all": "Hide all",
|
||||
"filter.button": "Show/hide calendars",
|
||||
"filter.banish": "Hide permanently",
|
||||
"filter.banished_footer": "Permanently hidden calendars appear under “Accounts & Calendars”, where you can show them again.",
|
||||
"accounts.banished_header": "Hidden calendars",
|
||||
"accounts.banished_unhide": "Show again",
|
||||
"accounts.banished_unknown": "Unknown calendar",
|
||||
|
||||
// CalDAV add sheet
|
||||
"caldav.section": "Account details",
|
||||
|
||||
@@ -14,6 +14,7 @@ struct AccountsView: View {
|
||||
@State private var showAddICal = false
|
||||
@State private var showAddHA = false
|
||||
@State private var errorAlert: String?
|
||||
@State private var banishedKeys: Set<String> = CalendarStore.loadBanishedKeys()
|
||||
|
||||
@AppStorage("appLanguage") private var appLang = "system"
|
||||
|
||||
@@ -24,6 +25,7 @@ struct AccountsView: View {
|
||||
ProgressView(L10n.t("accounts.loading", appLang))
|
||||
} else {
|
||||
List {
|
||||
if !banishedKeys.isEmpty { banishedSection }
|
||||
caldavSection
|
||||
localSection
|
||||
icalSection
|
||||
@@ -180,6 +182,69 @@ struct AccountsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var banishedSection: some View {
|
||||
Section {
|
||||
ForEach(Array(banishedKeys).sorted(), id: \.self) { key in
|
||||
let info = resolveBanished(key)
|
||||
HStack(spacing: 10) {
|
||||
Circle()
|
||||
.fill(Color(hex: info.colorHex))
|
||||
.frame(width: 12, height: 12)
|
||||
.opacity(0.5)
|
||||
Text(info.name)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
Button(L10n.t("accounts.banished_unhide", appLang)) {
|
||||
banishedKeys.remove(key)
|
||||
CalendarStore.saveBanishedKeys(banishedKeys)
|
||||
NotificationCenter.default.post(name: .banishedCalendarsChanged, object: nil)
|
||||
}
|
||||
.font(.callout)
|
||||
.foregroundStyle(Color.accentColor)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.t("accounts.banished_header", appLang))
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveBanished(_ key: String) -> (name: String, colorHex: String) {
|
||||
let parts = key.split(separator: ":", maxSplits: 1).map(String.init)
|
||||
guard parts.count == 2, let id = Int(parts[1]) else {
|
||||
return (L10n.t("accounts.banished_unknown", appLang), "#888888")
|
||||
}
|
||||
switch parts[0] {
|
||||
case "local":
|
||||
if let c = localCalendars.first(where: { $0.id == id }) {
|
||||
return (c.name, c.color)
|
||||
}
|
||||
case "caldav":
|
||||
for acc in caldavAccounts {
|
||||
if let c = acc.calendars?.first(where: { $0.id == id }) {
|
||||
return ("\(acc.name) – \(c.name)", c.color ?? acc.color)
|
||||
}
|
||||
}
|
||||
case "ical":
|
||||
if let s = icalSubs.first(where: { $0.id == id }) {
|
||||
return (s.name, s.color)
|
||||
}
|
||||
case "google":
|
||||
for acc in googleAccounts {
|
||||
if let c = acc.calendars?.first(where: { $0.id == id }) {
|
||||
return ("\(acc.email) – \(c.name)", c.color ?? "#4285f4")
|
||||
}
|
||||
}
|
||||
case "homeassistant":
|
||||
for acc in haAccounts {
|
||||
if let c = acc.calendars?.first(where: { $0.id == id }) {
|
||||
return ("\(acc.name) – \(c.name)", c.color ?? "#46bdc6")
|
||||
}
|
||||
}
|
||||
default: break
|
||||
}
|
||||
return (L10n.t("accounts.banished_unknown", appLang), "#888888")
|
||||
}
|
||||
|
||||
var haSection: some View {
|
||||
Section {
|
||||
if haAccounts.isEmpty {
|
||||
@@ -210,6 +275,7 @@ struct AccountsView: View {
|
||||
|
||||
private func load() async {
|
||||
isLoading = true
|
||||
banishedKeys = CalendarStore.loadBanishedKeys()
|
||||
async let c = (try? await api.getCalDAVAccounts()) ?? []
|
||||
async let l = (try? await api.getLocalCalendars()) ?? []
|
||||
async let i = (try? await api.getICalSubscriptions()) ?? []
|
||||
|
||||
203
Calendarr iOS/Views/CalendarFilterSheet.swift
Normal file
203
Calendarr iOS/Views/CalendarFilterSheet.swift
Normal file
@@ -0,0 +1,203 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Lets the user toggle which calendars contribute events to the displayed
|
||||
/// calendar views (and the home-screen widgets). Filtering is purely
|
||||
/// client-side: hidden keys live in UserDefaults via `CalendarStore`. No
|
||||
/// server roundtrip is required to toggle visibility.
|
||||
struct CalendarFilterSheet: View {
|
||||
let api: CalendarrAPI
|
||||
let store: CalendarStore
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@AppStorage("appLanguage") private var appLang = "system"
|
||||
|
||||
@State private var caldavAccounts: [CalDAVAccount] = []
|
||||
@State private var localCalendars: [LocalCalendar] = []
|
||||
@State private var icalSubs: [ICalSubscription] = []
|
||||
@State private var googleAccounts: [GoogleAccount] = []
|
||||
@State private var haAccounts: [HomeAssistantAccount] = []
|
||||
@State private var isLoading = true
|
||||
@State private var hidden: Set<String> = []
|
||||
@State private var banished: Set<String> = []
|
||||
/// All non-banished keys discovered during load — used by bulk show/hide.
|
||||
@State private var allKeys: Set<String> = []
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Group {
|
||||
if isLoading {
|
||||
ProgressView(L10n.t("filter.loading", appLang))
|
||||
} else if allKeys.isEmpty {
|
||||
Text(L10n.t("filter.empty", appLang))
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
List {
|
||||
let visibleLocals = localCalendars.filter {
|
||||
!banished.contains(CalendarStore.calendarKey(source: "local", calendarId: "\($0.id)"))
|
||||
}
|
||||
if !visibleLocals.isEmpty {
|
||||
Section(L10n.t("accounts.local.header", appLang)) {
|
||||
ForEach(visibleLocals) { cal in
|
||||
row(name: cal.name, colorHex: cal.color,
|
||||
key: CalendarStore.calendarKey(source: "local", calendarId: "\(cal.id)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
ForEach(caldavAccounts) { acc in
|
||||
let cals = (acc.calendars ?? []).filter {
|
||||
!banished.contains(CalendarStore.calendarKey(source: "caldav", calendarId: "\($0.id)"))
|
||||
}
|
||||
if !cals.isEmpty {
|
||||
Section(acc.name) {
|
||||
ForEach(cals) { cal in
|
||||
row(name: cal.name,
|
||||
colorHex: cal.color ?? acc.color,
|
||||
key: CalendarStore.calendarKey(source: "caldav", calendarId: "\(cal.id)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let visibleSubs = icalSubs.filter {
|
||||
!banished.contains(CalendarStore.calendarKey(source: "ical", calendarId: "\($0.id)"))
|
||||
}
|
||||
if !visibleSubs.isEmpty {
|
||||
Section(L10n.t("accounts.ical.header", appLang)) {
|
||||
ForEach(visibleSubs) { sub in
|
||||
row(name: sub.name, colorHex: sub.color,
|
||||
key: CalendarStore.calendarKey(source: "ical", calendarId: "\(sub.id)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
ForEach(googleAccounts) { acc in
|
||||
let cals = (acc.calendars ?? []).filter {
|
||||
!banished.contains(CalendarStore.calendarKey(source: "google", calendarId: "\($0.id)"))
|
||||
}
|
||||
if !cals.isEmpty {
|
||||
Section(acc.email) {
|
||||
ForEach(cals) { cal in
|
||||
row(name: cal.name,
|
||||
colorHex: cal.color ?? "#4285f4",
|
||||
key: CalendarStore.calendarKey(source: "google", calendarId: "\(cal.id)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ForEach(haAccounts) { acc in
|
||||
let cals = (acc.calendars ?? []).filter {
|
||||
!banished.contains(CalendarStore.calendarKey(source: "homeassistant", calendarId: "\($0.id)"))
|
||||
}
|
||||
if !cals.isEmpty {
|
||||
Section(acc.name) {
|
||||
ForEach(cals) { cal in
|
||||
row(name: cal.name,
|
||||
colorHex: cal.color ?? "#46bdc6",
|
||||
key: CalendarStore.calendarKey(source: "homeassistant", calendarId: "\(cal.id)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !banished.isEmpty {
|
||||
Section {
|
||||
Text(L10n.t("filter.banished_footer", appLang))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.t("filter.title", appLang))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Menu {
|
||||
Button(L10n.t("filter.show_all", appLang)) {
|
||||
hidden = []
|
||||
store.setHiddenCalendars(hidden)
|
||||
}
|
||||
Button(L10n.t("filter.hide_all", appLang)) {
|
||||
hidden = allKeys
|
||||
store.setHiddenCalendars(hidden)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
}
|
||||
.disabled(allKeys.isEmpty)
|
||||
}
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button(L10n.t("nav.done", appLang)) { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.task { await load() }
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func row(name: String, colorHex: String, key: String) -> some View {
|
||||
let isVisible = !hidden.contains(key)
|
||||
Button {
|
||||
if isVisible { hidden.insert(key) } else { hidden.remove(key) }
|
||||
store.setCalendarHidden(key, hidden: !isVisible)
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
Circle()
|
||||
.fill(Color(hex: colorHex))
|
||||
.frame(width: 14, height: 14)
|
||||
.opacity(isVisible ? 1.0 : 0.35)
|
||||
Text(name)
|
||||
.foregroundStyle(isVisible ? .primary : .secondary)
|
||||
.strikethrough(!isVisible, color: .secondary)
|
||||
Spacer()
|
||||
Image(systemName: isVisible ? "eye" : "eye.slash")
|
||||
.foregroundStyle(isVisible ? Color.accentColor : .secondary)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button(role: .destructive) {
|
||||
hidden.remove(key)
|
||||
banished.insert(key)
|
||||
store.setCalendarBanished(key, banished: true)
|
||||
} label: {
|
||||
Label(L10n.t("filter.banish", appLang), systemImage: "archivebox")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func load() async {
|
||||
isLoading = true
|
||||
hidden = store.hiddenCalendarKeys
|
||||
banished = store.banishedCalendarKeys
|
||||
async let c = (try? await api.getCalDAVAccounts()) ?? []
|
||||
async let l = (try? await api.getLocalCalendars()) ?? []
|
||||
async let i = (try? await api.getICalSubscriptions()) ?? []
|
||||
async let g = (try? await api.getGoogleAccounts()) ?? []
|
||||
async let h = (try? await api.getHomeAssistantAccounts()) ?? []
|
||||
(caldavAccounts, localCalendars, icalSubs, googleAccounts, haAccounts) = await (c, l, i, g, h)
|
||||
|
||||
var keys = Set<String>()
|
||||
for cal in localCalendars {
|
||||
keys.insert(CalendarStore.calendarKey(source: "local", calendarId: "\(cal.id)"))
|
||||
}
|
||||
for acc in caldavAccounts {
|
||||
for cal in acc.calendars ?? [] {
|
||||
keys.insert(CalendarStore.calendarKey(source: "caldav", calendarId: "\(cal.id)"))
|
||||
}
|
||||
}
|
||||
for sub in icalSubs {
|
||||
keys.insert(CalendarStore.calendarKey(source: "ical", calendarId: "\(sub.id)"))
|
||||
}
|
||||
for acc in googleAccounts {
|
||||
for cal in acc.calendars ?? [] {
|
||||
keys.insert(CalendarStore.calendarKey(source: "google", calendarId: "\(cal.id)"))
|
||||
}
|
||||
}
|
||||
for acc in haAccounts {
|
||||
for cal in acc.calendars ?? [] {
|
||||
keys.insert(CalendarStore.calendarKey(source: "homeassistant", calendarId: "\(cal.id)"))
|
||||
}
|
||||
}
|
||||
allKeys = keys
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ struct ProfileView: View {
|
||||
kontoSection(profile: profile)
|
||||
passwordSection
|
||||
twoFASection(profile: profile)
|
||||
adminNoteSection
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,6 +142,20 @@ struct ProfileView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var adminNoteSection: some View {
|
||||
Section {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.top, 1)
|
||||
Text(L10n.t("profile.admin_note", appLang))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
private func load() async {
|
||||
isLoading = true
|
||||
defer { isLoading = false }
|
||||
|
||||
@@ -16,6 +16,8 @@ struct SettingsView: View {
|
||||
@AppStorage("textColor") private var textHex = "#FFFFFF"
|
||||
@AppStorage("backgroundColor") private var bgHex = "#000000"
|
||||
@AppStorage("lineColor") private var lineHex = "#3A3A3C"
|
||||
@AppStorage("primaryColor") private var primaryHex = "#4285f4"
|
||||
@AppStorage("accentColor") private var accentHex = "#ea4335"
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
@@ -65,6 +67,16 @@ struct SettingsView: View {
|
||||
.animation(.easeInOut, value: showToast)
|
||||
}
|
||||
.task { await load() }
|
||||
// Live-update widgets the moment any appearance value changes, so the
|
||||
// user sees the new colours without having to wait for the next event
|
||||
// sync or save the settings.
|
||||
.onChange(of: primaryHex) { _, _ in WidgetStore.republishAppearanceOnly() }
|
||||
.onChange(of: accentHex) { _, _ in WidgetStore.republishAppearanceOnly() }
|
||||
.onChange(of: todayHex) { _, _ in WidgetStore.republishAppearanceOnly() }
|
||||
.onChange(of: textHex) { _, _ in WidgetStore.republishAppearanceOnly() }
|
||||
.onChange(of: bgHex) { _, _ in WidgetStore.republishAppearanceOnly() }
|
||||
.onChange(of: lineHex) { _, _ in WidgetStore.republishAppearanceOnly() }
|
||||
.onChange(of: appLang) { _, _ in WidgetStore.republishAppearanceOnly() }
|
||||
}
|
||||
|
||||
// MARK: – Liquid Glass
|
||||
@@ -143,8 +155,8 @@ struct SettingsView: View {
|
||||
|
||||
var farbenSection: some View {
|
||||
Section(L10n.t("settings.colors", appLang)) {
|
||||
ColorPickerRow(label: L10n.t("settings.color.primary", appLang), hex: $settings.primaryColor)
|
||||
ColorPickerRow(label: L10n.t("settings.color.accent", appLang), hex: $settings.accentColor)
|
||||
ColorPickerRow(label: L10n.t("settings.color.primary", appLang), hex: $primaryHex)
|
||||
ColorPickerRow(label: L10n.t("settings.color.accent", appLang), hex: $accentHex)
|
||||
ColorPickerRow(label: L10n.t("settings.color.today", appLang), hex: $todayHex)
|
||||
ColorPickerRow(label: L10n.t("settings.color.text", appLang), hex: $textHex)
|
||||
ColorPickerRow(label: L10n.t("settings.color.background", appLang), hex: $bgHex)
|
||||
@@ -260,6 +272,8 @@ struct SettingsView: View {
|
||||
textHex = s.textColor
|
||||
bgHex = s.backgroundColor
|
||||
lineHex = s.lineColor
|
||||
primaryHex = s.primaryColor
|
||||
accentHex = s.accentColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,6 +287,8 @@ struct SettingsView: View {
|
||||
settings.textColor = textHex
|
||||
settings.backgroundColor = bgHex
|
||||
settings.lineColor = lineHex
|
||||
settings.primaryColor = primaryHex
|
||||
settings.accentColor = accentHex
|
||||
do {
|
||||
try await api.updateSettings(settings)
|
||||
showNotice(L10n.t("settings.saved", appLang))
|
||||
|
||||
Reference in New Issue
Block a user