mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: support keymap sync
This commit is contained in:
@@ -2,7 +2,6 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.highlight.KeywordHighlight
|
import app.termora.highlight.KeywordHighlight
|
||||||
import app.termora.keymap.KeyShortcut
|
|
||||||
import app.termora.keymap.Keymap
|
import app.termora.keymap.Keymap
|
||||||
import app.termora.keymgr.OhKeyPair
|
import app.termora.keymgr.OhKeyPair
|
||||||
import app.termora.macro.Macro
|
import app.termora.macro.Macro
|
||||||
@@ -14,12 +13,10 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.*
|
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.swing.KeyStroke
|
|
||||||
import kotlin.collections.component1
|
import kotlin.collections.component1
|
||||||
import kotlin.collections.component2
|
import kotlin.collections.component2
|
||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
@@ -65,36 +62,17 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
|
|
||||||
fun getKeymaps(): Collection<Keymap> {
|
fun getKeymaps(): Collection<Keymap> {
|
||||||
val array = env.computeInTransaction { tx ->
|
val array = env.computeInTransaction { tx ->
|
||||||
openCursor<JsonObject>(tx, KEYMAP_STORE) { _, value ->
|
openCursor<String>(tx, KEYMAP_STORE) { _, value ->
|
||||||
ohMyJson.decodeFromString<JsonObject>(value)
|
value
|
||||||
}.values
|
}.values
|
||||||
}
|
}
|
||||||
|
|
||||||
val shortcuts = mutableListOf<Keymap>()
|
val keymaps = mutableListOf<Keymap>()
|
||||||
for (json in array.iterator()) {
|
for (text in array.iterator()) {
|
||||||
val name = json["name"]?.jsonPrimitive?.content ?: continue
|
keymaps.add(Keymap.fromJSON(text) ?: continue)
|
||||||
val readonly = json["readonly"]?.jsonPrimitive?.booleanOrNull ?: false
|
|
||||||
val keymap = Keymap(name, null, readonly)
|
|
||||||
|
|
||||||
for (shortcut in (json["shortcuts"]?.jsonArray ?: emptyList()).map { it.jsonObject }) {
|
|
||||||
val keyStroke = shortcut["keyStroke"]?.jsonPrimitive?.contentOrNull ?: continue
|
|
||||||
val keyboard = shortcut["keyboard"]?.jsonPrimitive?.booleanOrNull ?: true
|
|
||||||
val actionIds = ohMyJson.decodeFromJsonElement<List<String>>(
|
|
||||||
shortcut["actionIds"]?.jsonArray
|
|
||||||
?: continue
|
|
||||||
)
|
|
||||||
if (keyboard) {
|
|
||||||
val keyShortcut = KeyShortcut(KeyStroke.getKeyStroke(keyStroke))
|
|
||||||
for (actionId in actionIds) {
|
|
||||||
keymap.addShortcut(actionId, keyShortcut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shortcuts.add(keymap)
|
return keymaps
|
||||||
}
|
|
||||||
|
|
||||||
return shortcuts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addKeymap(keymap: Keymap) {
|
fun addKeymap(keymap: Keymap) {
|
||||||
@@ -599,6 +577,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
var rangeKeyPairs by BooleanPropertyDelegate(true)
|
var rangeKeyPairs by BooleanPropertyDelegate(true)
|
||||||
var rangeKeywordHighlights by BooleanPropertyDelegate(true)
|
var rangeKeywordHighlights by BooleanPropertyDelegate(true)
|
||||||
var rangeMacros by BooleanPropertyDelegate(true)
|
var rangeMacros by BooleanPropertyDelegate(true)
|
||||||
|
var rangeKeymap by BooleanPropertyDelegate(true)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token
|
* Token
|
||||||
|
|||||||
@@ -402,6 +402,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys"))
|
val keysCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keys"))
|
||||||
val keywordHighlightsCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keyword-highlights"))
|
val keywordHighlightsCheckBox = JCheckBox(I18n.getString("termora.settings.sync.range.keyword-highlights"))
|
||||||
val macrosCheckBox = JCheckBox(I18n.getString("termora.macro"))
|
val macrosCheckBox = JCheckBox(I18n.getString("termora.macro"))
|
||||||
|
val keymapCheckBox = JCheckBox(I18n.getString("termora.settings.keymap"))
|
||||||
val visitGistBtn = JButton(Icons.externalLink)
|
val visitGistBtn = JButton(Icons.externalLink)
|
||||||
val getTokenBtn = JButton(Icons.externalLink)
|
val getTokenBtn = JButton(Icons.externalLink)
|
||||||
|
|
||||||
@@ -593,6 +594,9 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
if (macrosCheckBox.isSelected) {
|
if (macrosCheckBox.isSelected) {
|
||||||
range.add(SyncRange.Macros)
|
range.add(SyncRange.Macros)
|
||||||
}
|
}
|
||||||
|
if (keymapCheckBox.isSelected) {
|
||||||
|
range.add(SyncRange.Keymap)
|
||||||
|
}
|
||||||
return SyncConfig(
|
return SyncConfig(
|
||||||
type = typeComboBox.selectedItem as SyncType,
|
type = typeComboBox.selectedItem as SyncType,
|
||||||
token = String(tokenTextField.password),
|
token = String(tokenTextField.password),
|
||||||
@@ -664,6 +668,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
tokenTextField.isEnabled = false
|
tokenTextField.isEnabled = false
|
||||||
keysCheckBox.isEnabled = false
|
keysCheckBox.isEnabled = false
|
||||||
macrosCheckBox.isEnabled = false
|
macrosCheckBox.isEnabled = false
|
||||||
|
keymapCheckBox.isEnabled = false
|
||||||
keywordHighlightsCheckBox.isEnabled = false
|
keywordHighlightsCheckBox.isEnabled = false
|
||||||
hostsCheckBox.isEnabled = false
|
hostsCheckBox.isEnabled = false
|
||||||
domainTextField.isEnabled = false
|
domainTextField.isEnabled = false
|
||||||
@@ -696,6 +701,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
hostsCheckBox.isEnabled = true
|
hostsCheckBox.isEnabled = true
|
||||||
typeComboBox.isEnabled = true
|
typeComboBox.isEnabled = true
|
||||||
macrosCheckBox.isEnabled = true
|
macrosCheckBox.isEnabled = true
|
||||||
|
keymapCheckBox.isEnabled = true
|
||||||
gistTextField.isEnabled = true
|
gistTextField.isEnabled = true
|
||||||
tokenTextField.isEnabled = true
|
tokenTextField.isEnabled = true
|
||||||
domainTextField.isEnabled = true
|
domainTextField.isEnabled = true
|
||||||
@@ -756,11 +762,13 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
keysCheckBox.isFocusable = false
|
keysCheckBox.isFocusable = false
|
||||||
keywordHighlightsCheckBox.isFocusable = false
|
keywordHighlightsCheckBox.isFocusable = false
|
||||||
macrosCheckBox.isFocusable = false
|
macrosCheckBox.isFocusable = false
|
||||||
|
keymapCheckBox.isFocusable = false
|
||||||
|
|
||||||
hostsCheckBox.isSelected = sync.rangeHosts
|
hostsCheckBox.isSelected = sync.rangeHosts
|
||||||
keysCheckBox.isSelected = sync.rangeKeyPairs
|
keysCheckBox.isSelected = sync.rangeKeyPairs
|
||||||
keywordHighlightsCheckBox.isSelected = sync.rangeKeywordHighlights
|
keywordHighlightsCheckBox.isSelected = sync.rangeKeywordHighlights
|
||||||
macrosCheckBox.isSelected = sync.rangeMacros
|
macrosCheckBox.isSelected = sync.rangeMacros
|
||||||
|
keymapCheckBox.isSelected = sync.rangeKeymap
|
||||||
|
|
||||||
typeComboBox.selectedItem = sync.type
|
typeComboBox.selectedItem = sync.type
|
||||||
gistTextField.text = sync.gist
|
gistTextField.text = sync.gist
|
||||||
@@ -823,6 +831,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
.add(keysCheckBox).xy(3, 1)
|
.add(keysCheckBox).xy(3, 1)
|
||||||
.add(keywordHighlightsCheckBox).xy(5, 1)
|
.add(keywordHighlightsCheckBox).xy(5, 1)
|
||||||
.add(macrosCheckBox).xy(1, 3)
|
.add(macrosCheckBox).xy(1, 3)
|
||||||
|
.add(keymapCheckBox).xy(3, 3)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
var rows = 1
|
var rows = 1
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
|
import app.termora.ApplicationScope
|
||||||
import app.termora.I18n
|
import app.termora.I18n
|
||||||
import app.termora.Icons
|
import app.termora.Icons
|
||||||
import app.termora.SettingsDialog
|
import app.termora.SettingsDialog
|
||||||
@@ -27,7 +28,8 @@ class SettingsAction : AnAction(
|
|||||||
init {
|
init {
|
||||||
FlatDesktop.setPreferencesHandler {
|
FlatDesktop.setPreferencesHandler {
|
||||||
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||||
if (owner != null) {
|
// Doorman 的情况下不允许打开
|
||||||
|
if (owner != null && ApplicationScope.windowScopes().isNotEmpty()) {
|
||||||
actionPerformed(ActionEvent(owner, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
actionPerformed(ActionEvent(owner, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package app.termora.keymap
|
|||||||
|
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.buildJsonArray
|
import kotlinx.serialization.json.*
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import javax.swing.KeyStroke
|
||||||
import kotlinx.serialization.json.encodeToJsonElement
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
|
|
||||||
open class Keymap(
|
open class Keymap(
|
||||||
val name: String,
|
val name: String,
|
||||||
@@ -16,6 +14,37 @@ open class Keymap(
|
|||||||
val isReadonly: Boolean = false,
|
val isReadonly: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromJSON(text: String): Keymap? {
|
||||||
|
return fromJSON(ohMyJson.decodeFromString<JsonObject>(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromJSON(json: JsonObject): Keymap? {
|
||||||
|
val shortcuts = mutableListOf<Keymap>()
|
||||||
|
val name = json["name"]?.jsonPrimitive?.content ?: return null
|
||||||
|
val readonly = json["readonly"]?.jsonPrimitive?.booleanOrNull ?: return null
|
||||||
|
val keymap = Keymap(name, null, readonly)
|
||||||
|
|
||||||
|
for (shortcut in (json["shortcuts"]?.jsonArray ?: emptyList()).map { it.jsonObject }) {
|
||||||
|
val keyStroke = shortcut["keyStroke"]?.jsonPrimitive?.contentOrNull ?: continue
|
||||||
|
val keyboard = shortcut["keyboard"]?.jsonPrimitive?.booleanOrNull ?: true
|
||||||
|
val actionIds = ohMyJson.decodeFromJsonElement<List<String>>(
|
||||||
|
shortcut["actionIds"]?.jsonArray
|
||||||
|
?: continue
|
||||||
|
)
|
||||||
|
if (keyboard) {
|
||||||
|
val keyShortcut = KeyShortcut(KeyStroke.getKeyStroke(keyStroke))
|
||||||
|
for (actionId in actionIds) {
|
||||||
|
keymap.addShortcut(actionId, keyShortcut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shortcuts.add(keymap)
|
||||||
|
return keymap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val shortcuts = mutableMapOf<Shortcut, MutableList<String>>()
|
private val shortcuts = mutableMapOf<Shortcut, MutableList<String>>()
|
||||||
|
|
||||||
open fun addShortcut(actionId: String, shortcut: Shortcut) {
|
open fun addShortcut(actionId: String, shortcut: Shortcut) {
|
||||||
@@ -66,7 +95,11 @@ open class Keymap(
|
|||||||
|
|
||||||
|
|
||||||
fun toJSON(): String {
|
fun toJSON(): String {
|
||||||
return ohMyJson.encodeToString(buildJsonObject {
|
return ohMyJson.encodeToString(toJSONObject())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJSONObject(): JsonObject {
|
||||||
|
return buildJsonObject {
|
||||||
put("name", name)
|
put("name", name)
|
||||||
put("readonly", isReadonly)
|
put("readonly", isReadonly)
|
||||||
parent?.let { put("parent", it.name) }
|
parent?.let { put("parent", it.name) }
|
||||||
@@ -81,7 +114,7 @@ open class Keymap(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -8,11 +8,14 @@ import app.termora.AES.encodeBase64String
|
|||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.highlight.KeywordHighlight
|
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.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
import app.termora.keymgr.OhKeyPair
|
import app.termora.keymgr.OhKeyPair
|
||||||
import app.termora.macro.Macro
|
import app.termora.macro.Macro
|
||||||
import app.termora.macro.MacroManager
|
import app.termora.macro.MacroManager
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
@@ -32,6 +35,7 @@ abstract class GitSyncer : Syncer {
|
|||||||
protected val keyManager get() = KeyManager.getInstance()
|
protected val keyManager get() = KeyManager.getInstance()
|
||||||
protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance()
|
||||||
protected val macroManager get() = MacroManager.getInstance()
|
protected val macroManager get() = MacroManager.getInstance()
|
||||||
|
protected val keymapManager get() = KeymapManager.getInstance()
|
||||||
|
|
||||||
override fun pull(config: SyncConfig): GistResponse {
|
override fun pull(config: SyncConfig): GistResponse {
|
||||||
|
|
||||||
@@ -74,6 +78,13 @@ abstract class GitSyncer : Syncer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decode keymaps
|
||||||
|
if (config.ranges.contains(SyncRange.Macros)) {
|
||||||
|
gistResponse.gists.findLast { it.filename == "Keymaps" }?.let {
|
||||||
|
decodeKeymaps(it.content, config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (log.isInfoEnabled) {
|
if (log.isInfoEnabled) {
|
||||||
log.info("Type: ${config.type} , Gist: ${config.gistId} Pulled")
|
log.info("Type: ${config.type} , Gist: ${config.gistId} Pulled")
|
||||||
}
|
}
|
||||||
@@ -231,6 +242,17 @@ abstract class GitSyncer : Syncer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun decodeKeymaps(text: String, config: SyncConfig) {
|
||||||
|
|
||||||
|
for (keymap in ohMyJson.decodeFromString<List<JsonObject>>(text).mapNotNull { Keymap.fromJSON(it) }) {
|
||||||
|
keymapManager.addKeymap(keymap)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Decode Keymaps: {}", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getKey(config: SyncConfig): ByteArray {
|
private fun getKey(config: SyncConfig): ByteArray {
|
||||||
return ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16)
|
return ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16)
|
||||||
}
|
}
|
||||||
@@ -244,6 +266,7 @@ abstract class GitSyncer : Syncer {
|
|||||||
// aes key
|
// aes key
|
||||||
val key = ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16)
|
val key = ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16)
|
||||||
|
|
||||||
|
// Hosts
|
||||||
if (config.ranges.contains(SyncRange.Hosts)) {
|
if (config.ranges.contains(SyncRange.Hosts)) {
|
||||||
val encryptedHosts = mutableListOf<EncryptedHost>()
|
val encryptedHosts = mutableListOf<EncryptedHost>()
|
||||||
for (host in hostManager.hosts()) {
|
for (host in hostManager.hosts()) {
|
||||||
@@ -282,6 +305,7 @@ abstract class GitSyncer : Syncer {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KeyPairs
|
||||||
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
if (config.ranges.contains(SyncRange.KeyPairs)) {
|
||||||
val encryptedKeys = mutableListOf<OhKeyPair>()
|
val encryptedKeys = mutableListOf<OhKeyPair>()
|
||||||
for (keyPair in keyManager.getOhKeyPairs()) {
|
for (keyPair in keyManager.getOhKeyPairs()) {
|
||||||
@@ -306,6 +330,7 @@ abstract class GitSyncer : Syncer {
|
|||||||
gistFiles.add(GistFile("KeyPairs", keysContent))
|
gistFiles.add(GistFile("KeyPairs", keysContent))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Highlights
|
||||||
if (config.ranges.contains(SyncRange.KeywordHighlights)) {
|
if (config.ranges.contains(SyncRange.KeywordHighlights)) {
|
||||||
val keywordHighlights = mutableListOf<KeywordHighlight>()
|
val keywordHighlights = mutableListOf<KeywordHighlight>()
|
||||||
for (keywordHighlight in keywordHighlightManager.getKeywordHighlights()) {
|
for (keywordHighlight in keywordHighlightManager.getKeywordHighlights()) {
|
||||||
@@ -324,6 +349,7 @@ abstract class GitSyncer : Syncer {
|
|||||||
gistFiles.add(GistFile("KeywordHighlights", keywordHighlightsContent))
|
gistFiles.add(GistFile("KeywordHighlights", keywordHighlightsContent))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Macros
|
||||||
if (config.ranges.contains(SyncRange.Macros)) {
|
if (config.ranges.contains(SyncRange.Macros)) {
|
||||||
val macros = mutableListOf<Macro>()
|
val macros = mutableListOf<Macro>()
|
||||||
for (macro in macroManager.getMacros()) {
|
for (macro in macroManager.getMacros()) {
|
||||||
@@ -342,6 +368,26 @@ abstract class GitSyncer : Syncer {
|
|||||||
gistFiles.add(GistFile("Macros", macrosContent))
|
gistFiles.add(GistFile("Macros", macrosContent))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keymap
|
||||||
|
if (config.ranges.contains(SyncRange.Keymap)) {
|
||||||
|
val keymaps = mutableListOf<JsonObject>()
|
||||||
|
for (keymap in keymapManager.getKeymaps()) {
|
||||||
|
// 只读的是内置的
|
||||||
|
if (keymap.isReadonly) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keymaps.add(keymap.toJSONObject())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keymaps.isNotEmpty()) {
|
||||||
|
val keymapsContent = ohMyJson.encodeToString(keymaps)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Push keymaps: {}", keymapsContent)
|
||||||
|
}
|
||||||
|
gistFiles.add(GistFile("Keymaps", keymapsContent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (gistFiles.isEmpty()) {
|
if (gistFiles.isEmpty()) {
|
||||||
throw IllegalArgumentException("No gist files found")
|
throw IllegalArgumentException("No gist files found")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ enum class SyncRange {
|
|||||||
KeyPairs,
|
KeyPairs,
|
||||||
KeywordHighlights,
|
KeywordHighlights,
|
||||||
Macros,
|
Macros,
|
||||||
|
Keymap,
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SyncConfig(
|
data class SyncConfig(
|
||||||
|
|||||||
Reference in New Issue
Block a user