Initial Commit
This commit is contained in:
489
Calendarr iOS/Views/AccountsView.swift
Normal file
489
Calendarr iOS/Views/AccountsView.swift
Normal file
@@ -0,0 +1,489 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user