Files
Calendarr-IOS/Calendarr iOS/Views/AccountsView.swift
2026-05-17 08:32:34 +02:00

490 lines
17 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
struct AccountsView: View {
let api: CalendarrAPI
@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 showAddCalDAV = false
@State private var showAddLocal = false
@State private var showAddICal = false
@State private var showAddHA = false
@State private var errorAlert: String?
var body: some View {
NavigationStack {
Group {
if isLoading {
ProgressView("Lade Konten…")
} else {
List {
caldavSection
localSection
icalSection
googleSection
haSection
}
}
}
.navigationTitle("Konten")
.navigationBarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Menu {
Button("CalDAV-Konto") { showAddCalDAV = true }
Button("Lokaler Kalender") { showAddLocal = true }
Button("iCal-URL abonnieren") { showAddICal = true }
Button("Home Assistant") { showAddHA = true }
} label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showAddCalDAV) {
AddCalDAVSheet(api: api) { await load() }
}
.sheet(isPresented: $showAddLocal) {
AddLocalCalSheet(api: api) { await load() }
}
.sheet(isPresented: $showAddICal) {
AddICalSheet(api: api) { await load() }
}
.sheet(isPresented: $showAddHA) {
AddHASheet(api: api) { await load() }
}
.alert("Fehler", isPresented: .constant(errorAlert != nil), actions: {
Button("OK") { errorAlert = nil }
}, message: {
Text(errorAlert ?? "")
})
}
.task { await load() }
}
// MARK: Sections
var caldavSection: some View {
Section {
if caldavAccounts.isEmpty {
Text("Keine CalDAV-Konten")
.foregroundStyle(.secondary)
} else {
ForEach(caldavAccounts) { acc in
HStack {
Circle()
.fill(Color(hex: acc.color))
.frame(width: 12, height: 12)
VStack(alignment: .leading, spacing: 2) {
Text(acc.name).font(.body)
Text(acc.url)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
}
.onDelete { offsets in
Task { await deleteCalDAV(offsets: offsets) }
}
}
Button("CalDAV hinzufügen") { showAddCalDAV = true }
.foregroundStyle(Color.accentColor)
} header: {
Text("CalDAV-Konten")
}
}
var localSection: some View {
Section {
if localCalendars.isEmpty {
Text("Keine lokalen Kalender")
.foregroundStyle(.secondary)
} else {
ForEach(localCalendars) { cal in
HStack {
Circle()
.fill(Color(hex: cal.color))
.frame(width: 12, height: 12)
Text(cal.name)
}
}
.onDelete { offsets in
Task { await deleteLocal(offsets: offsets) }
}
}
Button("Lokalen Kalender erstellen") { showAddLocal = true }
.foregroundStyle(Color.accentColor)
} header: {
Text("Lokale Kalender")
}
}
var icalSection: some View {
Section {
if icalSubs.isEmpty {
Text("Keine Abonnements")
.foregroundStyle(.secondary)
} else {
ForEach(icalSubs) { sub in
HStack {
Circle()
.fill(Color(hex: sub.color))
.frame(width: 12, height: 12)
VStack(alignment: .leading, spacing: 2) {
Text(sub.name).font(.body)
Text("Alle \(sub.refreshMinutes) Min.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
.onDelete { offsets in
Task { await deleteICal(offsets: offsets) }
}
}
Button("iCal-URL abonnieren") { showAddICal = true }
.foregroundStyle(Color.accentColor)
} header: {
Text("iCal-Abonnements")
}
}
var googleSection: some View {
Section {
if googleAccounts.isEmpty {
Text("Keine Google-Konten")
.foregroundStyle(.secondary)
} else {
ForEach(googleAccounts) { acc in
HStack {
Image(systemName: "g.circle.fill")
.foregroundStyle(.red)
Text(acc.email)
}
}
.onDelete { offsets in
Task { await deleteGoogle(offsets: offsets) }
}
}
Text("Google-Konten werden über den Browser verknüpft")
.font(.caption)
.foregroundStyle(.secondary)
} header: {
Text("Google-Konten")
}
}
var haSection: some View {
Section {
if haAccounts.isEmpty {
Text("Keine Home Assistant-Konten")
.foregroundStyle(.secondary)
} else {
ForEach(haAccounts) { acc in
VStack(alignment: .leading, spacing: 2) {
Text(acc.name).font(.body)
Text(acc.url)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
.onDelete { offsets in
Task { await deleteHA(offsets: offsets) }
}
}
Button("Home Assistant hinzufügen") { showAddHA = true }
.foregroundStyle(Color.accentColor)
} header: {
Text("Home Assistant")
}
}
// MARK: Actions
private func load() async {
isLoading = true
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)
isLoading = false
}
private func deleteCalDAV(offsets: IndexSet) async {
for i in offsets {
try? await api.deleteCalDAVAccount(id: caldavAccounts[i].id)
}
await load()
}
private func deleteLocal(offsets: IndexSet) async {
for i in offsets {
try? await api.deleteLocalCalendar(id: localCalendars[i].id)
}
await load()
}
private func deleteICal(offsets: IndexSet) async {
for i in offsets {
try? await api.deleteICalSubscription(id: icalSubs[i].id)
}
await load()
}
private func deleteGoogle(offsets: IndexSet) async {
for i in offsets {
try? await api.deleteGoogleAccount(id: googleAccounts[i].id)
}
await load()
}
private func deleteHA(offsets: IndexSet) async {
for i in offsets {
try? await api.deleteHomeAssistantAccount(id: haAccounts[i].id)
}
await load()
}
}
// MARK: Add Sheets
struct AddCalDAVSheet: View {
let api: CalendarrAPI
let onDone: () async -> Void
@Environment(\.dismiss) var dismiss
@State private var name = ""
@State private var url = ""
@State private var username = ""
@State private var password = ""
@State private var color = Color(hex: "#4285f4")
@State private var isLoading = false
@State private var error = ""
var body: some View {
NavigationStack {
Form {
Section("Konto-Details") {
TextField("Anzeigename", text: $name)
TextField("CalDAV-URL", text: $url)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.URL)
TextField("Benutzername", text: $username)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
SecureField("Passwort", text: $password)
}
Section("Farbe") {
ColorPicker("Farbe", selection: $color, supportsOpacity: false)
}
if !error.isEmpty {
Section {
Text(error).foregroundStyle(.red)
}
}
}
.navigationTitle("CalDAV-Konto")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Abbrechen") { dismiss() }
}
ToolbarItem(placement: .primaryAction) {
Button("Verbinden") {
Task { await save() }
}
.bold()
.disabled(name.isEmpty || url.isEmpty || username.isEmpty || password.isEmpty || isLoading)
}
}
}
}
private func save() async {
isLoading = true
error = ""
do {
_ = try await api.addCalDAVAccount(name: name, url: url, username: username, password: password, color: color.toHex())
await onDone()
dismiss()
} catch {
self.error = error.localizedDescription
}
isLoading = false
}
}
struct AddLocalCalSheet: View {
let api: CalendarrAPI
let onDone: () async -> Void
@Environment(\.dismiss) var dismiss
@State private var name = ""
@State private var color = Color(hex: "#34a853")
@State private var isLoading = false
@State private var error = ""
var body: some View {
NavigationStack {
Form {
Section {
TextField("Name", text: $name)
ColorPicker("Farbe", selection: $color, supportsOpacity: false)
}
if !error.isEmpty {
Section { Text(error).foregroundStyle(.red) }
}
}
.navigationTitle("Lokaler Kalender")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) { Button("Abbrechen") { dismiss() } }
ToolbarItem(placement: .primaryAction) {
Button("Erstellen") {
Task { await save() }
}
.bold()
.disabled(name.isEmpty || isLoading)
}
}
}
}
private func save() async {
isLoading = true
do {
_ = try await api.addLocalCalendar(name: name, color: color.toHex())
await onDone()
dismiss()
} catch { self.error = error.localizedDescription }
isLoading = false
}
}
struct AddICalSheet: View {
let api: CalendarrAPI
let onDone: () async -> Void
@Environment(\.dismiss) var dismiss
@State private var name = ""
@State private var url = ""
@State private var color = Color(hex: "#46bdc6")
@State private var refreshMinutes = 60
@State private var isLoading = false
@State private var error = ""
let refreshOptions = [(15, "Alle 15 Min."), (30, "Alle 30 Min."), (60, "Stündlich"), (360, "Alle 6 Std."), (1440, "Täglich")]
var body: some View {
NavigationStack {
Form {
Section("Abonnement") {
TextField("Name", text: $name)
TextField("iCal-URL", text: $url)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.URL)
ColorPicker("Farbe", selection: $color, supportsOpacity: false)
}
Section("Aktualisierung") {
Picker("Intervall", selection: $refreshMinutes) {
ForEach(refreshOptions, id: \.0) { opt in
Text(opt.1).tag(opt.0)
}
}
}
if !error.isEmpty {
Section { Text(error).foregroundStyle(.red) }
}
}
.navigationTitle("iCal abonnieren")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) { Button("Abbrechen") { dismiss() } }
ToolbarItem(placement: .primaryAction) {
Button("Abonnieren") {
Task { await save() }
}
.bold()
.disabled(name.isEmpty || url.isEmpty || isLoading)
}
}
}
}
private func save() async {
isLoading = true
do {
_ = try await api.addICalSubscription(name: name, url: url, color: color.toHex(), refreshMinutes: refreshMinutes)
await onDone()
dismiss()
} catch { self.error = error.localizedDescription }
isLoading = false
}
}
struct AddHASheet: View {
let api: CalendarrAPI
let onDone: () async -> Void
@Environment(\.dismiss) var dismiss
@State private var name = ""
@State private var url = ""
@State private var token = ""
@State private var isLoading = false
@State private var error = ""
var body: some View {
NavigationStack {
Form {
Section("Home Assistant") {
TextField("Anzeigename", text: $name)
TextField("URL (z.B. http://homeassistant.local:8123)", text: $url)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.URL)
}
Section("Authentifizierung") {
SecureField("Long-Lived Access Token", text: $token)
Text("Token erstellen unter: Profil → Sicherheit → Long-Lived Access Tokens")
.font(.caption)
.foregroundStyle(.secondary)
}
if !error.isEmpty {
Section { Text(error).foregroundStyle(.red) }
}
}
.navigationTitle("Home Assistant")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) { Button("Abbrechen") { dismiss() } }
ToolbarItem(placement: .primaryAction) {
Button("Verbinden") {
Task { await save() }
}
.bold()
.disabled(name.isEmpty || url.isEmpty || token.isEmpty || isLoading)
}
}
}
}
private func save() async {
isLoading = true
do {
_ = try await api.addHomeAssistantAccount(name: name, url: url, token: token)
await onDone()
dismiss()
} catch { self.error = error.localizedDescription }
isLoading = false
}
}