From f385f4b27750ac3dcf7906b52686ee7084a5d492 Mon Sep 17 00:00:00 2001 From: hstyi Date: Fri, 24 Jan 2025 16:45:36 +0800 Subject: [PATCH] feat: support import (#127) --- .../kotlin/app/termora/SettingsOptionsPane.kt | 147 ++++++++++++++++-- .../kotlin/app/termora/native/FileChooser.kt | 4 + src/main/resources/i18n/messages.properties | 5 +- .../resources/i18n/messages_zh_CN.properties | 3 +- .../resources/i18n/messages_zh_TW.properties | 3 +- 5 files changed, 148 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/app/termora/SettingsOptionsPane.kt b/src/main/kotlin/app/termora/SettingsOptionsPane.kt index 606ef3b..b7a1128 100644 --- a/src/main/kotlin/app/termora/SettingsOptionsPane.kt +++ b/src/main/kotlin/app/termora/SettingsOptionsPane.kt @@ -4,9 +4,14 @@ import app.termora.AES.encodeBase64String import app.termora.Application.ohMyJson import app.termora.actions.AnAction import app.termora.actions.AnActionEvent +import app.termora.highlight.KeywordHighlight import app.termora.highlight.KeywordHighlightManager +import app.termora.keymap.Keymap +import app.termora.keymap.KeymapManager import app.termora.keymap.KeymapPanel import app.termora.keymgr.KeyManager +import app.termora.keymgr.OhKeyPair +import app.termora.macro.Macro import app.termora.macro.MacroManager import app.termora.native.FileChooser import app.termora.sync.SyncConfig @@ -28,12 +33,11 @@ import com.sun.jna.LastErrorException import kotlinx.coroutines.* import kotlinx.coroutines.swing.Swing import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.encodeToJsonElement -import kotlinx.serialization.json.put +import kotlinx.serialization.json.* import org.apache.commons.io.IOUtils import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.SystemUtils +import org.apache.commons.lang3.exception.ExceptionUtils import org.apache.commons.lang3.time.DateFormatUtils import org.jdesktop.swingx.JXEditorPane import org.slf4j.LoggerFactory @@ -55,6 +59,11 @@ import kotlin.time.Duration.Companion.milliseconds class SettingsOptionsPane : OptionsPane() { private val owner get() = SwingUtilities.getWindowAncestor(this@SettingsOptionsPane) private val database get() = Database.getDatabase() + private val hostManager get() = HostManager.getInstance() + private val keymapManager get() = KeymapManager.getInstance() + private val macroManager get() = MacroManager.getInstance() + private val keywordHighlightManager get() = KeywordHighlightManager.getInstance() + private val keyManager get() = KeyManager.getInstance() companion object { private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java) @@ -499,6 +508,7 @@ class SettingsOptionsPane : OptionsPane() { val domainTextField = OutlineTextField(255) val uploadConfigButton = JButton(I18n.getString("termora.settings.sync.push"), Icons.upload) val exportConfigButton = JButton(I18n.getString("termora.settings.sync.export"), Icons.export) + val importConfigButton = JButton(I18n.getString("termora.settings.sync.import"), Icons.import) val downloadConfigButton = JButton(I18n.getString("termora.settings.sync.pull"), Icons.download) val lastSyncTimeLabel = JLabel() val sync get() = database.sync @@ -610,6 +620,7 @@ class SettingsOptionsPane : OptionsPane() { } exportConfigButton.addActionListener { export() } + importConfigButton.addActionListener { import() } keysCheckBox.addActionListener { refreshButtons() } hostsCheckBox.addActionListener { refreshButtons() } @@ -626,6 +637,7 @@ class SettingsOptionsPane : OptionsPane() { || keywordHighlightsCheckBox.isSelected uploadConfigButton.isEnabled = downloadConfigButton.isEnabled exportConfigButton.isEnabled = downloadConfigButton.isEnabled + importConfigButton.isEnabled = downloadConfigButton.isEnabled } private fun export() { @@ -641,6 +653,109 @@ class SettingsOptionsPane : OptionsPane() { } } + private fun import() { + val fileChooser = FileChooser() + fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY + fileChooser.osxAllowedFileTypes = listOf("json") + fileChooser.win32Filters.add(Pair("JSON files", listOf("json"))) + fileChooser.showOpenDialog(owner).thenAccept { files -> + if (files.isNotEmpty()) { + SwingUtilities.invokeLater { importFromFile(files.first()) } + } + } + } + + private fun importFromFile(file: File) { + if (!file.exists()) { + return + } + + val ranges = getSyncConfig().ranges + if (ranges.isEmpty()) { + return + } + + // 最大 100MB + if (file.length() >= 1024 * 1024 * 100) { + OptionPane.showMessageDialog( + owner, I18n.getString("termora.settings.sync.import.file-too-large"), + messageType = JOptionPane.ERROR_MESSAGE + ) + return + } + + val text = file.readText() + val jsonResult = ohMyJson.runCatching { decodeFromString(text) } + if (jsonResult.isFailure) { + val e = jsonResult.exceptionOrNull() ?: return + OptionPane.showMessageDialog( + owner, ExceptionUtils.getRootCauseMessage(e), + messageType = JOptionPane.ERROR_MESSAGE + ) + return + } + + val json = jsonResult.getOrNull() ?: return + if (ranges.contains(SyncRange.Hosts)) { + val hosts = json["hosts"] + if (hosts is JsonArray) { + ohMyJson.runCatching { decodeFromJsonElement>(hosts.jsonArray) }.onSuccess { + for (host in it) { + hostManager.addHost(host) + } + } + } + } + + if (ranges.contains(SyncRange.KeyPairs)) { + val keyPairs = json["keyPairs"] + if (keyPairs is JsonArray) { + ohMyJson.runCatching { decodeFromJsonElement>(keyPairs.jsonArray) }.onSuccess { + for (keyPair in it) { + keyManager.addOhKeyPair(keyPair) + } + } + } + } + + if (ranges.contains(SyncRange.KeywordHighlights)) { + val keywordHighlights = json["keywordHighlights"] + if (keywordHighlights is JsonArray) { + ohMyJson.runCatching { decodeFromJsonElement>(keywordHighlights.jsonArray) } + .onSuccess { + for (keyPair in it) { + keywordHighlightManager.addKeywordHighlight(keyPair) + } + } + } + } + + if (ranges.contains(SyncRange.Macros)) { + val macros = json["macros"] + if (macros is JsonArray) { + ohMyJson.runCatching { decodeFromJsonElement>(macros.jsonArray) }.onSuccess { + for (macro in it) { + macroManager.addMacro(macro) + } + } + } + } + + if (ranges.contains(SyncRange.Keymap)) { + val keymaps = json["keymaps"] + if (keymaps is JsonArray) { + for (keymap in keymaps.jsonArray.mapNotNull { Keymap.fromJSON(it.jsonObject) }) { + keymapManager.addKeymap(keymap) + } + } + } + + OptionPane.showMessageDialog( + owner, I18n.getString("termora.settings.sync.import.successful"), + messageType = JOptionPane.INFORMATION_MESSAGE + ) + } + private fun exportText(file: File) { val syncConfig = getSyncConfig() val text = ohMyJson.encodeToString(buildJsonObject { @@ -651,21 +766,29 @@ class SettingsOptionsPane : OptionsPane() { put("os", SystemUtils.OS_NAME) put("exportDateHuman", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(Date(now))) if (syncConfig.ranges.contains(SyncRange.Hosts)) { - put("hosts", ohMyJson.encodeToJsonElement(HostManager.getInstance().hosts())) + put("hosts", ohMyJson.encodeToJsonElement(hostManager.hosts())) } if (syncConfig.ranges.contains(SyncRange.KeyPairs)) { - put("keyPairs", ohMyJson.encodeToJsonElement(KeyManager.getInstance().getOhKeyPairs())) + put("keyPairs", ohMyJson.encodeToJsonElement(keyManager.getOhKeyPairs())) } if (syncConfig.ranges.contains(SyncRange.KeywordHighlights)) { put( "keywordHighlights", - ohMyJson.encodeToJsonElement(KeywordHighlightManager.getInstance().getKeywordHighlights()) + ohMyJson.encodeToJsonElement(keywordHighlightManager.getKeywordHighlights()) ) } if (syncConfig.ranges.contains(SyncRange.Macros)) { put( "macros", - ohMyJson.encodeToJsonElement(MacroManager.getInstance().getMacros()) + ohMyJson.encodeToJsonElement(macroManager.getMacros()) + ) + } + if (syncConfig.ranges.contains(SyncRange.Keymap)) { + val keymaps = keymapManager.getKeymaps().filter { !it.isReadonly } + .map { it.toJSONObject() } + put( + "keymaps", + ohMyJson.encodeToJsonElement(keymaps) ) } put("settings", buildJsonObject { @@ -710,6 +833,7 @@ class SettingsOptionsPane : OptionsPane() { ) } + @Suppress("DuplicatedCode") private suspend fun pushOrPull(push: Boolean) { if (typeComboBox.selectedItem == SyncType.GitLab) { @@ -765,6 +889,7 @@ class SettingsOptionsPane : OptionsPane() { withContext(Dispatchers.Swing) { exportConfigButton.isEnabled = false + importConfigButton.isEnabled = false downloadConfigButton.isEnabled = false uploadConfigButton.isEnabled = false typeComboBox.isEnabled = false @@ -800,6 +925,7 @@ class SettingsOptionsPane : OptionsPane() { withContext(Dispatchers.Swing) { downloadConfigButton.isEnabled = true exportConfigButton.isEnabled = true + importConfigButton.isEnabled = true uploadConfigButton.isEnabled = true keysCheckBox.isEnabled = true hostsCheckBox.isEnabled = true @@ -940,7 +1066,7 @@ class SettingsOptionsPane : OptionsPane() { var rows = 1 val step = 2 - val builder = FormBuilder.create().layout(layout).debug(false); + val builder = FormBuilder.create().layout(layout).debug(false) val box = Box.createHorizontalBox() box.add(typeComboBox) if (typeComboBox.selectedItem == SyncType.GitLab) { @@ -959,10 +1085,11 @@ class SettingsOptionsPane : OptionsPane() { // Sync buttons .add( FormBuilder.create() - .layout(FormLayout("left:pref, $formMargin, left:pref, $formMargin, left:pref", "pref")) + .layout(FormLayout("pref, 2dlu, pref, 2dlu, pref, 2dlu, pref", "pref")) .add(uploadConfigButton).xy(1, 1) .add(downloadConfigButton).xy(3, 1) .add(exportConfigButton).xy(5, 1) + .add(importConfigButton).xy(7, 1) .build() ).xy(3, rows, "center, fill").apply { rows += step } .add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step } @@ -1057,8 +1184,6 @@ class SettingsOptionsPane : OptionsPane() { private val tip = FlatLabel() private val safeBtn = FlatButton() private val doorman get() = Doorman.getInstance() - private val hostManager get() = HostManager.getInstance() - private val keyManager get() = KeyManager.getInstance() init { initView() diff --git a/src/main/kotlin/app/termora/native/FileChooser.kt b/src/main/kotlin/app/termora/native/FileChooser.kt index 2f97921..77e60ba 100644 --- a/src/main/kotlin/app/termora/native/FileChooser.kt +++ b/src/main/kotlin/app/termora/native/FileChooser.kt @@ -17,6 +17,10 @@ class FileChooser { var allowsOtherFileTypes = true var canCreateDirectories = true var win32Filters = mutableListOf>>() + + /** + * e.g. listOf("json") + */ var osxAllowedFileTypes = emptyList() /** diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index e7ab915..20d43e4 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -70,7 +70,10 @@ termora.settings.sync.push=Push termora.settings.sync.push-warning=Pushing will overwrite the configuration. It is recommended to pull before pushing termora.settings.sync.pull=Pull termora.settings.sync.done=Synchronized data successfully -termora.settings.sync.export=Export +termora.settings.sync.export=${termora.keymgr.export} +termora.settings.sync.import=${termora.keymgr.import} +termora.settings.sync.import.file-too-large=The file is too large +termora.settings.sync.import.successful=Import data successfully termora.settings.sync.export-done=The export was successful termora.settings.sync.export-done-open-folder=The export was successful. Do you want to open the folder? termora.settings.sync.range=Range diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index 7964140..04bbfba 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -74,13 +74,14 @@ termora.settings.sync=同步 termora.settings.sync.push=推送 termora.settings.sync.push-warning=推送将覆盖已有配置,建议先拉取再推送 termora.settings.sync.pull=拉取 -termora.settings.sync.export=导出 termora.settings.sync.export-done=导出成功 termora.settings.sync.export-done-open-folder=导出成功,是否需要打开所在文件夹? termora.settings.sync.range=范围 termora.settings.sync.range.keys=我的密钥 termora.settings.sync.last-sync-time=最后同步时间 termora.settings.sync.done=同步数据成功 +termora.settings.sync.import.file-too-large=文件太大 +termora.settings.sync.import.successful=导入数据成功 termora.settings.sync.gist=片段 termora.settings.sync.token=令牌 termora.settings.sync.type=类型 diff --git a/src/main/resources/i18n/messages_zh_TW.properties b/src/main/resources/i18n/messages_zh_TW.properties index 27b6593..dde1149 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -78,13 +78,14 @@ termora.settings.sync=同步 termora.settings.sync.push=推送 termora.settings.sync.push-warning=推送將覆蓋先前的配置,建議先拉取再推送 termora.settings.sync.pull=拉取 -termora.settings.sync.export=匯出 termora.settings.sync.export-done=匯出成功 termora.settings.sync.export-done-open-folder=匯出成功,是否需要打開所在資料夾? termora.settings.sync.range=範圍 termora.settings.sync.range.keys=我的密鑰 termora.settings.sync.last-sync-time=最後同步時間 termora.settings.sync.done=同步資料成功 +termora.settings.sync.import.file-too-large=檔案太大 +termora.settings.sync.import.successful=導入資料成功 termora.settings.sync.gist=片段 termora.settings.sync.token=令牌 termora.settings.sync.type=類型