diff --git a/plugins/sync/build.gradle.kts b/plugins/sync/build.gradle.kts index 686e6ae..6395726 100644 --- a/plugins/sync/build.gradle.kts +++ b/plugins/sync/build.gradle.kts @@ -3,7 +3,7 @@ plugins { } -project.version = "0.0.1" +project.version = "0.0.2" dependencies { diff --git a/plugins/sync/src/main/kotlin/app/termora/plugins/sync/CloudSyncOption.kt b/plugins/sync/src/main/kotlin/app/termora/plugins/sync/CloudSyncOption.kt index ca8ac4f..9575ece 100644 --- a/plugins/sync/src/main/kotlin/app/termora/plugins/sync/CloudSyncOption.kt +++ b/plugins/sync/src/main/kotlin/app/termora/plugins/sync/CloudSyncOption.kt @@ -1,22 +1,16 @@ package app.termora.plugins.sync import app.termora.* -import app.termora.AES.encodeBase64String -import app.termora.Application.ohMyJson import app.termora.account.AccountManager import app.termora.account.AccountOwner import app.termora.database.DatabaseManager import app.termora.database.OwnerType -import app.termora.highlight.KeywordHighlight import app.termora.highlight.KeywordHighlightManager -import app.termora.keymap.Keymap import app.termora.keymap.KeymapManager import app.termora.keymgr.KeyManager -import app.termora.keymgr.OhKeyPair -import app.termora.macro.Macro import app.termora.macro.MacroManager -import app.termora.snippet.Snippet import app.termora.snippet.SnippetManager +import app.termora.tag.TagManager import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.extras.components.FlatComboBox import com.jgoodies.forms.builder.FormBuilder @@ -25,21 +19,14 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.withContext -import kotlinx.serialization.json.* -import org.apache.commons.codec.binary.Base64 -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.slf4j.LoggerFactory import java.awt.BorderLayout import java.awt.Component import java.awt.event.ActionEvent import java.awt.event.ItemEvent -import java.io.File import java.net.URI -import java.nio.charset.StandardCharsets import java.util.* import java.util.function.Consumer import javax.swing.* @@ -53,6 +40,7 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption { private val database get() = DatabaseManager.getInstance() private val hostManager get() = HostManager.getInstance() + private val tagManager get() = TagManager.getInstance() private val snippetManager get() = SnippetManager.getInstance() private val keymapManager get() = KeymapManager.getInstance() private val macroManager get() = MacroManager.getInstance() @@ -72,17 +60,15 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption { val gistTextField = OutlineTextField(255) val policyComboBox = JComboBox() val domainTextField = OutlineTextField(255) - val syncConfigButton = JButton(I18n.getString("termora.settings.sync"), Icons.settingSync) - val exportConfigButton = JButton(I18n.getString("termora.settings.sync.export"), Icons.export) - val importConfigButton = JButton(I18n.getString("termora.settings.sync.import"), Icons.import) + val syncConfigButton = JButton(SyncI18n.getString("termora.settings.sync"), Icons.settingSync) val lastSyncTimeLabel = JLabel() val sync get() = SyncProperties.getInstance() val hostsCheckBox = JCheckBox(I18n.getString("termora.welcome.my-hosts")) - val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys")) - val snippetsCheckBox = JCheckBox(I18n.getString("termora.snippet.title")) - val keywordHighlightsCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keyword-highlights")) - val macrosCheckBox = JCheckBox(I18n.getString("termora.macro")) - val keymapCheckBox = JCheckBox(I18n.getString("termora.settings.keymap")) + val keysCheckBox = JCheckBox(SyncI18n.getString("termora.settings.sync.range.keys")) + val snippetsCheckBox = JCheckBox(SyncI18n.getString("termora.snippet.title")) + val keywordHighlightsCheckBox = JCheckBox(SyncI18n.getString("termora.settings.sync.range.keyword-highlights")) + val macrosCheckBox = JCheckBox(SyncI18n.getString("termora.macro")) + val keymapCheckBox = JCheckBox(SyncI18n.getString("termora.settings.keymap")) val visitGistBtn = JButton(Icons.externalLink) val getTokenBtn = JButton(Icons.externalLink) private val owner get() = SwingUtilities.getWindowAncestor(this) @@ -191,9 +177,6 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption { } } - exportConfigButton.addActionListener { export() } - importConfigButton.addActionListener { import() } - keysCheckBox.addActionListener { refreshButtons() } hostsCheckBox.addActionListener { refreshButtons() } snippetsCheckBox.addActionListener { refreshButtons() } @@ -212,7 +195,7 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption { } withContext(Dispatchers.Swing) { - OptionPane.showMessageDialog(owner, message = I18n.getString("termora.settings.sync.done")) + OptionPane.showMessageDialog(owner, message = SyncI18n.getString("termora.settings.sync.done")) } } @@ -233,302 +216,6 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption { syncConfigButton.isEnabled = keysCheckBox.isSelected || hostsCheckBox.isSelected || keywordHighlightsCheckBox.isSelected - exportConfigButton.isEnabled = syncConfigButton.isEnabled - importConfigButton.isEnabled = syncConfigButton.isEnabled - } - - private fun export() { - - assertEventDispatchThread() - - val passwordField = OutlinePasswordField() - val panel = object : JPanel(BorderLayout()) { - override fun requestFocusInWindow(): Boolean { - return passwordField.requestFocusInWindow() - } - } - - val label = JLabel(I18n.getString("termora.settings.sync.export-encrypt") + StringUtils.SPACE.repeat(25)) - label.border = BorderFactory.createEmptyBorder(0, 0, 8, 0) - panel.add(label, BorderLayout.NORTH) - panel.add(passwordField, BorderLayout.CENTER) - - var password = StringUtils.EMPTY - - if (OptionPane.showConfirmDialog( - owner, - panel, - optionType = JOptionPane.YES_NO_OPTION, - initialValue = passwordField - ) == JOptionPane.YES_OPTION - ) { - password = String(passwordField.password).trim() - } - - - val fileChooser = FileChooser() - fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY - fileChooser.win32Filters.add(Pair("All Files", listOf("*"))) - fileChooser.win32Filters.add(Pair("JSON files", listOf("json"))) - fileChooser.showSaveDialog(owner, "${Application.getName()}.json").thenAccept { file -> - if (file != null) { - SwingUtilities.invokeLater { exportText(file, password) } - } - } - } - - 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()) } - } - } - } - - @Suppress("DuplicatedCode") - 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 - } - - var json = jsonResult.getOrNull() ?: return - - // 如果加密了 则解密数据 - if (json["encryption"]?.jsonPrimitive?.booleanOrNull == true) { - val data = json["data"]?.jsonPrimitive?.content ?: StringUtils.EMPTY - if (data.isBlank()) { - OptionPane.showMessageDialog( - owner, "Data file corruption", - messageType = JOptionPane.ERROR_MESSAGE - ) - return - } - - while (true) { - val passwordField = OutlinePasswordField() - val panel = object : JPanel(BorderLayout()) { - override fun requestFocusInWindow(): Boolean { - return passwordField.requestFocusInWindow() - } - } - - val label = JLabel("Please enter the password" + StringUtils.SPACE.repeat(25)) - label.border = BorderFactory.createEmptyBorder(0, 0, 8, 0) - panel.add(label, BorderLayout.NORTH) - panel.add(passwordField, BorderLayout.CENTER) - - if (OptionPane.showConfirmDialog( - owner, - panel, - optionType = JOptionPane.YES_NO_OPTION, - initialValue = passwordField - ) != JOptionPane.YES_OPTION - ) { - return - } - - if (passwordField.password.isEmpty()) { - OptionPane.showMessageDialog( - owner, I18n.getString("termora.doorman.unlock-data"), - messageType = JOptionPane.ERROR_MESSAGE - ) - continue - } - - val password = String(passwordField.password) - val key = PBKDF2.generateSecret( - password.toCharArray(), - password.toByteArray(), keyLength = 128 - ) - - try { - val dataText = AES.ECB.decrypt(key, Base64.decodeBase64(data)).toString(Charsets.UTF_8) - val dataJsonResult = ohMyJson.runCatching { decodeFromString(dataText) } - if (dataJsonResult.isFailure) { - val e = dataJsonResult.exceptionOrNull() ?: return - OptionPane.showMessageDialog( - owner, ExceptionUtils.getRootCauseMessage(e), - messageType = JOptionPane.ERROR_MESSAGE - ) - return - } - json = dataJsonResult.getOrNull() ?: return - break - } catch (_: Exception) { - OptionPane.showMessageDialog( - owner, I18n.getString("termora.doorman.password-wrong"), - messageType = JOptionPane.ERROR_MESSAGE - ) - } - - } - } - - 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.Snippets)) { - val snippets = json["snippets"] - if (snippets is JsonArray) { - ohMyJson.runCatching { decodeFromJsonElement>(snippets.jsonArray) }.onSuccess { - for (snippet in it) { - snippetManager.addSnippet(snippet) - } - } - } - } - - 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, accountOwner) - } - } - } - } - - 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, accountOwner) - } - } - } - } - - 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, password: String) { - val syncConfig = getSyncConfig() - var text = ohMyJson.encodeToString(buildJsonObject { - val now = System.currentTimeMillis() - put("exporter", SystemUtils.USER_NAME) - put("version", Application.getVersion()) - put("exportDate", now) - 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.hosts())) - } - if (syncConfig.ranges.contains(SyncRange.Snippets)) { - put("snippets", ohMyJson.encodeToJsonElement(snippetManager.snippets())) - } - if (syncConfig.ranges.contains(SyncRange.KeyPairs)) { - put("keyPairs", ohMyJson.encodeToJsonElement(keyManager.getOhKeyPairs())) - } - if (syncConfig.ranges.contains(SyncRange.KeywordHighlights)) { - put( - "keywordHighlights", - ohMyJson.encodeToJsonElement(keywordHighlightManager.getKeywordHighlights()) - ) - } - if (syncConfig.ranges.contains(SyncRange.Macros)) { - put( - "macros", - 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 { - put("appearance", ohMyJson.encodeToJsonElement(database.appearance.getProperties())) - put("sync", ohMyJson.encodeToJsonElement(sync.getProperties())) - put("terminal", ohMyJson.encodeToJsonElement(database.terminal.getProperties())) - }) - }) - - if (password.isNotBlank()) { - val key = PBKDF2.generateSecret( - password.toCharArray(), - password.toByteArray(), keyLength = 128 - ) - - text = ohMyJson.encodeToString(buildJsonObject { - put("encryption", true) - put("data", AES.ECB.encrypt(key, text.toByteArray(Charsets.UTF_8)).encodeBase64String()) - }) - } - - file.outputStream().use { - IOUtils.write(text, it, StandardCharsets.UTF_8) - OptionPane.openFileInFolder( - owner, - file, I18n.getString("termora.settings.sync.export-done-open-folder"), - I18n.getString("termora.settings.sync.export-done") - ) - } } private fun getSyncConfig(): SyncConfig { @@ -593,8 +280,6 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption { } withContext(Dispatchers.Swing) { - exportConfigButton.isEnabled = false - importConfigButton.isEnabled = false syncConfigButton.isEnabled = false typeComboBox.isEnabled = false gistTextField.isEnabled = false @@ -606,7 +291,7 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption { hostsCheckBox.isEnabled = false snippetsCheckBox.isEnabled = false domainTextField.isEnabled = false - syncConfigButton.text = "${I18n.getString("termora.settings.sync")}..." + syncConfigButton.text = "${SyncI18n.getString("termora.settings.sync")}..." } val syncConfig = getSyncConfig() @@ -624,8 +309,6 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption { // 恢复状态 withContext(Dispatchers.Swing) { syncConfigButton.isEnabled = true - exportConfigButton.isEnabled = true - importConfigButton.isEnabled = true keysCheckBox.isEnabled = true hostsCheckBox.isEnabled = true snippetsCheckBox.isEnabled = true @@ -636,7 +319,7 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption { tokenTextField.isEnabled = true domainTextField.isEnabled = true keywordHighlightsCheckBox.isEnabled = true - syncConfigButton.text = I18n.getString("termora.settings.sync") + syncConfigButton.text = SyncI18n.getString("termora.settings.sync") } // 如果失败,提示错误 @@ -662,7 +345,7 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption { val now = System.currentTimeMillis() sync.lastSyncTime = now val date = DateFormatUtils.format(Date(now), I18n.getString("termora.date-format")) - lastSyncTimeLabel.text = "${I18n.getString("termora.settings.sync.last-sync-time")}: $date" + lastSyncTimeLabel.text = "${SyncI18n.getString("termora.settings.sync.last-sync-time")}: $date" if (push && gistTextField.text.isBlank()) { gistTextField.text = syncResult.map { it.config }.getOrDefault(syncConfig).gistId } @@ -715,7 +398,7 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption { if (url.isNullOrBlank()) { OptionPane.showMessageDialog( owner, - I18n.getString("termora.settings.sync.webdav.help") + SyncI18n.getString("termora.settings.sync.webdav.help") ) } else { val uri = URI.create(url) @@ -759,18 +442,18 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption { ): Component { var text = value?.toString() ?: StringUtils.EMPTY if (value == SyncPolicy.Manual) { - text = I18n.getString("termora.settings.sync.policy.manual") + text = SyncI18n.getString("termora.settings.sync.policy.manual") } else if (value == SyncPolicy.OnChange) { - text = I18n.getString("termora.settings.sync.policy.on-change") + text = SyncI18n.getString("termora.settings.sync.policy.on-change") } return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus) } } val lastSyncTime = sync.lastSyncTime - lastSyncTimeLabel.text = "${I18n.getString("termora.settings.sync.last-sync-time")}: ${ + lastSyncTimeLabel.text = "${SyncI18n.getString("termora.settings.sync.last-sync-time")}: ${ if (lastSyncTime > 0) DateFormatUtils.format( - Date(lastSyncTime), I18n.getString("termora.date-format") + Date(lastSyncTime), SyncI18n.getString("termora.date-format") ) else "-" }" @@ -784,7 +467,7 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption { } override fun getTitle(): String { - return I18n.getString("termora.settings.sync") + return SyncI18n.getString("termora.settings.sync") } override fun getJComponent(): JComponent { @@ -821,21 +504,21 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption { box.add(Box.createHorizontalStrut(4)) box.add(domainTextField) } - builder.add("${I18n.getString("termora.settings.sync.type")}:").xy(1, rows) + builder.add("${SyncI18n.getString("termora.settings.sync.type")}:").xy(1, rows) .add(box).xy(3, rows).apply { rows += step } val isWebDAV = typeComboBox.selectedItem == SyncType.WebDAV val tokenText = if (isWebDAV) { - I18n.getString("termora.new-host.general.username") + SyncI18n.getString("termora.new-host.general.username") } else { - I18n.getString("termora.settings.sync.token") + SyncI18n.getString("termora.settings.sync.token") } val gistText = if (isWebDAV) { - I18n.getString("termora.new-host.general.password") + SyncI18n.getString("termora.new-host.general.password") } else { - I18n.getString("termora.settings.sync.gist") + SyncI18n.getString("termora.settings.sync.gist") } if (typeComboBox.selectedItem == SyncType.Gitee || isWebDAV) { @@ -853,17 +536,15 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption { .add(if (isWebDAV) gistTextField else tokenTextField).xy(3, rows).apply { rows += step } .add("${gistText}:").xy(1, rows) .add(if (isWebDAV) tokenTextField else gistTextField).xy(3, rows).apply { rows += step } - .add("${I18n.getString("termora.settings.sync.policy")}:").xy(1, rows) + .add("${SyncI18n.getString("termora.settings.sync.policy")}:").xy(1, rows) .add(syncPolicyBox).xy(3, rows).apply { rows += step } - .add("${I18n.getString("termora.settings.sync.range")}:").xy(1, rows) + .add("${SyncI18n.getString("termora.settings.sync.range")}:").xy(1, rows) .add(rangeBox).xy(3, rows).apply { rows += step } // Sync buttons .add( FormBuilder.create() - .layout(FormLayout("pref, 2dlu, pref, 2dlu, pref", "pref")) + .layout(FormLayout("pref", "pref")) .add(syncConfigButton).xy(1, 1) - .add(exportConfigButton).xy(3, 1) - .add(importConfigButton).xy(5, 1) .build() ).xy(3, rows, "center, fill").apply { rows += step } .add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step } diff --git a/plugins/sync/src/main/kotlin/app/termora/plugins/sync/DeleteDataManager.kt b/plugins/sync/src/main/kotlin/app/termora/plugins/sync/DeleteDataManager.kt new file mode 100644 index 0000000..4b6c0db --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/termora/plugins/sync/DeleteDataManager.kt @@ -0,0 +1,57 @@ +package app.termora.plugins.sync + +import app.termora.Application.ohMyJson +import app.termora.ApplicationScope +import app.termora.DeletedData +import app.termora.EnableManager +import app.termora.database.DatabaseChangedExtension +import app.termora.database.DatabaseManager + +/** + * 仅标记 + */ +class DeleteDataManager private constructor() : DatabaseChangedExtension { + companion object { + fun getInstance(): DeleteDataManager { + return ApplicationScope.Companion.forApplicationScope() + .getOrCreate(DeleteDataManager::class) { DeleteDataManager() } + } + } + + private val data = mutableMapOf() + private val databaseManager get() = DatabaseManager.Companion.getInstance() + private val enableManager get() = EnableManager.Companion.getInstance() + + init { + for (e in databaseManager.properties.getProperties()) { + if (e.key.startsWith("Setting.Properties.DeleteData_")) { + val deletedData = runCatching { ohMyJson.decodeFromString(e.value) } + .getOrNull() ?: continue + data[deletedData.id] = deletedData + } + } + } + + private fun addDeletedData(deletedData: DeletedData) { + data[deletedData.id] = deletedData + } + + fun getDeletedData(): List { + return data.values.sortedBy { it.deleteDate } + } + + + override fun onDataChanged( + id: String, + type: String, + action: DatabaseChangedExtension.Action, + source: DatabaseChangedExtension.Source + ) { + if (action != DatabaseChangedExtension.Action.Removed) return + if (id.isBlank() || type.isBlank()) return + val key = "DeleteData_${type}_${id}" + val deletedData = DeletedData(id, type, System.currentTimeMillis()) + enableManager.setFlag(key, ohMyJson.encodeToString(deletedData)) + addDeletedData(deletedData) + } +} \ No newline at end of file diff --git a/plugins/sync/src/main/kotlin/app/termora/plugins/sync/GitSyncer.kt b/plugins/sync/src/main/kotlin/app/termora/plugins/sync/GitSyncer.kt index edc9ea2..5e2b2a5 100644 --- a/plugins/sync/src/main/kotlin/app/termora/plugins/sync/GitSyncer.kt +++ b/plugins/sync/src/main/kotlin/app/termora/plugins/sync/GitSyncer.kt @@ -37,6 +37,9 @@ abstract class GitSyncer : SafetySyncer() { // decode hosts if (config.ranges.contains(SyncRange.Hosts)) { + gistResponse.gists.findLast { it.filename == "Tags" }?.let { + decodeTags(it.content, deletedData.filter { e -> e.type == "Tag" }, config) + } gistResponse.gists.findLast { it.filename == "Hosts" }?.let { decodeHosts(it.content, deletedData.filter { e -> e.type == "Host" }, config) } @@ -103,6 +106,12 @@ abstract class GitSyncer : SafetySyncer() { log.debug("Push encryptedHosts: {}", hostsContent) } gistFiles.add(GistFile("Hosts", hostsContent)) + + val tagsContent = encodeHosts(key) + if (log.isDebugEnabled) { + log.debug("Push encryptedTags: {}", tagsContent) + } + gistFiles.add(GistFile("Tags", tagsContent)) } diff --git a/plugins/sync/src/main/kotlin/app/termora/plugins/sync/SafetySyncer.kt b/plugins/sync/src/main/kotlin/app/termora/plugins/sync/SafetySyncer.kt index ca18ea6..556db0b 100644 --- a/plugins/sync/src/main/kotlin/app/termora/plugins/sync/SafetySyncer.kt +++ b/plugins/sync/src/main/kotlin/app/termora/plugins/sync/SafetySyncer.kt @@ -8,6 +8,7 @@ import app.termora.AES.encodeBase64String import app.termora.Application.ohMyJson import app.termora.account.AccountManager import app.termora.account.AccountOwner +import app.termora.database.DatabaseChangedExtension import app.termora.database.OwnerType import app.termora.highlight.KeywordHighlight import app.termora.highlight.KeywordHighlightManager @@ -19,6 +20,8 @@ import app.termora.macro.Macro import app.termora.macro.MacroManager import app.termora.snippet.Snippet import app.termora.snippet.SnippetManager +import app.termora.tag.Tag +import app.termora.tag.TagManager import kotlinx.serialization.json.JsonObject import org.apache.commons.lang3.ArrayUtils import org.slf4j.LoggerFactory @@ -39,6 +42,7 @@ abstract class SafetySyncer : Syncer { protected val snippetManager get() = SnippetManager.getInstance() protected val deleteDataManager get() = DeleteDataManager.getInstance() protected val accountManager get() = AccountManager.getInstance() + protected val tagManager get() = TagManager.getInstance() protected val accountOwner get() = AccountOwner( id = accountManager.getAccountId(), @@ -90,10 +94,11 @@ abstract class SafetySyncer : Syncer { parentId = encryptedHost.parentId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), ownerId = encryptedHost.ownerId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), creatorId = encryptedHost.creatorId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), + ownerType = OwnerType.User.name, createDate = encryptedHost.createDate, updateDate = encryptedHost.updateDate, ) - SwingUtilities.invokeLater { hostManager.addHost(host) } + SwingUtilities.invokeLater { hostManager.addHost(host, DatabaseChangedExtension.Source.Sync) } } catch (e: Exception) { if (log.isWarnEnabled) { log.warn("Decode host: ${encryptedHost.id} failed. error: {}", e.message, e) @@ -104,7 +109,6 @@ abstract class SafetySyncer : Syncer { SwingUtilities.invokeLater { deletedData.forEach { hostManager.removeHost(it.id) - deleteDataManager.removeHost(it.id, it.deleteDate) } } @@ -192,7 +196,6 @@ abstract class SafetySyncer : Syncer { SwingUtilities.invokeLater { deletedData.forEach { snippetManager.removeSnippet(it.id) - deleteDataManager.removeSnippet(it.id, it.deleteDate) } } @@ -256,7 +259,6 @@ abstract class SafetySyncer : Syncer { SwingUtilities.invokeLater { deletedData.forEach { keyManager.removeOhKeyPair(it.id) - deleteDataManager.removeKeyPair(it.id, it.deleteDate) } } @@ -318,7 +320,6 @@ abstract class SafetySyncer : Syncer { SwingUtilities.invokeLater { deletedData.forEach { keywordHighlightManager.removeKeywordHighlight(it.id) - deleteDataManager.removeKeywordHighlight(it.id, it.deleteDate) } } @@ -329,18 +330,76 @@ abstract class SafetySyncer : Syncer { protected fun encodeKeywordHighlights(key: ByteArray): String { val keywordHighlights = mutableListOf() - for (keywordHighlight in keywordHighlightManager.getKeywordHighlights()) { - // aes iv - val iv = getIv(keywordHighlight.id) - val encryptedKeyPair = keywordHighlight.copy( - keyword = keywordHighlight.keyword.aesCBCEncrypt(key, iv).encodeBase64String(), - description = keywordHighlight.description.aesCBCEncrypt(key, iv).encodeBase64String(), - ) - keywordHighlights.add(encryptedKeyPair) + for (ownerId in accountManager.getOwnerIds()) { + for (keywordHighlight in keywordHighlightManager.getKeywordHighlights(ownerId)) { + // aes iv + val iv = getIv(keywordHighlight.id) + val encryptedKeyPair = keywordHighlight.copy( + keyword = keywordHighlight.keyword.aesCBCEncrypt(key, iv).encodeBase64String(), + description = keywordHighlight.description.aesCBCEncrypt(key, iv).encodeBase64String(), + ) + keywordHighlights.add(encryptedKeyPair) + } } return ohMyJson.encodeToString(keywordHighlights) } + + protected fun decodeTags(text: String, deletedData: List, config: SyncConfig) { + // aes key + val key = getKey(config) + val encryptedTags = runCatching { ohMyJson.decodeFromString>(text) }.getOrNull() ?: emptyList() + val tags = accountManager.getOwnerIds().map { tagManager.getTags(it) } + .flatten().associateBy { it.id } + + for (e in encryptedTags) { + val tag = tags[e.id] + if (tag != null) { + if (tag.updateDate >= e.updateDate) { + continue + } + } + + try { + // aes iv + val iv = getIv(e.id) + + tagManager.addTag( + e.copy( + text = e.text.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), + ), accountOwner + ) + } catch (ex: Exception) { + if (log.isWarnEnabled) { + log.warn("Decode Tag: ${e.id} failed. error: {}", ex.message, ex) + } + } + } + + SwingUtilities.invokeLater { + deletedData.forEach { + tagManager.removeTag(it.id) + } + } + + if (log.isDebugEnabled) { + log.debug("Decode Tag: {}", text) + } + } + + protected fun encodeTags(key: ByteArray): String { + val tags = mutableListOf() + for (ownerId in accountManager.getOwnerIds()) { + for (tag in tagManager.getTags(ownerId)) { + // aes iv + val iv = getIv(tag.id) + val encryptedKeyPair = tag.copy(text = tag.text.aesCBCEncrypt(key, iv).encodeBase64String()) + tags.add(encryptedKeyPair) + } + } + return ohMyJson.encodeToString(tags) + } + protected fun decodeMacros(text: String, deletedData: List, config: SyncConfig) { // aes key val key = getKey(config) @@ -373,7 +432,6 @@ abstract class SafetySyncer : Syncer { SwingUtilities.invokeLater { deletedData.forEach { macroManager.removeMacro(it.id) - deleteDataManager.removeMacro(it.id, it.deleteDate) } } @@ -413,7 +471,6 @@ abstract class SafetySyncer : Syncer { SwingUtilities.invokeLater { deletedData.forEach { keymapManager.removeKeymap(it.id) - deleteDataManager.removeKeymap(it.id, it.deleteDate) } } diff --git a/plugins/sync/src/main/kotlin/app/termora/plugins/sync/SyncI18n.kt b/plugins/sync/src/main/kotlin/app/termora/plugins/sync/SyncI18n.kt new file mode 100644 index 0000000..8f13748 --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/termora/plugins/sync/SyncI18n.kt @@ -0,0 +1,24 @@ +package app.termora.plugins.sync + +import app.termora.I18n +import app.termora.NamedI18n +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.* + +object SyncI18n : NamedI18n("i18n/messages") { + private val log = LoggerFactory.getLogger(SyncI18n::class.java) + + override fun getLogger(): Logger { + return log + } + + override fun getString(key: String): String { + return try { + substitutor.replace(getBundle().getString(key)) + } catch (_: MissingResourceException) { + I18n.getString(key) + } + } + +} \ No newline at end of file diff --git a/plugins/sync/src/main/kotlin/app/termora/plugins/sync/SyncManager.kt b/plugins/sync/src/main/kotlin/app/termora/plugins/sync/SyncManager.kt index 819cc4b..957bf82 100644 --- a/plugins/sync/src/main/kotlin/app/termora/plugins/sync/SyncManager.kt +++ b/plugins/sync/src/main/kotlin/app/termora/plugins/sync/SyncManager.kt @@ -2,6 +2,7 @@ package app.termora.plugins.sync import app.termora.ApplicationScope import app.termora.Disposable +import app.termora.account.AccountManager import kotlinx.coroutines.* import org.slf4j.LoggerFactory import kotlin.random.Random @@ -21,6 +22,7 @@ class SyncManager private constructor() : Disposable { private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private var job: Job? = null private var disableTrigger = false + private val accountManager get() = AccountManager.getInstance() private fun trigger() { @@ -38,6 +40,10 @@ class SyncManager private constructor() : Disposable { job?.cancel() + if (accountManager.isLocally().not()) { + return + } + if (log.isInfoEnabled) { log.info("Automatic synchronisation is interrupted") } @@ -124,6 +130,10 @@ class SyncManager private constructor() : Disposable { fun pull(config: SyncConfig): GistResponse { + if (accountManager.isLocally().not()) { + throw IllegalStateException(SyncI18n.getString("termora.plugins.sync.disabled-sync")) + } + synchronized(this) { disableTrigger = true try { @@ -135,6 +145,10 @@ class SyncManager private constructor() : Disposable { } fun push(config: SyncConfig): GistResponse { + if (accountManager.isLocally().not()) { + throw IllegalStateException(SyncI18n.getString("termora.plugins.sync.disabled-sync")) + } + synchronized(this) { try { disableTrigger = true diff --git a/plugins/sync/src/main/kotlin/app/termora/plugins/sync/SyncPlugin.kt b/plugins/sync/src/main/kotlin/app/termora/plugins/sync/SyncPlugin.kt index 6464045..624638f 100644 --- a/plugins/sync/src/main/kotlin/app/termora/plugins/sync/SyncPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/termora/plugins/sync/SyncPlugin.kt @@ -12,6 +12,7 @@ class SyncPlugin : Plugin { init { support.addExtension(SettingsOptionExtension::class.java) { SyncSettingsOptionExtension.instance } support.addExtension(DatabaseChangedExtension::class.java) { SyncDatabaseChangedExtension.instance } + support.addExtension(DatabaseChangedExtension::class.java) { DeleteDataManager.getInstance() } } override fun getAuthor(): String { diff --git a/plugins/sync/src/main/kotlin/app/termora/plugins/sync/WebDAVSyncer.kt b/plugins/sync/src/main/kotlin/app/termora/plugins/sync/WebDAVSyncer.kt index 01e48cb..b1bdb79 100644 --- a/plugins/sync/src/main/kotlin/app/termora/plugins/sync/WebDAVSyncer.kt +++ b/plugins/sync/src/main/kotlin/app/termora/plugins/sync/WebDAVSyncer.kt @@ -44,6 +44,9 @@ class WebDAVSyncer private constructor() : SafetySyncer() { // decode hosts if (config.ranges.contains(SyncRange.Hosts)) { + json["Tags"]?.jsonPrimitive?.content?.let { + decodeTags(it, deletedData.filter { e -> e.type == "Tags" }, config) + } json["Hosts"]?.jsonPrimitive?.content?.let { decodeHosts(it, deletedData.filter { e -> e.type == "Host" }, config) } @@ -98,6 +101,12 @@ class WebDAVSyncer private constructor() : SafetySyncer() { log.debug("Push encryptedHosts: {}", hostsContent) } put("Hosts", hostsContent) + + val tagsContent = encodeTags(key) + if (log.isDebugEnabled) { + log.debug("Push encryptedTags: {}", tagsContent) + } + put("Tags", tagsContent) } // Snippets diff --git a/plugins/sync/src/main/resources/i18n/messages.properties b/plugins/sync/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..9747ff5 --- /dev/null +++ b/plugins/sync/src/main/resources/i18n/messages.properties @@ -0,0 +1,22 @@ +termora.plugins.sync.disabled-sync=You are already logged in and cannot use this feature + +termora.settings.sync=Sync +termora.settings.sync.done=Synchronized data successfully +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-encrypt=Enter password to encrypt file (optional) +termora.settings.sync.export-done-open-folder=The export was successful. Do you want to open the folder? +termora.settings.sync.range=Range +termora.settings.sync.range.keys=My keys +termora.settings.sync.range.keyword-highlights=${termora.highlight} +termora.settings.sync.last-sync-time=Last sync time +termora.settings.sync.gist=Gist +termora.settings.sync.token=Token +termora.settings.sync.type=Type +termora.settings.sync.webdav.help=WebDAV storage address, e.g. https://yourhost/webdav/termora.json +termora.settings.sync.policy=Sync Policy +termora.settings.sync.policy.manual=Manual +termora.settings.sync.policy.on-change=On Change diff --git a/plugins/sync/src/main/resources/i18n/messages_zh_CN.properties b/plugins/sync/src/main/resources/i18n/messages_zh_CN.properties new file mode 100644 index 0000000..2854908 --- /dev/null +++ b/plugins/sync/src/main/resources/i18n/messages_zh_CN.properties @@ -0,0 +1,20 @@ +termora.plugins.sync.disabled-sync=你已登录,无法使用此功能 + + +termora.settings.sync=同步 +termora.settings.sync.export-done=导出成功 +termora.settings.sync.export-encrypt=输入密码加密文件 (可选) +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=类型 +termora.settings.sync.webdav.help=WebDAV 的存储地址,例如:https://yourhost/webdav/termora.json +termora.settings.sync.policy=同步策略 +termora.settings.sync.policy.manual=手动 +termora.settings.sync.policy.on-change=数据变动时 diff --git a/plugins/sync/src/main/resources/i18n/messages_zh_TW.properties b/plugins/sync/src/main/resources/i18n/messages_zh_TW.properties new file mode 100644 index 0000000..20152a6 --- /dev/null +++ b/plugins/sync/src/main/resources/i18n/messages_zh_TW.properties @@ -0,0 +1,19 @@ +termora.plugins.sync.disabled-sync=你已登錄,無法使用此功能 + +termora.settings.sync=同步 +termora.settings.sync.export-done=匯出成功 +termora.settings.sync.export-encrypt=輸入密碼加密檔案 (可選) +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=類型 +termora.settings.sync.webdav.help=WebDAV 的儲存位址,例如:https://yourhost/webdav/termora.json +termora.settings.sync.policy=同步策略 +termora.settings.sync.policy.manual=手動 +termora.settings.sync.policy.on-change=資料變動時 diff --git a/src/main/kotlin/app/termora/AbstractI18n.kt b/src/main/kotlin/app/termora/AbstractI18n.kt index c0cc79c..0b4790f 100644 --- a/src/main/kotlin/app/termora/AbstractI18n.kt +++ b/src/main/kotlin/app/termora/AbstractI18n.kt @@ -8,9 +8,9 @@ import java.util.* abstract class AbstractI18n { private val log get() = getLogger() - private val substitutor by lazy { StringSubstitutor { key -> getString(key) } } + protected val substitutor by lazy { StringSubstitutor { key -> getString(key) } } - fun getString(key: String, vararg args: Any): String { + open fun getString(key: String, vararg args: Any): String { val text = getString(key) if (args.isNotEmpty()) { return MessageFormat.format(text, *args) @@ -19,7 +19,7 @@ abstract class AbstractI18n { } - fun getString(key: String): String { + open fun getString(key: String): String { try { return substitutor.replace(getBundle().getString(key)) } catch (e: MissingResourceException) { diff --git a/src/main/kotlin/app/termora/DeleteDataManager.kt b/src/main/kotlin/app/termora/DeleteDataManager.kt deleted file mode 100644 index 07835d1..0000000 --- a/src/main/kotlin/app/termora/DeleteDataManager.kt +++ /dev/null @@ -1,54 +0,0 @@ -package app.termora - -import app.termora.database.DatabaseManager - -/** - * 仅标记 - */ -class DeleteDataManager private constructor() { - companion object { - fun getInstance(): DeleteDataManager { - return ApplicationScope.forApplicationScope().getOrCreate(DeleteDataManager::class) { DeleteDataManager() } - } - } - - private val data = mutableMapOf() - private val database get() = DatabaseManager.getInstance() - - fun removeHost(id: String, deleteDate: Long = System.currentTimeMillis()) { - addDeletedData(DeletedData(id, "Host", deleteDate)) - } - - fun removeKeymap(id: String, deleteDate: Long = System.currentTimeMillis()) { - addDeletedData(DeletedData(id, "Keymap", deleteDate)) - } - - fun removeKeyPair(id: String, deleteDate: Long = System.currentTimeMillis()) { - addDeletedData(DeletedData(id, "KeyPair", deleteDate)) - } - - fun removeKeywordHighlight(id: String, deleteDate: Long = System.currentTimeMillis()) { - addDeletedData(DeletedData(id, "KeywordHighlight", deleteDate)) - } - - fun removeMacro(id: String, deleteDate: Long = System.currentTimeMillis()) { - addDeletedData(DeletedData(id, "Macro", deleteDate)) - } - - fun removeSnippet(id: String, deleteDate: Long = System.currentTimeMillis()) { - addDeletedData(DeletedData(id, "Snippet", deleteDate)) - } - - private fun addDeletedData(deletedData: DeletedData) { - if (data.containsKey(deletedData.id)) return - data[deletedData.id] = deletedData - // TODO database.addDeletedData(deletedData) - } - - fun getDeletedData(): List { - if (data.isEmpty()) { - // TODO data.putAll(database.getDeletedData().associateBy { it.id }) - } - return data.values.sortedBy { it.deleteDate } - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/account/AccountManager.kt b/src/main/kotlin/app/termora/account/AccountManager.kt index ccded90..65300c6 100644 --- a/src/main/kotlin/app/termora/account/AccountManager.kt +++ b/src/main/kotlin/app/termora/account/AccountManager.kt @@ -2,6 +2,7 @@ package app.termora.account import app.termora.* import app.termora.Application.ohMyJson +import app.termora.database.OwnerType import app.termora.plugin.ExtensionManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -47,6 +48,10 @@ class AccountManager private constructor() : ApplicationRunnerExtension { fun getAccessToken() = account.accessToken fun getRefreshToken() = account.refreshToken fun getOwnerIds() = account.teams.map { it.id }.toMutableList().apply { add(getAccountId()) }.toSet() + fun getOwners() = + account.teams.map { AccountOwner(it.id, it.name, OwnerType.Team) } + .toMutableList().apply { AccountOwner(getAccountId(), getEmail(), OwnerType.User) } + .toSet() fun isFreePlan(): Boolean { return isLocally() || getSubscription().plan == SubscriptionPlan.Free diff --git a/src/main/kotlin/app/termora/database/DatabaseManager.kt b/src/main/kotlin/app/termora/database/DatabaseManager.kt index 886829f..ab32997 100644 --- a/src/main/kotlin/app/termora/database/DatabaseManager.kt +++ b/src/main/kotlin/app/termora/database/DatabaseManager.kt @@ -252,7 +252,7 @@ class DatabaseManager private constructor() : Disposable { source ) } else { - save(data) + save(data, source) } } diff --git a/src/main/kotlin/app/termora/highlight/KeywordHighlightManager.kt b/src/main/kotlin/app/termora/highlight/KeywordHighlightManager.kt index 74c7dcf..c379f9c 100644 --- a/src/main/kotlin/app/termora/highlight/KeywordHighlightManager.kt +++ b/src/main/kotlin/app/termora/highlight/KeywordHighlightManager.kt @@ -2,7 +2,6 @@ package app.termora.highlight import app.termora.Application.ohMyJson import app.termora.ApplicationScope -import app.termora.DeleteDataManager import app.termora.TerminalPanelFactory import app.termora.account.AccountOwner import app.termora.database.Data @@ -48,7 +47,6 @@ class KeywordHighlightManager private constructor() { fun removeKeywordHighlight(id: String) { database.delete(id, DataType.KeywordHighlight.name) TerminalPanelFactory.getInstance().repaintAll() - DeleteDataManager.getInstance().removeKeywordHighlight(id) if (log.isDebugEnabled) { log.debug("Keyword highlighter removed. {}", id) diff --git a/src/main/kotlin/app/termora/keymgr/KeyManager.kt b/src/main/kotlin/app/termora/keymgr/KeyManager.kt index 54b96a1..6577b71 100644 --- a/src/main/kotlin/app/termora/keymgr/KeyManager.kt +++ b/src/main/kotlin/app/termora/keymgr/KeyManager.kt @@ -2,7 +2,6 @@ package app.termora.keymgr import app.termora.Application.ohMyJson import app.termora.ApplicationScope -import app.termora.DeleteDataManager import app.termora.account.AccountOwner import app.termora.database.Data import app.termora.database.DataType @@ -36,7 +35,6 @@ class KeyManager private constructor() { fun removeOhKeyPair(id: String) { databaseManager.delete(id, DataType.KeyPair.name) - DeleteDataManager.getInstance().removeKeyPair(id) } fun getOhKeyPairs(): List { diff --git a/src/main/kotlin/app/termora/macro/MacroManager.kt b/src/main/kotlin/app/termora/macro/MacroManager.kt index a6e3118..b8c20a3 100644 --- a/src/main/kotlin/app/termora/macro/MacroManager.kt +++ b/src/main/kotlin/app/termora/macro/MacroManager.kt @@ -2,7 +2,6 @@ package app.termora.macro import app.termora.Application.ohMyJson import app.termora.ApplicationScope -import app.termora.DeleteDataManager import app.termora.account.AccountManager import app.termora.database.Data import app.termora.database.DataType @@ -50,7 +49,6 @@ class MacroManager private constructor() { fun removeMacro(id: String) { database.delete(id, DataType.Macro.name) - DeleteDataManager.getInstance().removeMacro(id) if (log.isDebugEnabled) { log.debug("Removed macro $id") diff --git a/src/main/kotlin/app/termora/snippet/SnippetManager.kt b/src/main/kotlin/app/termora/snippet/SnippetManager.kt index f74aeca..af36892 100644 --- a/src/main/kotlin/app/termora/snippet/SnippetManager.kt +++ b/src/main/kotlin/app/termora/snippet/SnippetManager.kt @@ -2,7 +2,6 @@ package app.termora.snippet import app.termora.Application.ohMyJson import app.termora.ApplicationScope -import app.termora.DeleteDataManager import app.termora.account.AccountManager import app.termora.assertEventDispatchThread import app.termora.database.Data @@ -45,7 +44,6 @@ class SnippetManager private constructor() { fun removeSnippet(id: String) { database.delete(id, DataType.Snippet.name) - DeleteDataManager.getInstance().removeSnippet(id) } /** diff --git a/src/main/kotlin/app/termora/tag/Tag.kt b/src/main/kotlin/app/termora/tag/Tag.kt index 567830b..3e70650 100644 --- a/src/main/kotlin/app/termora/tag/Tag.kt +++ b/src/main/kotlin/app/termora/tag/Tag.kt @@ -6,6 +6,6 @@ import kotlinx.serialization.Serializable data class Tag( val id: String, val text: String, - val createDate: Long = System.currentTimeMillis(), - val updateDate: Long = System.currentTimeMillis(), + val createDate: Long, + val updateDate: Long, ) \ No newline at end of file diff --git a/src/main/kotlin/app/termora/tag/TagPanel.kt b/src/main/kotlin/app/termora/tag/TagPanel.kt index 37925ae..3dc445d 100644 --- a/src/main/kotlin/app/termora/tag/TagPanel.kt +++ b/src/main/kotlin/app/termora/tag/TagPanel.kt @@ -63,7 +63,14 @@ class TagPanel(accountOwner: AccountOwner) : JPanel(BorderLayout()), Disposable title = I18n.getString("termora.tag"), ) if (text.isNullOrBlank().not()) { - model.addElement(Tag(id = randomUUID(), text = text)) + model.addElement( + Tag( + id = randomUUID(), + text = text, + createDate = System.currentTimeMillis(), + updateDate = System.currentTimeMillis(), + ) + ) } } diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index c7448dc..b2fa688 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -70,26 +70,6 @@ termora.settings.terminal.floating-toolbar=Floating Toolbar termora.settings.terminal.auto-close-tab=Auto Close Tab termora.settings.terminal.auto-close-tab-description=Automatically close the tab when the terminal is disconnected normally -termora.settings.sync=Sync -termora.settings.sync.done=Synchronized data successfully -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-encrypt=Enter password to encrypt file (optional) -termora.settings.sync.export-done-open-folder=The export was successful. Do you want to open the folder? -termora.settings.sync.range=Range -termora.settings.sync.range.keys=My keys -termora.settings.sync.range.keyword-highlights=${termora.highlight} -termora.settings.sync.last-sync-time=Last sync time -termora.settings.sync.gist=Gist -termora.settings.sync.token=Token -termora.settings.sync.type=Type -termora.settings.sync.webdav.help=WebDAV storage address, e.g. https://yourhost/webdav/termora.json -termora.settings.sync.policy=Sync Policy -termora.settings.sync.policy.manual=Manual -termora.settings.sync.policy.on-change=On Change termora.settings.about=About termora.settings.about.author=Author diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index 585173d..67452f3 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -81,24 +81,6 @@ termora.settings.terminal.auto-close-tab=自动关闭标签 termora.settings.terminal.auto-close-tab-description=当终端正常断开连接时自动关闭标签页 -termora.settings.sync=同步 -termora.settings.sync.export-done=导出成功 -termora.settings.sync.export-encrypt=输入密码加密文件 (可选) -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=类型 -termora.settings.sync.webdav.help=WebDAV 的存储地址,例如:https://yourhost/webdav/termora.json -termora.settings.sync.policy=同步策略 -termora.settings.sync.policy.manual=手动 -termora.settings.sync.policy.on-change=数据变动时 - termora.settings.about=关于 termora.settings.about.author=作者 termora.settings.about.source=源代码 diff --git a/src/main/resources/i18n/messages_zh_TW.properties b/src/main/resources/i18n/messages_zh_TW.properties index 3144038..9269dae 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -92,23 +92,6 @@ termora.settings.terminal.floating-toolbar=懸浮工具列 termora.settings.terminal.auto-close-tab=自動關閉標籤 termora.settings.terminal.auto-close-tab-description=當終端正常斷開連線時自動關閉標籤頁 -termora.settings.sync=同步 -termora.settings.sync.export-done=匯出成功 -termora.settings.sync.export-encrypt=輸入密碼加密檔案 (可選) -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=類型 -termora.settings.sync.webdav.help=WebDAV 的儲存位址,例如:https://yourhost/webdav/termora.json -termora.settings.sync.policy=同步策略 -termora.settings.sync.policy.manual=手動 -termora.settings.sync.policy.on-change=資料變動時 termora.settings.about=關於 termora.settings.about.author=作者