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