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 {
|
||||
|
||||
@@ -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<SyncPolicy>()
|
||||
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<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 {
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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<KeywordHighlight>()
|
||||
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<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) {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
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 {
|
||||
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) {
|
||||
|
||||
@@ -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.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
|
||||
|
||||
@@ -252,7 +252,7 @@ class DatabaseManager private constructor() : Disposable {
|
||||
source
|
||||
)
|
||||
} else {
|
||||
save(data)
|
||||
save(data, source)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<OhKeyPair> {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=源代码
|
||||
|
||||
@@ -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=作者
|
||||
|
||||
Reference in New Issue
Block a user