Widget anpassung vorbereitung

This commit is contained in:
Scarriffle
2026-05-25 11:53:02 +02:00
parent d1004a9111
commit 6c506770ba
21 changed files with 1838 additions and 3 deletions

View File

@@ -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()) ?? []

View 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
}
}

View File

@@ -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 }

View File

@@ -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))