mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
chore: improve sync plugin
This commit is contained in:
@@ -3,7 +3,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
project.version = "0.0.1"
|
project.version = "0.0.2"
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
package app.termora.plugins.sync
|
package app.termora.plugins.sync
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.AES.encodeBase64String
|
|
||||||
import app.termora.Application.ohMyJson
|
|
||||||
import app.termora.account.AccountManager
|
import app.termora.account.AccountManager
|
||||||
import app.termora.account.AccountOwner
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
import app.termora.database.OwnerType
|
import app.termora.database.OwnerType
|
||||||
import app.termora.highlight.KeywordHighlight
|
|
||||||
import app.termora.highlight.KeywordHighlightManager
|
import app.termora.highlight.KeywordHighlightManager
|
||||||
import app.termora.keymap.Keymap
|
|
||||||
import app.termora.keymap.KeymapManager
|
import app.termora.keymap.KeymapManager
|
||||||
import app.termora.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
import app.termora.keymgr.OhKeyPair
|
|
||||||
import app.termora.macro.Macro
|
|
||||||
import app.termora.macro.MacroManager
|
import app.termora.macro.MacroManager
|
||||||
import app.termora.snippet.Snippet
|
|
||||||
import app.termora.snippet.SnippetManager
|
import app.termora.snippet.SnippetManager
|
||||||
|
import app.termora.tag.TagManager
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
@@ -25,21 +19,14 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.coroutines.withContext
|
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.StringUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils
|
import org.apache.commons.lang3.time.DateFormatUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import java.awt.event.ItemEvent
|
import java.awt.event.ItemEvent
|
||||||
import java.io.File
|
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
@@ -53,6 +40,7 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
|||||||
|
|
||||||
private val database get() = DatabaseManager.getInstance()
|
private val database get() = DatabaseManager.getInstance()
|
||||||
private val hostManager get() = HostManager.getInstance()
|
private val hostManager get() = HostManager.getInstance()
|
||||||
|
private val tagManager get() = TagManager.getInstance()
|
||||||
private val snippetManager get() = SnippetManager.getInstance()
|
private val snippetManager get() = SnippetManager.getInstance()
|
||||||
private val keymapManager get() = KeymapManager.getInstance()
|
private val keymapManager get() = KeymapManager.getInstance()
|
||||||
private val macroManager get() = MacroManager.getInstance()
|
private val macroManager get() = MacroManager.getInstance()
|
||||||
@@ -72,17 +60,15 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
|||||||
val gistTextField = OutlineTextField(255)
|
val gistTextField = OutlineTextField(255)
|
||||||
val policyComboBox = JComboBox<SyncPolicy>()
|
val policyComboBox = JComboBox<SyncPolicy>()
|
||||||
val domainTextField = OutlineTextField(255)
|
val domainTextField = OutlineTextField(255)
|
||||||
val syncConfigButton = JButton(I18n.getString("termora.settings.sync"), Icons.settingSync)
|
val syncConfigButton = JButton(SyncI18n.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 lastSyncTimeLabel = JLabel()
|
val lastSyncTimeLabel = JLabel()
|
||||||
val sync get() = SyncProperties.getInstance()
|
val sync get() = SyncProperties.getInstance()
|
||||||
val hostsCheckBox = JCheckBox(I18n.getString("termora.welcome.my-hosts"))
|
val hostsCheckBox = JCheckBox(I18n.getString("termora.welcome.my-hosts"))
|
||||||
val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys"))
|
val keysCheckBox = JCheckBox(SyncI18n.getString("termora.settings.sync.range.keys"))
|
||||||
val snippetsCheckBox = JCheckBox(I18n.getString("termora.snippet.title"))
|
val snippetsCheckBox = JCheckBox(SyncI18n.getString("termora.snippet.title"))
|
||||||
val keywordHighlightsCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keyword-highlights"))
|
val keywordHighlightsCheckBox = JCheckBox(SyncI18n.getString("termora.settings.sync.range.keyword-highlights"))
|
||||||
val macrosCheckBox = JCheckBox(I18n.getString("termora.macro"))
|
val macrosCheckBox = JCheckBox(SyncI18n.getString("termora.macro"))
|
||||||
val keymapCheckBox = JCheckBox(I18n.getString("termora.settings.keymap"))
|
val keymapCheckBox = JCheckBox(SyncI18n.getString("termora.settings.keymap"))
|
||||||
val visitGistBtn = JButton(Icons.externalLink)
|
val visitGistBtn = JButton(Icons.externalLink)
|
||||||
val getTokenBtn = JButton(Icons.externalLink)
|
val getTokenBtn = JButton(Icons.externalLink)
|
||||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
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() }
|
keysCheckBox.addActionListener { refreshButtons() }
|
||||||
hostsCheckBox.addActionListener { refreshButtons() }
|
hostsCheckBox.addActionListener { refreshButtons() }
|
||||||
snippetsCheckBox.addActionListener { refreshButtons() }
|
snippetsCheckBox.addActionListener { refreshButtons() }
|
||||||
@@ -212,7 +195,7 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
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
|
syncConfigButton.isEnabled = keysCheckBox.isSelected || hostsCheckBox.isSelected
|
||||||
|| keywordHighlightsCheckBox.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<JsonObject>(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<JsonObject>(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<List<Host>>(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<List<Snippet>>(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<List<OhKeyPair>>(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<List<KeywordHighlight>>(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<List<Macro>>(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 {
|
private fun getSyncConfig(): SyncConfig {
|
||||||
@@ -593,8 +280,6 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
exportConfigButton.isEnabled = false
|
|
||||||
importConfigButton.isEnabled = false
|
|
||||||
syncConfigButton.isEnabled = false
|
syncConfigButton.isEnabled = false
|
||||||
typeComboBox.isEnabled = false
|
typeComboBox.isEnabled = false
|
||||||
gistTextField.isEnabled = false
|
gistTextField.isEnabled = false
|
||||||
@@ -606,7 +291,7 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
|||||||
hostsCheckBox.isEnabled = false
|
hostsCheckBox.isEnabled = false
|
||||||
snippetsCheckBox.isEnabled = false
|
snippetsCheckBox.isEnabled = false
|
||||||
domainTextField.isEnabled = false
|
domainTextField.isEnabled = false
|
||||||
syncConfigButton.text = "${I18n.getString("termora.settings.sync")}..."
|
syncConfigButton.text = "${SyncI18n.getString("termora.settings.sync")}..."
|
||||||
}
|
}
|
||||||
|
|
||||||
val syncConfig = getSyncConfig()
|
val syncConfig = getSyncConfig()
|
||||||
@@ -624,8 +309,6 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
|||||||
// 恢复状态
|
// 恢复状态
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
syncConfigButton.isEnabled = true
|
syncConfigButton.isEnabled = true
|
||||||
exportConfigButton.isEnabled = true
|
|
||||||
importConfigButton.isEnabled = true
|
|
||||||
keysCheckBox.isEnabled = true
|
keysCheckBox.isEnabled = true
|
||||||
hostsCheckBox.isEnabled = true
|
hostsCheckBox.isEnabled = true
|
||||||
snippetsCheckBox.isEnabled = true
|
snippetsCheckBox.isEnabled = true
|
||||||
@@ -636,7 +319,7 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
|||||||
tokenTextField.isEnabled = true
|
tokenTextField.isEnabled = true
|
||||||
domainTextField.isEnabled = true
|
domainTextField.isEnabled = true
|
||||||
keywordHighlightsCheckBox.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()
|
val now = System.currentTimeMillis()
|
||||||
sync.lastSyncTime = now
|
sync.lastSyncTime = now
|
||||||
val date = DateFormatUtils.format(Date(now), I18n.getString("termora.date-format"))
|
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()) {
|
if (push && gistTextField.text.isBlank()) {
|
||||||
gistTextField.text = syncResult.map { it.config }.getOrDefault(syncConfig).gistId
|
gistTextField.text = syncResult.map { it.config }.getOrDefault(syncConfig).gistId
|
||||||
}
|
}
|
||||||
@@ -715,7 +398,7 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
|||||||
if (url.isNullOrBlank()) {
|
if (url.isNullOrBlank()) {
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
owner,
|
owner,
|
||||||
I18n.getString("termora.settings.sync.webdav.help")
|
SyncI18n.getString("termora.settings.sync.webdav.help")
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val uri = URI.create(url)
|
val uri = URI.create(url)
|
||||||
@@ -759,18 +442,18 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
|||||||
): Component {
|
): Component {
|
||||||
var text = value?.toString() ?: StringUtils.EMPTY
|
var text = value?.toString() ?: StringUtils.EMPTY
|
||||||
if (value == SyncPolicy.Manual) {
|
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) {
|
} 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)
|
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val lastSyncTime = sync.lastSyncTime
|
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(
|
if (lastSyncTime > 0) DateFormatUtils.format(
|
||||||
Date(lastSyncTime), I18n.getString("termora.date-format")
|
Date(lastSyncTime), SyncI18n.getString("termora.date-format")
|
||||||
) else "-"
|
) else "-"
|
||||||
}"
|
}"
|
||||||
|
|
||||||
@@ -784,7 +467,7 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getTitle(): String {
|
override fun getTitle(): String {
|
||||||
return I18n.getString("termora.settings.sync")
|
return SyncI18n.getString("termora.settings.sync")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getJComponent(): JComponent {
|
override fun getJComponent(): JComponent {
|
||||||
@@ -821,21 +504,21 @@ class CloudSyncOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
|||||||
box.add(Box.createHorizontalStrut(4))
|
box.add(Box.createHorizontalStrut(4))
|
||||||
box.add(domainTextField)
|
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 }
|
.add(box).xy(3, rows).apply { rows += step }
|
||||||
|
|
||||||
val isWebDAV = typeComboBox.selectedItem == SyncType.WebDAV
|
val isWebDAV = typeComboBox.selectedItem == SyncType.WebDAV
|
||||||
|
|
||||||
val tokenText = if (isWebDAV) {
|
val tokenText = if (isWebDAV) {
|
||||||
I18n.getString("termora.new-host.general.username")
|
SyncI18n.getString("termora.new-host.general.username")
|
||||||
} else {
|
} else {
|
||||||
I18n.getString("termora.settings.sync.token")
|
SyncI18n.getString("termora.settings.sync.token")
|
||||||
}
|
}
|
||||||
|
|
||||||
val gistText = if (isWebDAV) {
|
val gistText = if (isWebDAV) {
|
||||||
I18n.getString("termora.new-host.general.password")
|
SyncI18n.getString("termora.new-host.general.password")
|
||||||
} else {
|
} else {
|
||||||
I18n.getString("termora.settings.sync.gist")
|
SyncI18n.getString("termora.settings.sync.gist")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeComboBox.selectedItem == SyncType.Gitee || isWebDAV) {
|
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(if (isWebDAV) gistTextField else tokenTextField).xy(3, rows).apply { rows += step }
|
||||||
.add("${gistText}:").xy(1, rows)
|
.add("${gistText}:").xy(1, rows)
|
||||||
.add(if (isWebDAV) tokenTextField else gistTextField).xy(3, rows).apply { rows += step }
|
.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(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 }
|
.add(rangeBox).xy(3, rows).apply { rows += step }
|
||||||
// Sync buttons
|
// Sync buttons
|
||||||
.add(
|
.add(
|
||||||
FormBuilder.create()
|
FormBuilder.create()
|
||||||
.layout(FormLayout("pref, 2dlu, pref, 2dlu, pref", "pref"))
|
.layout(FormLayout("pref", "pref"))
|
||||||
.add(syncConfigButton).xy(1, 1)
|
.add(syncConfigButton).xy(1, 1)
|
||||||
.add(exportConfigButton).xy(3, 1)
|
|
||||||
.add(importConfigButton).xy(5, 1)
|
|
||||||
.build()
|
.build()
|
||||||
).xy(3, rows, "center, fill").apply { rows += step }
|
).xy(3, rows, "center, fill").apply { rows += step }
|
||||||
.add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step }
|
.add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step }
|
||||||
|
|||||||
@@ -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<String, DeletedData>()
|
||||||
|
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<DeletedData>(e.value) }
|
||||||
|
.getOrNull() ?: continue
|
||||||
|
data[deletedData.id] = deletedData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addDeletedData(deletedData: DeletedData) {
|
||||||
|
data[deletedData.id] = deletedData
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDeletedData(): List<DeletedData> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,9 @@ abstract class GitSyncer : SafetySyncer() {
|
|||||||
|
|
||||||
// decode hosts
|
// decode hosts
|
||||||
if (config.ranges.contains(SyncRange.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 {
|
gistResponse.gists.findLast { it.filename == "Hosts" }?.let {
|
||||||
decodeHosts(it.content, deletedData.filter { e -> e.type == "Host" }, config)
|
decodeHosts(it.content, deletedData.filter { e -> e.type == "Host" }, config)
|
||||||
}
|
}
|
||||||
@@ -103,6 +106,12 @@ abstract class GitSyncer : SafetySyncer() {
|
|||||||
log.debug("Push encryptedHosts: {}", hostsContent)
|
log.debug("Push encryptedHosts: {}", hostsContent)
|
||||||
}
|
}
|
||||||
gistFiles.add(GistFile("Hosts", hostsContent))
|
gistFiles.add(GistFile("Hosts", hostsContent))
|
||||||
|
|
||||||
|
val tagsContent = encodeHosts(key)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Push encryptedTags: {}", tagsContent)
|
||||||
|
}
|
||||||
|
gistFiles.add(GistFile("Tags", tagsContent))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import app.termora.AES.encodeBase64String
|
|||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.account.AccountManager
|
import app.termora.account.AccountManager
|
||||||
import app.termora.account.AccountOwner
|
import app.termora.account.AccountOwner
|
||||||
|
import app.termora.database.DatabaseChangedExtension
|
||||||
import app.termora.database.OwnerType
|
import app.termora.database.OwnerType
|
||||||
import app.termora.highlight.KeywordHighlight
|
import app.termora.highlight.KeywordHighlight
|
||||||
import app.termora.highlight.KeywordHighlightManager
|
import app.termora.highlight.KeywordHighlightManager
|
||||||
@@ -19,6 +20,8 @@ import app.termora.macro.Macro
|
|||||||
import app.termora.macro.MacroManager
|
import app.termora.macro.MacroManager
|
||||||
import app.termora.snippet.Snippet
|
import app.termora.snippet.Snippet
|
||||||
import app.termora.snippet.SnippetManager
|
import app.termora.snippet.SnippetManager
|
||||||
|
import app.termora.tag.Tag
|
||||||
|
import app.termora.tag.TagManager
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import org.apache.commons.lang3.ArrayUtils
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -39,6 +42,7 @@ abstract class SafetySyncer : Syncer {
|
|||||||
protected val snippetManager get() = SnippetManager.getInstance()
|
protected val snippetManager get() = SnippetManager.getInstance()
|
||||||
protected val deleteDataManager get() = DeleteDataManager.getInstance()
|
protected val deleteDataManager get() = DeleteDataManager.getInstance()
|
||||||
protected val accountManager get() = AccountManager.getInstance()
|
protected val accountManager get() = AccountManager.getInstance()
|
||||||
|
protected val tagManager get() = TagManager.getInstance()
|
||||||
protected val accountOwner
|
protected val accountOwner
|
||||||
get() = AccountOwner(
|
get() = AccountOwner(
|
||||||
id = accountManager.getAccountId(),
|
id = accountManager.getAccountId(),
|
||||||
@@ -90,10 +94,11 @@ abstract class SafetySyncer : Syncer {
|
|||||||
parentId = encryptedHost.parentId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
parentId = encryptedHost.parentId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
ownerId = encryptedHost.ownerId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
ownerId = encryptedHost.ownerId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
creatorId = encryptedHost.creatorId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
creatorId = encryptedHost.creatorId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
|
||||||
|
ownerType = OwnerType.User.name,
|
||||||
createDate = encryptedHost.createDate,
|
createDate = encryptedHost.createDate,
|
||||||
updateDate = encryptedHost.updateDate,
|
updateDate = encryptedHost.updateDate,
|
||||||
)
|
)
|
||||||
SwingUtilities.invokeLater { hostManager.addHost(host) }
|
SwingUtilities.invokeLater { hostManager.addHost(host, DatabaseChangedExtension.Source.Sync) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (log.isWarnEnabled) {
|
if (log.isWarnEnabled) {
|
||||||
log.warn("Decode host: ${encryptedHost.id} failed. error: {}", e.message, e)
|
log.warn("Decode host: ${encryptedHost.id} failed. error: {}", e.message, e)
|
||||||
@@ -104,7 +109,6 @@ abstract class SafetySyncer : Syncer {
|
|||||||
SwingUtilities.invokeLater {
|
SwingUtilities.invokeLater {
|
||||||
deletedData.forEach {
|
deletedData.forEach {
|
||||||
hostManager.removeHost(it.id)
|
hostManager.removeHost(it.id)
|
||||||
deleteDataManager.removeHost(it.id, it.deleteDate)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +196,6 @@ abstract class SafetySyncer : Syncer {
|
|||||||
SwingUtilities.invokeLater {
|
SwingUtilities.invokeLater {
|
||||||
deletedData.forEach {
|
deletedData.forEach {
|
||||||
snippetManager.removeSnippet(it.id)
|
snippetManager.removeSnippet(it.id)
|
||||||
deleteDataManager.removeSnippet(it.id, it.deleteDate)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +259,6 @@ abstract class SafetySyncer : Syncer {
|
|||||||
SwingUtilities.invokeLater {
|
SwingUtilities.invokeLater {
|
||||||
deletedData.forEach {
|
deletedData.forEach {
|
||||||
keyManager.removeOhKeyPair(it.id)
|
keyManager.removeOhKeyPair(it.id)
|
||||||
deleteDataManager.removeKeyPair(it.id, it.deleteDate)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,7 +320,6 @@ abstract class SafetySyncer : Syncer {
|
|||||||
SwingUtilities.invokeLater {
|
SwingUtilities.invokeLater {
|
||||||
deletedData.forEach {
|
deletedData.forEach {
|
||||||
keywordHighlightManager.removeKeywordHighlight(it.id)
|
keywordHighlightManager.removeKeywordHighlight(it.id)
|
||||||
deleteDataManager.removeKeywordHighlight(it.id, it.deleteDate)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +330,8 @@ abstract class SafetySyncer : Syncer {
|
|||||||
|
|
||||||
protected fun encodeKeywordHighlights(key: ByteArray): String {
|
protected fun encodeKeywordHighlights(key: ByteArray): String {
|
||||||
val keywordHighlights = mutableListOf<KeywordHighlight>()
|
val keywordHighlights = mutableListOf<KeywordHighlight>()
|
||||||
for (keywordHighlight in keywordHighlightManager.getKeywordHighlights()) {
|
for (ownerId in accountManager.getOwnerIds()) {
|
||||||
|
for (keywordHighlight in keywordHighlightManager.getKeywordHighlights(ownerId)) {
|
||||||
// aes iv
|
// aes iv
|
||||||
val iv = getIv(keywordHighlight.id)
|
val iv = getIv(keywordHighlight.id)
|
||||||
val encryptedKeyPair = keywordHighlight.copy(
|
val encryptedKeyPair = keywordHighlight.copy(
|
||||||
@@ -338,9 +340,66 @@ abstract class SafetySyncer : Syncer {
|
|||||||
)
|
)
|
||||||
keywordHighlights.add(encryptedKeyPair)
|
keywordHighlights.add(encryptedKeyPair)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return ohMyJson.encodeToString(keywordHighlights)
|
return ohMyJson.encodeToString(keywordHighlights)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected fun decodeTags(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
||||||
|
// aes key
|
||||||
|
val key = getKey(config)
|
||||||
|
val encryptedTags = runCatching { ohMyJson.decodeFromString<List<Tag>>(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<Tag>()
|
||||||
|
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<DeletedData>, config: SyncConfig) {
|
protected fun decodeMacros(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
|
||||||
// aes key
|
// aes key
|
||||||
val key = getKey(config)
|
val key = getKey(config)
|
||||||
@@ -373,7 +432,6 @@ abstract class SafetySyncer : Syncer {
|
|||||||
SwingUtilities.invokeLater {
|
SwingUtilities.invokeLater {
|
||||||
deletedData.forEach {
|
deletedData.forEach {
|
||||||
macroManager.removeMacro(it.id)
|
macroManager.removeMacro(it.id)
|
||||||
deleteDataManager.removeMacro(it.id, it.deleteDate)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,7 +471,6 @@ abstract class SafetySyncer : Syncer {
|
|||||||
SwingUtilities.invokeLater {
|
SwingUtilities.invokeLater {
|
||||||
deletedData.forEach {
|
deletedData.forEach {
|
||||||
keymapManager.removeKeymap(it.id)
|
keymapManager.removeKeymap(it.id)
|
||||||
deleteDataManager.removeKeymap(it.id, it.deleteDate)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package app.termora.plugins.sync
|
|||||||
|
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
import app.termora.Disposable
|
import app.termora.Disposable
|
||||||
|
import app.termora.account.AccountManager
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
@@ -21,6 +22,7 @@ class SyncManager private constructor() : Disposable {
|
|||||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
private var disableTrigger = false
|
private var disableTrigger = false
|
||||||
|
private val accountManager get() = AccountManager.getInstance()
|
||||||
|
|
||||||
|
|
||||||
private fun trigger() {
|
private fun trigger() {
|
||||||
@@ -38,6 +40,10 @@ class SyncManager private constructor() : Disposable {
|
|||||||
|
|
||||||
job?.cancel()
|
job?.cancel()
|
||||||
|
|
||||||
|
if (accountManager.isLocally().not()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (log.isInfoEnabled) {
|
if (log.isInfoEnabled) {
|
||||||
log.info("Automatic synchronisation is interrupted")
|
log.info("Automatic synchronisation is interrupted")
|
||||||
}
|
}
|
||||||
@@ -124,6 +130,10 @@ class SyncManager private constructor() : Disposable {
|
|||||||
|
|
||||||
|
|
||||||
fun pull(config: SyncConfig): GistResponse {
|
fun pull(config: SyncConfig): GistResponse {
|
||||||
|
if (accountManager.isLocally().not()) {
|
||||||
|
throw IllegalStateException(SyncI18n.getString("termora.plugins.sync.disabled-sync"))
|
||||||
|
}
|
||||||
|
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
disableTrigger = true
|
disableTrigger = true
|
||||||
try {
|
try {
|
||||||
@@ -135,6 +145,10 @@ class SyncManager private constructor() : Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun push(config: SyncConfig): GistResponse {
|
fun push(config: SyncConfig): GistResponse {
|
||||||
|
if (accountManager.isLocally().not()) {
|
||||||
|
throw IllegalStateException(SyncI18n.getString("termora.plugins.sync.disabled-sync"))
|
||||||
|
}
|
||||||
|
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
try {
|
try {
|
||||||
disableTrigger = true
|
disableTrigger = true
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class SyncPlugin : Plugin {
|
|||||||
init {
|
init {
|
||||||
support.addExtension(SettingsOptionExtension::class.java) { SyncSettingsOptionExtension.instance }
|
support.addExtension(SettingsOptionExtension::class.java) { SyncSettingsOptionExtension.instance }
|
||||||
support.addExtension(DatabaseChangedExtension::class.java) { SyncDatabaseChangedExtension.instance }
|
support.addExtension(DatabaseChangedExtension::class.java) { SyncDatabaseChangedExtension.instance }
|
||||||
|
support.addExtension(DatabaseChangedExtension::class.java) { DeleteDataManager.getInstance() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAuthor(): String {
|
override fun getAuthor(): String {
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ class WebDAVSyncer private constructor() : SafetySyncer() {
|
|||||||
|
|
||||||
// decode hosts
|
// decode hosts
|
||||||
if (config.ranges.contains(SyncRange.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 {
|
json["Hosts"]?.jsonPrimitive?.content?.let {
|
||||||
decodeHosts(it, deletedData.filter { e -> e.type == "Host" }, config)
|
decodeHosts(it, deletedData.filter { e -> e.type == "Host" }, config)
|
||||||
}
|
}
|
||||||
@@ -98,6 +101,12 @@ class WebDAVSyncer private constructor() : SafetySyncer() {
|
|||||||
log.debug("Push encryptedHosts: {}", hostsContent)
|
log.debug("Push encryptedHosts: {}", hostsContent)
|
||||||
}
|
}
|
||||||
put("Hosts", hostsContent)
|
put("Hosts", hostsContent)
|
||||||
|
|
||||||
|
val tagsContent = encodeTags(key)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Push encryptedTags: {}", tagsContent)
|
||||||
|
}
|
||||||
|
put("Tags", tagsContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snippets
|
// Snippets
|
||||||
|
|||||||
22
plugins/sync/src/main/resources/i18n/messages.properties
Normal file
22
plugins/sync/src/main/resources/i18n/messages.properties
Normal file
@@ -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
|
||||||
@@ -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=数据变动时
|
||||||
@@ -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=資料變動時
|
||||||
@@ -8,9 +8,9 @@ import java.util.*
|
|||||||
abstract class AbstractI18n {
|
abstract class AbstractI18n {
|
||||||
private val log get() = getLogger()
|
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)
|
val text = getString(key)
|
||||||
if (args.isNotEmpty()) {
|
if (args.isNotEmpty()) {
|
||||||
return MessageFormat.format(text, *args)
|
return MessageFormat.format(text, *args)
|
||||||
@@ -19,7 +19,7 @@ abstract class AbstractI18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getString(key: String): String {
|
open fun getString(key: String): String {
|
||||||
try {
|
try {
|
||||||
return substitutor.replace(getBundle().getString(key))
|
return substitutor.replace(getBundle().getString(key))
|
||||||
} catch (e: MissingResourceException) {
|
} catch (e: MissingResourceException) {
|
||||||
|
|||||||
@@ -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<String, DeletedData>()
|
|
||||||
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<DeletedData> {
|
|
||||||
if (data.isEmpty()) {
|
|
||||||
// TODO data.putAll(database.getDeletedData().associateBy { it.id })
|
|
||||||
}
|
|
||||||
return data.values.sortedBy { it.deleteDate }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ package app.termora.account
|
|||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
|
import app.termora.database.OwnerType
|
||||||
import app.termora.plugin.ExtensionManager
|
import app.termora.plugin.ExtensionManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -47,6 +48,10 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
|
|||||||
fun getAccessToken() = account.accessToken
|
fun getAccessToken() = account.accessToken
|
||||||
fun getRefreshToken() = account.refreshToken
|
fun getRefreshToken() = account.refreshToken
|
||||||
fun getOwnerIds() = account.teams.map { it.id }.toMutableList().apply { add(getAccountId()) }.toSet()
|
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 {
|
fun isFreePlan(): Boolean {
|
||||||
return isLocally() || getSubscription().plan == SubscriptionPlan.Free
|
return isLocally() || getSubscription().plan == SubscriptionPlan.Free
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
source
|
source
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
save(data)
|
save(data, source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package app.termora.highlight
|
|||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
import app.termora.DeleteDataManager
|
|
||||||
import app.termora.TerminalPanelFactory
|
import app.termora.TerminalPanelFactory
|
||||||
import app.termora.account.AccountOwner
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.database.Data
|
import app.termora.database.Data
|
||||||
@@ -48,7 +47,6 @@ class KeywordHighlightManager private constructor() {
|
|||||||
fun removeKeywordHighlight(id: String) {
|
fun removeKeywordHighlight(id: String) {
|
||||||
database.delete(id, DataType.KeywordHighlight.name)
|
database.delete(id, DataType.KeywordHighlight.name)
|
||||||
TerminalPanelFactory.getInstance().repaintAll()
|
TerminalPanelFactory.getInstance().repaintAll()
|
||||||
DeleteDataManager.getInstance().removeKeywordHighlight(id)
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Keyword highlighter removed. {}", id)
|
log.debug("Keyword highlighter removed. {}", id)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package app.termora.keymgr
|
|||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
import app.termora.DeleteDataManager
|
|
||||||
import app.termora.account.AccountOwner
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.database.Data
|
import app.termora.database.Data
|
||||||
import app.termora.database.DataType
|
import app.termora.database.DataType
|
||||||
@@ -36,7 +35,6 @@ class KeyManager private constructor() {
|
|||||||
|
|
||||||
fun removeOhKeyPair(id: String) {
|
fun removeOhKeyPair(id: String) {
|
||||||
databaseManager.delete(id, DataType.KeyPair.name)
|
databaseManager.delete(id, DataType.KeyPair.name)
|
||||||
DeleteDataManager.getInstance().removeKeyPair(id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOhKeyPairs(): List<OhKeyPair> {
|
fun getOhKeyPairs(): List<OhKeyPair> {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package app.termora.macro
|
|||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
import app.termora.DeleteDataManager
|
|
||||||
import app.termora.account.AccountManager
|
import app.termora.account.AccountManager
|
||||||
import app.termora.database.Data
|
import app.termora.database.Data
|
||||||
import app.termora.database.DataType
|
import app.termora.database.DataType
|
||||||
@@ -50,7 +49,6 @@ class MacroManager private constructor() {
|
|||||||
|
|
||||||
fun removeMacro(id: String) {
|
fun removeMacro(id: String) {
|
||||||
database.delete(id, DataType.Macro.name)
|
database.delete(id, DataType.Macro.name)
|
||||||
DeleteDataManager.getInstance().removeMacro(id)
|
|
||||||
|
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Removed macro $id")
|
log.debug("Removed macro $id")
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package app.termora.snippet
|
|||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.ApplicationScope
|
import app.termora.ApplicationScope
|
||||||
import app.termora.DeleteDataManager
|
|
||||||
import app.termora.account.AccountManager
|
import app.termora.account.AccountManager
|
||||||
import app.termora.assertEventDispatchThread
|
import app.termora.assertEventDispatchThread
|
||||||
import app.termora.database.Data
|
import app.termora.database.Data
|
||||||
@@ -45,7 +44,6 @@ class SnippetManager private constructor() {
|
|||||||
|
|
||||||
fun removeSnippet(id: String) {
|
fun removeSnippet(id: String) {
|
||||||
database.delete(id, DataType.Snippet.name)
|
database.delete(id, DataType.Snippet.name)
|
||||||
DeleteDataManager.getInstance().removeSnippet(id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ import kotlinx.serialization.Serializable
|
|||||||
data class Tag(
|
data class Tag(
|
||||||
val id: String,
|
val id: String,
|
||||||
val text: String,
|
val text: String,
|
||||||
val createDate: Long = System.currentTimeMillis(),
|
val createDate: Long,
|
||||||
val updateDate: Long = System.currentTimeMillis(),
|
val updateDate: Long,
|
||||||
)
|
)
|
||||||
@@ -63,7 +63,14 @@ class TagPanel(accountOwner: AccountOwner) : JPanel(BorderLayout()), Disposable
|
|||||||
title = I18n.getString("termora.tag"),
|
title = I18n.getString("termora.tag"),
|
||||||
)
|
)
|
||||||
if (text.isNullOrBlank().not()) {
|
if (text.isNullOrBlank().not()) {
|
||||||
model.addElement(Tag(id = randomUUID(), text = text))
|
model.addElement(
|
||||||
|
Tag(
|
||||||
|
id = randomUUID(),
|
||||||
|
text = text,
|
||||||
|
createDate = System.currentTimeMillis(),
|
||||||
|
updateDate = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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=Auto Close Tab
|
||||||
termora.settings.terminal.auto-close-tab-description=Automatically close the tab when the terminal is disconnected normally
|
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=About
|
||||||
termora.settings.about.author=Author
|
termora.settings.about.author=Author
|
||||||
|
|||||||
@@ -81,24 +81,6 @@ termora.settings.terminal.auto-close-tab=自动关闭标签
|
|||||||
termora.settings.terminal.auto-close-tab-description=当终端正常断开连接时自动关闭标签页
|
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=关于
|
||||||
termora.settings.about.author=作者
|
termora.settings.about.author=作者
|
||||||
termora.settings.about.source=源代码
|
termora.settings.about.source=源代码
|
||||||
|
|||||||
@@ -92,23 +92,6 @@ termora.settings.terminal.floating-toolbar=懸浮工具列
|
|||||||
termora.settings.terminal.auto-close-tab=自動關閉標籤
|
termora.settings.terminal.auto-close-tab=自動關閉標籤
|
||||||
termora.settings.terminal.auto-close-tab-description=當終端正常斷開連線時自動關閉標籤頁
|
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=關於
|
||||||
termora.settings.about.author=作者
|
termora.settings.about.author=作者
|
||||||
|
|||||||
Reference in New Issue
Block a user