iCloud Backup, better chatview exp. bugfixes++
This commit is contained in:
@@ -112,7 +112,8 @@ private let helpCategories: [CommandCategory] = [
|
||||
command: "/save <name>",
|
||||
brief: "Save current conversation",
|
||||
detail: "Saves all messages in the current session under the given name. You can load it later to continue the conversation.",
|
||||
examples: ["/save my-project-chat", "/save debug session"]
|
||||
examples: ["/save my-project-chat", "/save debug session"],
|
||||
shortcut: "⌘S"
|
||||
),
|
||||
CommandDetail(
|
||||
command: "/load",
|
||||
@@ -191,7 +192,8 @@ private let helpCategories: [CommandCategory] = [
|
||||
command: "/stats",
|
||||
brief: "Show session statistics",
|
||||
detail: "Displays statistics for the current session: message count, total tokens used, estimated cost, and session duration.",
|
||||
examples: ["/stats"]
|
||||
examples: ["/stats"],
|
||||
shortcut: "⌘⇧S"
|
||||
),
|
||||
]),
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user