iCloud Backup, better chatview exp. bugfixes++

This commit is contained in:
2026-02-27 14:05:11 +01:00
parent 3997f3feee
commit e9d0ad3c66
11 changed files with 634 additions and 30 deletions

View File

@@ -65,6 +65,14 @@ struct SettingsView: View {
@State private var isTestingPaperless = false
@State private var paperlessTestResult: String?
// Backup state
private let backupService = BackupService.shared
@State private var isExporting = false
@State private var isImporting = false
@State private var backupMessage: String?
@State private var backupMessageIsError = false
@State private var showRestoreFilePicker = false
// Email handler state
@State private var showEmailLog = false
@State private var showEmailModelSelector = false
@@ -135,6 +143,7 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
tabButton(4, icon: "arrow.triangle.2.circlepath", label: "Sync")
tabButton(5, icon: "envelope", label: "Email")
tabButton(8, icon: "doc.text", label: "Paperless")
tabButton(9, icon: "icloud.and.arrow.up", label: "Backup")
}
.padding(.horizontal, 16)
.padding(.bottom, 12)
@@ -162,6 +171,8 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
agentSkillsTab
case 8:
paperlessTab
case 9:
backupTab
default:
generalTab
}
@@ -171,10 +182,24 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
}
}
.frame(minWidth: 740, idealWidth: 820, minHeight: 620, idealHeight: 760)
.frame(minWidth: 860, idealWidth: 940, minHeight: 620, idealHeight: 760)
.sheet(isPresented: $showEmailLog) {
EmailLogView()
}
.fileImporter(
isPresented: $showRestoreFilePicker,
allowedContentTypes: [.json],
allowsMultipleSelection: false
) { result in
switch result {
case .success(let urls):
guard let url = urls.first else { return }
Task { await performRestore(from: url) }
case .failure(let error):
backupMessage = "Could not open file: \(error.localizedDescription)"
backupMessageIsError = true
}
}
}
// MARK: - General Tab
@@ -1823,6 +1848,184 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
}
}
// MARK: - Backup Tab
@ViewBuilder
private var backupTab: some View {
VStack(alignment: .leading, spacing: 20) {
// Warning notice
VStack(alignment: .leading, spacing: 6) {
sectionHeader("iCloud Drive Backup")
HStack(alignment: .top, spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.orange)
.font(.system(size: 14))
Text("API keys and credentials are **not** included in the backup. You will need to re-enter them after restoring on a new machine.")
.font(.system(size: 13))
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal, 4)
.padding(.top, 2)
}
// Status
VStack(alignment: .leading, spacing: 6) {
sectionHeader("Status")
formSection {
row("iCloud Drive") {
HStack(spacing: 6) {
Circle()
.fill(backupService.iCloudAvailable ? Color.green : Color.orange)
.frame(width: 8, height: 8)
Text(backupService.iCloudAvailable ? "Available" : "Not available — using Downloads")
.font(.system(size: 13))
.foregroundStyle(.secondary)
}
}
rowDivider()
row("Last Backup") {
if let date = backupService.lastBackupDate {
Text(formatBackupDate(date))
.font(.system(size: 13))
.foregroundStyle(.secondary)
} else {
Text("Never")
.font(.system(size: 13))
.foregroundStyle(.secondary)
}
}
}
}
// Actions
VStack(alignment: .leading, spacing: 6) {
sectionHeader("Actions")
formSection {
HStack(spacing: 12) {
Button(action: { Task { await performBackup() } }) {
HStack(spacing: 6) {
if isExporting {
ProgressView().scaleEffect(0.7).frame(width: 14, height: 14)
} else {
Image(systemName: "icloud.and.arrow.up")
}
Text("Back Up Now")
}
}
.disabled(isExporting || isImporting)
Button(action: { showRestoreFilePicker = true }) {
HStack(spacing: 6) {
if isImporting {
ProgressView().scaleEffect(0.7).frame(width: 14, height: 14)
} else {
Image(systemName: "icloud.and.arrow.down")
}
Text("Restore from File…")
}
}
.disabled(isExporting || isImporting)
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
if let msg = backupMessage {
rowDivider()
HStack(spacing: 6) {
Image(systemName: backupMessageIsError ? "xmark.circle.fill" : "checkmark.circle.fill")
.foregroundStyle(backupMessageIsError ? .red : .green)
Text(msg)
.font(.system(size: 13))
.foregroundStyle(backupMessageIsError ? .red : .primary)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
}
}
}
// Backup location
if let url = backupService.lastBackupURL {
VStack(alignment: .leading, spacing: 4) {
Text("Backup location:")
.font(.system(size: 13, weight: .medium))
Text(url.path.replacingOccurrences(
of: FileManager.default.homeDirectoryForCurrentUser.path,
with: "~"))
.font(.system(size: 12))
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
.padding(.horizontal, 4)
}
}
.onAppear {
backupService.checkForExistingBackup()
}
}
private func performBackup() async {
await MainActor.run {
isExporting = true
backupMessage = nil
}
do {
let url = try await backupService.exportSettings()
let shortPath = url.path.replacingOccurrences(
of: FileManager.default.homeDirectoryForCurrentUser.path,
with: "~")
await MainActor.run {
backupMessage = "Backup saved to \(shortPath)"
backupMessageIsError = false
isExporting = false
}
} catch {
await MainActor.run {
backupMessage = error.localizedDescription
backupMessageIsError = true
isExporting = false
}
}
}
private func performRestore(from url: URL) async {
await MainActor.run {
isImporting = true
backupMessage = nil
}
do {
_ = url.startAccessingSecurityScopedResource()
defer { url.stopAccessingSecurityScopedResource() }
try await backupService.importSettings(from: url)
await MainActor.run {
backupMessage = "Settings restored. Re-enter your API keys to resume using oAI."
backupMessageIsError = false
isImporting = false
}
} catch {
await MainActor.run {
backupMessage = error.localizedDescription
backupMessageIsError = true
isImporting = false
}
}
}
private func formatBackupDate(_ date: Date) -> String {
let cal = Calendar.current
if cal.isDateInToday(date) {
let tf = DateFormatter()
tf.dateFormat = "HH:mm"
return "Today \(tf.string(from: date))"
}
let df = DateFormatter()
df.dateFormat = "dd.MM.yyyy HH:mm"
return df.string(from: date)
}
// MARK: - Tab Navigation
private func tabButton(_ tag: Int, icon: String, label: String) -> some View {
@@ -1856,6 +2059,7 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
case 6: return "Shortcuts"
case 7: return "Skills"
case 8: return "Paperless"
case 9: return "Backup"
default: return "Settings"
}
}