diff --git a/assets/zh-Hans.lproj/Localizable.strings b/assets/zh-Hans.lproj/Localizable.strings index c9312af..57c7609 100644 Binary files a/assets/zh-Hans.lproj/Localizable.strings and b/assets/zh-Hans.lproj/Localizable.strings differ diff --git a/src/config/about.swift b/src/config/about.swift index e411706..74cf423 100644 --- a/src/config/about.swift +++ b/src/config/about.swift @@ -88,7 +88,7 @@ struct AboutView: View { .resizable() .frame(width: 80, height: 80) } - Text("Fcitx5 macOS") + Text(String("Fcitx5 macOS")) // no i18n by design .font(.title) Spacer().frame(height: gapSize) diff --git a/src/config/datamanager.swift b/src/config/datamanager.swift index fcd5294..b8b20e7 100644 --- a/src/config/datamanager.swift +++ b/src/config/datamanager.swift @@ -4,20 +4,34 @@ import UniformTypeIdentifiers let extractDir = cacheDir.appendingPathComponent("import") let extractPath = extractDir.localPath() +let composeDir = cacheDir.appendingPathComponent("export") private func extractZip(_ file: URL) -> Bool { // unzip is unfriendly with Chinese file names return exec("/usr/bin/ditto", ["-xk", file.localPath(), extractPath]) } +private func getTimeString() -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH_mm_ss'Z'" + dateFormatter.timeZone = TimeZone(abbreviation: "UTC") + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + return dateFormatter.string(from: Date()) +} + struct DataView: View { @State private var openPanel = NSOpenPanel() @AppStorage("ImportDataSelectedDirectory") var importDataSelectedDirectory: String? + @AppStorage("ExportDataSelectedDirectory") var exportDataSelectedDirectory: String? @State private var showImportF5a = false + @State private var showImportF5m = false @State private var showImportSquirrel = false @State private var showImportHamster = false @State private var showSquirrelError = false @State private var showInvalidZip = false + @State private var showRunning = false + @State private var showExportSuccess = false + @State private var showExportFailure = false private func importZip(_ binding: Binding, _ validator: @escaping () -> Bool) { // Keep a single openPanel to avoid confusion. @@ -45,6 +59,44 @@ struct DataView: View { } } + private func exportZip(_ name: String) -> Bool { + _ = removeFile(composeDir) + // Fake f5a structure. + for name in ["databases", "external", "recently_used", "shared_prefs"] { + mkdirP(composeDir.appendingPathComponent(name).localPath()) + } + let externalDir = composeDir.appendingPathComponent("external") + for operation in [ + { copyFile(configDir, externalDir.appendingPathComponent("config")) }, + { copyFile(localDir, externalDir.appendingPathComponent("data")) }, + { + writeUTF8( + composeDir.appendingPathComponent("metadata.json"), + """ + { + "packageName": "org.fcitx.fcitx5.android", + "versionCode": 0, + "versionName": "", + "exportTime": \(Int(Date().timeIntervalSince1970 * 1000)) + }\n + """) + }, + { + exec( + "/bin/zsh", + [ + "-c", + "cd \(quote(composeDir.localPath())) && /usr/bin/zip -r \(name) * -x \"*.DS_Store\"", + ]) + }, + ] { + if !operation() { + return false + } + } + return true + } + var body: some View { VStack { Text("Import data from …") @@ -75,6 +127,18 @@ struct DataView: View { ImportDataView().load(f5aItems) } + Button { + importZip( + $showImportF5m, + { + extractDir.appendingPathComponent("metadata.json").exists() + }) + } label: { + Text("Fcitx5 macOS").tooltip("fcitx5-macos_YYYY-MM-DD*.zip") + }.sheet(isPresented: $showImportF5m) { + ImportDataView().load(f5mItems) + } + Button { importZip( $showImportHamster, @@ -86,6 +150,46 @@ struct DataView: View { }.sheet(isPresented: $showImportHamster) { ImportDataView().load(hamsterItems) } + + Spacer().frame(height: gapSize) + + Text("Export data to …") + + Button { + showRunning = true + let name = "fcitx5-macos_\(getTimeString()).zip" + DispatchQueue.global().async { + let res = exportZip(name) + DispatchQueue.main.async { + showRunning = false + if res { + if openPanel.isVisible { + openPanel.cancel(nil) + openPanel = NSOpenPanel() + } + openPanel.allowsMultipleSelection = false + openPanel.canChooseDirectories = true + openPanel.canChooseFiles = false + if openPanel.runModal() == .OK { + if let url = openPanel.url { + if moveFile( + composeDir.appendingPathComponent(name), url.appendingPathComponent(name)) + { + showExportSuccess = true + } else { + showExportFailure = true + } + exportDataSelectedDirectory = url.localPath() + } + } + } else { + showExportFailure = true + } + } + } + } label: { + Text("Fcitx5 Android/macOS") + } }.padding() .toast(isPresenting: $showSquirrelError) { AlertToast( @@ -97,5 +201,20 @@ struct DataView: View { displayMode: .hud, type: .error(Color.red), title: NSLocalizedString("Invalid zip", comment: "")) } + .toast(isPresenting: $showExportSuccess) { + AlertToast( + displayMode: .hud, + type: .complete(Color.green), title: NSLocalizedString("Export succeeded", comment: "")) + } + .toast(isPresenting: $showExportFailure) { + AlertToast( + displayMode: .hud, type: .error(Color.red), + title: NSLocalizedString("Export failed", comment: "")) + } + .toast( + isPresenting: $showRunning + ) { + AlertToast(type: .loading) + } } } diff --git a/src/config/importdata.swift b/src/config/importdata.swift index 49f3662..1449492 100644 --- a/src/config/importdata.swift +++ b/src/config/importdata.swift @@ -199,6 +199,28 @@ let f5aItems = [ importableRimeUser(f5aRimeDir), ] +let f5mItems = [ + ImportableItem( + name: NSLocalizedString("Config", comment: ""), enabled: true, + exists: { + dataDir.appendingPathComponent("config").exists() + }, + doImport: { + mkdirP(configDir.localPath()) + return moveAndMerge(dataDir.appendingPathComponent("config"), configDir) + }), + ImportableItem( + name: NSLocalizedString("Data", comment: ""), enabled: true, + exists: { + dataDir.appendingPathComponent("data").exists() + }, + doImport: { + mkdirP(localDir.localPath()) + return moveAndMerge( + dataDir.appendingPathComponent("data"), localDir) + }), +] + class ImportDataVM: ObservableObject { @Published var importableItems = [ImportableItem]() @Published var items = [ImportableItem]()