From 682147c550ae1d6512b2fc5984993f395ff44e06 Mon Sep 17 00:00:00 2001 From: Fuwn Date: Mon, 3 Mar 2025 03:15:53 -0800 Subject: feat: Development commit --- Localizable.xcstrings | 12 +- Sora/Data/Settings/SettingsManager.swift | 8 ++ .../Settings/Section/SettingsExportView.swift | 78 ------------- .../Section/SettingsImportExportView.swift | 125 +++++++++++++++++++++ Sora/Views/Settings/SettingsView.swift | 4 +- 5 files changed, 144 insertions(+), 83 deletions(-) delete mode 100644 Sora/Views/Settings/Section/SettingsExportView.swift create mode 100644 Sora/Views/Settings/Section/SettingsImportExportView.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ee67e2a..8deab4f 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -96,9 +96,6 @@ }, "Enable \"Share Image\" Shortcut" : { - }, - "Export" : { - }, "Export Bookmarks" : { @@ -108,6 +105,15 @@ }, "Image Quality" : { + }, + "Import & Export" : { + + }, + "Import Bookmarks" : { + + }, + "Import Failed" : { + }, "Loading %@…" : { diff --git a/Sora/Data/Settings/SettingsManager.swift b/Sora/Data/Settings/SettingsManager.swift index 7d38d0f..0ad3973 100644 --- a/Sora/Data/Settings/SettingsManager.swift +++ b/Sora/Data/Settings/SettingsManager.swift @@ -129,6 +129,14 @@ class SettingsManager: ObservableObject { try JSONEncoder().encode(bookmarks) } + func importBookmarks(from data: Data) throws { + let importedBookmarks = try JSONDecoder().decode([SettingsBookmark].self, from: data) + let existingIDs = Set(bookmarks.map(\.id)) + let newBookmarks = importedBookmarks.filter { !existingIDs.contains($0.id) } + + bookmarks.append(contentsOf: newBookmarks) + } + // MARK: - Search History Management func removeSearchHistoryEntry(at offsets: IndexSet) { searchHistory.remove(atOffsets: offsets) diff --git a/Sora/Views/Settings/Section/SettingsExportView.swift b/Sora/Views/Settings/Section/SettingsExportView.swift deleted file mode 100644 index 2e9723e..0000000 --- a/Sora/Views/Settings/Section/SettingsExportView.swift +++ /dev/null @@ -1,78 +0,0 @@ -import SwiftUI -import UniformTypeIdentifiers - -struct SettingsExportView: View { - @EnvironmentObject private var settings: SettingsManager - @State private var isFileExporterPresented = false - @State private var exportError: Error? - - var body: some View { - Button("Export Bookmarks") { - exportBookmarksToFile() - } - #if os(macOS) - .frame(maxWidth: .infinity, alignment: .trailing) - .fileExporter( - isPresented: $isFileExporterPresented, - document: try? JSONFileDocument(settings.exportBookmarks()), - contentType: .json, - defaultFilename: "sora_bookmarks.json" - ) { result in - switch result { - case .success: - break - - case .failure(let error): - exportError = error - } - } - #endif - .alert( - "Export Failed", - isPresented: Binding( - get: { exportError != nil }, - set: { if !$0 { exportError = nil } } - ) - ) { - Button("OK", role: .cancel) { () } - } message: { - Text(exportError?.localizedDescription ?? "An unknown error occurred while exporting.") - } - } - - private func exportBookmarksToFile() { - do { - #if os(macOS) - _ = try settings.exportBookmarks() - isFileExporterPresented = true - - #elseif os(iOS) - let data = try settings.exportBookmarks() - let temporaryURL = FileManager.default.temporaryDirectory - .appendingPathComponent("sora_bookmarks.json") - - try data.write(to: temporaryURL) - - let activityController = UIActivityViewController( - activityItems: [temporaryURL], - applicationActivities: nil - ) - - if let windowScene = UIApplication.shared.connectedScenes - .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController - { - activityController.popoverPresentationController?.sourceView = rootViewController.view - - rootViewController.present(activityController, animated: true) - } - - activityController.completionWithItemsHandler = { _, _, _, _ in - try? FileManager.default.removeItem(at: temporaryURL) - } - #endif - } catch { - exportError = error - } - } -} diff --git a/Sora/Views/Settings/Section/SettingsImportExportView.swift b/Sora/Views/Settings/Section/SettingsImportExportView.swift new file mode 100644 index 0000000..983b175 --- /dev/null +++ b/Sora/Views/Settings/Section/SettingsImportExportView.swift @@ -0,0 +1,125 @@ +import SwiftUI +import UniformTypeIdentifiers + +struct SettingsImportExportView: View { + @EnvironmentObject private var settings: SettingsManager + @State private var isFileExporterPresented = false + @State private var isFileImporterPresented = false + @State private var exportError: Error? + @State private var importError: Error? + + var body: some View { + Group { + Button("Import Bookmarks") { + isFileImporterPresented = true + } + + Button("Export Bookmarks") { + exportBookmarksToFile() + } + } + #if os(macOS) + .frame(maxWidth: .infinity, alignment: .trailing) + .fileExporter( + isPresented: $isFileExporterPresented, + document: try? JSONFileDocument(settings.exportBookmarks()), + contentType: .json, + defaultFilename: "sora_bookmarks.json" + ) { result in + switch result { + case .success: + break + + case .failure(let error): + exportError = error + } + } + #endif + .fileImporter( + isPresented: $isFileImporterPresented, + allowedContentTypes: [.json], + allowsMultipleSelection: false + ) { result in + handleImportResult(result) + } + .alert( + "Export Failed", + isPresented: Binding( + get: { exportError != nil }, + set: { if !$0 { exportError = nil } } + ) + ) { + Button("OK", role: .cancel) { () } + } message: { + Text(exportError?.localizedDescription ?? "An unknown error occurred while exporting.") + } + .alert( + "Import Failed", + isPresented: Binding( + get: { importError != nil }, + set: { if !$0 { importError = nil } } + ) + ) { + Button("OK", role: .cancel) { () } + } message: { + Text(importError?.localizedDescription ?? "An unknown error occurred while importing.") + } + } + + private func exportBookmarksToFile() { + do { + #if os(macOS) + _ = try settings.exportBookmarks() + isFileExporterPresented = true + + #elseif os(iOS) + let data = try settings.exportBookmarks() + let temporaryURL = FileManager.default.temporaryDirectory + .appendingPathComponent("sora_bookmarks.json") + + try data.write(to: temporaryURL) + + let activityController = UIActivityViewController( + activityItems: [temporaryURL], + applicationActivities: nil + ) + + if let windowScene = UIApplication.shared.connectedScenes + .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController + { + activityController.popoverPresentationController?.sourceView = rootViewController.view + + rootViewController.present(activityController, animated: true) + } + + activityController.completionWithItemsHandler = { _, _, _, _ in + try? FileManager.default.removeItem(at: temporaryURL) + } + #endif + } catch { + exportError = error + } + } + + private func handleImportResult(_ result: Result<[URL], Error>) { + do { + guard let selectedFile = try result.get().first else { return } + guard selectedFile.startAccessingSecurityScopedResource() else { + throw ImportError.accessDenied + } + + defer { selectedFile.stopAccessingSecurityScopedResource() } + + let data = try Data(contentsOf: selectedFile) + + try settings.importBookmarks(from: data) + } catch { + importError = error + } + } + + private enum ImportError: Error { + case accessDenied + } +} diff --git a/Sora/Views/Settings/SettingsView.swift b/Sora/Views/Settings/SettingsView.swift index 82738c8..59271ad 100644 --- a/Sora/Views/Settings/SettingsView.swift +++ b/Sora/Views/Settings/SettingsView.swift @@ -22,8 +22,8 @@ struct SettingsView: View { SettingsSearchView() } - Section(header: Text("Export")) { - SettingsExportView() + Section(header: Text("Import & Export")) { + SettingsImportExportView() } Section(header: Text("Settings")) { -- cgit v1.2.3