mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
89 Commits
2.0.0-beta
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e9111db23 | ||
|
|
3ece32e427 | ||
|
|
dad4d26fd8 | ||
|
|
9b387c71fc | ||
|
|
7ac833b53b | ||
|
|
2327c5fd48 | ||
|
|
bfc63a3983 | ||
|
|
c727925791 | ||
|
|
cae1173180 | ||
|
|
10d2736232 | ||
|
|
97f01b7e3f | ||
|
|
21c7dd7a42 | ||
|
|
bbc64043ed | ||
|
|
79842f4625 | ||
|
|
626b344088 | ||
|
|
5b165ed587 | ||
|
|
d73b3b706e | ||
|
|
2928b35585 | ||
|
|
04bece21ff | ||
|
|
9e2e104baa | ||
|
|
0615378a17 | ||
|
|
013b03f9ef | ||
|
|
026b13ba05 | ||
|
|
6ec526eeeb | ||
|
|
e064bb9bb5 | ||
|
|
1f3fb5e2c0 | ||
|
|
5984f3e856 | ||
|
|
572c381e90 | ||
|
|
7a8ecb06bf | ||
|
|
4c928ac826 | ||
|
|
d07f9ede8c | ||
|
|
21a015bf8c | ||
|
|
71a1f5db4b | ||
|
|
96fd07a6ff | ||
|
|
733e062a7b | ||
|
|
e87a779adc | ||
|
|
9c6aa4dcb6 | ||
|
|
566b087eb1 | ||
|
|
2235e4c2a4 | ||
|
|
3b9d1f277b | ||
|
|
5110595404 | ||
|
|
034e0939be | ||
|
|
4ccfa82c8a | ||
|
|
d21ae5499a | ||
|
|
d80a9d48ab | ||
|
|
b305d6fd34 | ||
|
|
756fd305d1 | ||
|
|
f9549fbb7d | ||
|
|
e18b454fcc | ||
|
|
4f4ccfa7d4 | ||
|
|
5dfd5fefb2 | ||
|
|
a7ea4c70d2 | ||
|
|
b7796f58f0 | ||
|
|
c7bedc57e0 | ||
|
|
935f305ada | ||
|
|
8cf47a7ca1 | ||
|
|
c6c5ad711d | ||
|
|
5fc76d955a | ||
|
|
0aabe1b0dc | ||
|
|
820c4274e7 | ||
|
|
fcec30d70a | ||
|
|
f6dc0098f7 | ||
|
|
ca7b30bdb0 | ||
|
|
f73e7f4214 | ||
|
|
613a1ca78a | ||
|
|
bf9e3ea2e2 | ||
|
|
a4390c4c6d | ||
|
|
9cf317e245 | ||
|
|
d000d73122 | ||
|
|
88613ed2f6 | ||
|
|
2fc381caa5 | ||
|
|
30e245f7a3 | ||
|
|
35cf92e685 | ||
|
|
522ee44ca2 | ||
|
|
5cf03e1f1f | ||
|
|
afca4ddf0e | ||
|
|
ca757f975a | ||
|
|
79c304ae3d | ||
|
|
1848c869e7 | ||
|
|
029e570551 | ||
|
|
905c570e4c | ||
|
|
a3069229b8 | ||
|
|
1e930d61c9 | ||
|
|
0015c3a7fb | ||
|
|
4bfb87e5c7 | ||
|
|
4fbb626c42 | ||
|
|
35b175d944 | ||
|
|
5939297550 | ||
|
|
e6e5867742 |
4
.github/workflows/linux.yml
vendored
4
.github/workflows/linux.yml
vendored
@@ -3,8 +3,8 @@ name: Linux
|
|||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
JBR_MAJOR: 21.0.7
|
JBR_MAJOR: 21.0.8
|
||||||
JBR_PATCH: b1038.58
|
JBR_PATCH: b1163.69
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
6
.github/workflows/osx.yml
vendored
6
.github/workflows/osx.yml
vendored
@@ -8,15 +8,15 @@ env:
|
|||||||
# 只有发布版本时才需要公证
|
# 只有发布版本时才需要公证
|
||||||
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||||
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||||
JBR_MAJOR: 21.0.7
|
JBR_MAJOR: 21.0.8
|
||||||
JBR_PATCH: b1038.58
|
JBR_PATCH: b1163.69
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos-15, macos-13 ]
|
os: [ macos-15-intel, macos-latest ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
4
.github/workflows/windows.yml
vendored
4
.github/workflows/windows.yml
vendored
@@ -3,8 +3,8 @@ name: Windows
|
|||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
JBR_MAJOR: 21.0.7
|
JBR_MAJOR: 21.0.8
|
||||||
JBR_PATCH: b1038.58
|
JBR_PATCH: b1163.69
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -339,6 +339,7 @@ tasks.register<Exec>("jlink") {
|
|||||||
"java.security.jgss",
|
"java.security.jgss",
|
||||||
"jdk.crypto.ec",
|
"jdk.crypto.ec",
|
||||||
"jdk.unsupported",
|
"jdk.unsupported",
|
||||||
|
"jdk.httpserver",
|
||||||
)
|
)
|
||||||
|
|
||||||
commandLine(
|
commandLine(
|
||||||
@@ -404,18 +405,6 @@ tasks.register<Exec>("jpackage") {
|
|||||||
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
||||||
arguments.addAll(listOf("--app-content", "$buildDir/plugins"))
|
arguments.addAll(listOf("--app-content", "$buildDir/plugins"))
|
||||||
|
|
||||||
if (os.isWindows) {
|
|
||||||
arguments.addAll(
|
|
||||||
listOf(
|
|
||||||
"--description",
|
|
||||||
"${project.name.uppercaseFirstChar()}: A terminal emulator and SSH client"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
arguments.addAll(listOf("--description", "A terminal emulator and SSH client."))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
arguments.addAll(listOf("--mac-package-name", project.name.uppercaseFirstChar()))
|
arguments.addAll(listOf("--mac-package-name", project.name.uppercaseFirstChar()))
|
||||||
arguments.addAll(listOf("--mac-app-category", "developer-tools"))
|
arguments.addAll(listOf("--mac-app-category", "developer-tools"))
|
||||||
|
|||||||
@@ -1,50 +1,50 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.2.10"
|
kotlin = "2.3.0"
|
||||||
slf4j = "2.0.17"
|
slf4j = "2.0.17"
|
||||||
pty4j = "0.13.10"
|
pty4j = "0.13.10"
|
||||||
tinylog = "2.7.0"
|
tinylog = "2.7.0"
|
||||||
kotlinx-coroutines = "1.10.2"
|
kotlinx-coroutines = "1.10.2"
|
||||||
flatlaf = "3.6.1"
|
flatlaf = "3.7"
|
||||||
kotlinx-serialization-json = "1.9.0"
|
kotlinx-serialization-json = "1.9.0"
|
||||||
commons-codec = "1.19.0"
|
commons-codec = "1.20.0"
|
||||||
commons-lang3 = "3.18.0"
|
commons-lang3 = "3.20.0"
|
||||||
commons-csv = "1.14.1"
|
commons-csv = "1.14.1"
|
||||||
commons-net = "3.12.0"
|
commons-net = "3.12.0"
|
||||||
commons-text = "1.14.0"
|
commons-text = "1.15.0"
|
||||||
commons-compress = "1.28.0"
|
commons-compress = "1.28.0"
|
||||||
commons-vfs2 = "2.10.0"
|
commons-vfs2 = "2.10.0"
|
||||||
swingx = "1.6.5-1"
|
swingx = "1.6.5-1"
|
||||||
jgoodies-forms = "1.9.0"
|
jgoodies-forms = "1.9.0"
|
||||||
jfa = "1.2.0"
|
jfa = "1.2.0"
|
||||||
oshi = "6.8.1"
|
oshi = "6.9.1"
|
||||||
versioncompare = "1.4.1"
|
versioncompare = "1.4.1"
|
||||||
jna = "5.17.0"
|
jna = "5.18.1"
|
||||||
jSystemThemeDetector = "3.9.1"
|
jSystemThemeDetector = "3.9.1"
|
||||||
commons-io = "2.20.0"
|
commons-io = "2.21.0"
|
||||||
jbr-api = "17.1.10.1"
|
jbr-api = "17.1.10.1"
|
||||||
hutool = "5.8.39"
|
hutool = "5.8.40"
|
||||||
jsch = "2.27.2"
|
jsch = "2.27.3"
|
||||||
okhttp = "5.1.0"
|
okhttp = "5.3.0"
|
||||||
sshj = "0.39.0"
|
sshj = "0.39.0"
|
||||||
sshd-core = "2.15.0"
|
sshd-core = "2.15.0"
|
||||||
jgit = "7.2.0.202503040940-r"
|
jgit = "7.4.0.202509020913-r"
|
||||||
commonmark = "0.25.1"
|
commonmark = "0.27.0"
|
||||||
jnafilechooser = "1.1.2"
|
jnafilechooser = "1.1.2"
|
||||||
xodus = "2.0.1"
|
xodus = "2.0.1"
|
||||||
bip39 = "1.0.9"
|
bip39 = "1.0.9"
|
||||||
colorpicker = "2.0.1"
|
colorpicker = "2.0.1"
|
||||||
rhino = "1.8.0"
|
rhino = "1.8.0"
|
||||||
delight-rhino-sandbox = "0.0.17"
|
delight-rhino-sandbox = "0.2.1"
|
||||||
testcontainers = "1.21.3"
|
testcontainers = "2.0.3"
|
||||||
mixpanel = "1.5.3"
|
mixpanel = "1.5.4"
|
||||||
jSerialComm = "2.11.2"
|
jSerialComm = "2.11.4"
|
||||||
ini4j = "0.5.5-2"
|
ini4j = "0.5.5-2"
|
||||||
restart4j = "0.0.1"
|
restart4j = "0.0.1"
|
||||||
eddsa = "0.3.0"
|
eddsa = "0.3.0"
|
||||||
exposed = "1.0.0-beta-5"
|
exposed = "1.0.0-rc-4"
|
||||||
h2 = "2.3.232"
|
h2 = "2.3.232"
|
||||||
sqlite = "3.50.3.0"
|
sqlite = "3.50.3.0"
|
||||||
jug = "5.1.0"
|
jug = "5.2.0"
|
||||||
semver4j = "6.0.0"
|
semver4j = "6.0.0"
|
||||||
jsvg = "2.0.0"
|
jsvg = "2.0.0"
|
||||||
dom4j = "2.2.0"
|
dom4j = "2.2.0"
|
||||||
@@ -70,7 +70,7 @@ flatlafextras = { group = "com.formdev", name = "flatlaf-extras", version.ref =
|
|||||||
flatlafswingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
|
flatlafswingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
|
||||||
testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" }
|
testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" }
|
||||||
testcontainers = { module = "org.testcontainers:testcontainers" }
|
testcontainers = { module = "org.testcontainers:testcontainers" }
|
||||||
testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter" }
|
testcontainers-junit-jupiter = { module = "org.testcontainers:testcontainers-junit-jupiter" }
|
||||||
swingx = { module = "org.swinglabs.swingx:swingx-all", version.ref = "swingx" }
|
swingx = { module = "org.swinglabs.swingx:swingx-all", version.ref = "swingx" }
|
||||||
jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" }
|
jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" }
|
||||||
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
||||||
@@ -106,7 +106,7 @@ eddsa = { module = "net.i2p.crypto:eddsa", version.ref = "eddsa" }
|
|||||||
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
|
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
|
||||||
exposed-crypt = { module = "org.jetbrains.exposed:exposed-crypt", version.ref = "exposed" }
|
exposed-crypt = { module = "org.jetbrains.exposed:exposed-crypt", version.ref = "exposed" }
|
||||||
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
||||||
exposed-migration = { module = "org.jetbrains.exposed:exposed-migration", version.ref = "exposed" }
|
exposed-migration = { module = "org.jetbrains.exposed:exposed-migration-core", version.ref = "exposed" }
|
||||||
h2 = { module = "com.h2database:h2", version.ref = "h2" }
|
h2 = { module = "com.h2database:h2", version.ref = "h2" }
|
||||||
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
|
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
|
||||||
jug = { module = "com.fasterxml.uuid:java-uuid-generator", version.ref = "jug" }
|
jug = { module = "com.fasterxml.uuid:java-uuid-generator", version.ref = "jug" }
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ project.version = "0.0.4"
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
implementation("com.qcloud:cos_api:5.6.251")
|
implementation("com.qcloud:cos_api:5.6.259")
|
||||||
compileOnly(project(":"))
|
compileOnly(project(":"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ plugins {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
project.version = "0.0.7"
|
project.version = "0.0.8"
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package app.termora.plugins.editor
|
package app.termora.plugins.editor
|
||||||
|
|
||||||
import app.termora.DocumentAdaptor
|
import app.termora.*
|
||||||
import app.termora.DynamicColor
|
|
||||||
import app.termora.EnableManager
|
|
||||||
import app.termora.Icons
|
|
||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import com.formdev.flatlaf.extras.components.FlatTextField
|
import com.formdev.flatlaf.extras.components.FlatTextField
|
||||||
@@ -39,6 +36,10 @@ class EditorPanel(private val window: JFrame, private val file: File) : JPanel(B
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(EditorPanel::class.java)
|
private val log = LoggerFactory.getLogger(EditorPanel::class.java)
|
||||||
|
private val saveIcon = DynamicIcon(
|
||||||
|
"icons/save.svg", "icons/save_dark.svg",
|
||||||
|
loader = EditorPlugin::class.java.classLoader
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var text = file.readText(Charsets.UTF_8)
|
private var text = file.readText(Charsets.UTF_8)
|
||||||
@@ -54,6 +55,7 @@ class EditorPanel(private val window: JFrame, private val file: File) : JPanel(B
|
|||||||
private val prevBtn = JButton(Icons.up)
|
private val prevBtn = JButton(Icons.up)
|
||||||
private val context = SearchContext()
|
private val context = SearchContext()
|
||||||
private val softWrapBtn = JToggleButton(Icons.softWrap)
|
private val softWrapBtn = JToggleButton(Icons.softWrap)
|
||||||
|
private val saveBtn = JButton(saveIcon)
|
||||||
private val scrollUpBtn = JButton(Icons.scrollUp)
|
private val scrollUpBtn = JButton(Icons.scrollUp)
|
||||||
private val scrollEndBtn = JButton(Icons.scrollDown)
|
private val scrollEndBtn = JButton(Icons.scrollDown)
|
||||||
private val prettyBtn = JButton(Icons.reformatCode)
|
private val prettyBtn = JButton(Icons.reformatCode)
|
||||||
@@ -141,11 +143,18 @@ class EditorPanel(private val window: JFrame, private val file: File) : JPanel(B
|
|||||||
)
|
)
|
||||||
|
|
||||||
toolbar.orientation = VERTICAL
|
toolbar.orientation = VERTICAL
|
||||||
|
toolbar.add(saveBtn)
|
||||||
toolbar.add(scrollUpBtn)
|
toolbar.add(scrollUpBtn)
|
||||||
toolbar.add(prettyBtn)
|
toolbar.add(prettyBtn)
|
||||||
toolbar.add(softWrapBtn)
|
toolbar.add(softWrapBtn)
|
||||||
toolbar.add(scrollEndBtn)
|
toolbar.add(scrollEndBtn)
|
||||||
|
|
||||||
|
saveBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.save")
|
||||||
|
scrollUpBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.first-line")
|
||||||
|
scrollEndBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.last-line")
|
||||||
|
softWrapBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.soft-wrap")
|
||||||
|
prettyBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.format")
|
||||||
|
|
||||||
val viewPanel = JPanel(BorderLayout())
|
val viewPanel = JPanel(BorderLayout())
|
||||||
viewPanel.add(scrollPane, BorderLayout.CENTER)
|
viewPanel.add(scrollPane, BorderLayout.CENTER)
|
||||||
viewPanel.add(toolbar, BorderLayout.EAST)
|
viewPanel.add(toolbar, BorderLayout.EAST)
|
||||||
@@ -211,6 +220,8 @@ class EditorPanel(private val window: JFrame, private val file: File) : JPanel(B
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
saveBtn.addActionListener(textArea.actionMap.get("Save"))
|
||||||
|
|
||||||
textArea.actionMap.put("Format", object : AbstractAction() {
|
textArea.actionMap.put("Format", object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
format()
|
format()
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
termora.plugins.editor.not-save=The file has not been saved. Are you sure you want to exit?
|
termora.plugins.editor.not-save=The file has not been saved. Are you sure you want to exit?
|
||||||
|
termora.plugins.editor.save=Save
|
||||||
|
termora.plugins.editor.first-line=Jump to first line
|
||||||
|
termora.plugins.editor.last-line=Jump to last line
|
||||||
|
termora.plugins.editor.soft-wrap=Soft-wrap
|
||||||
|
termora.plugins.editor.format=Format
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
termora.plugins.editor.not-save=Файл не сохранён. Вы уверены, что хотите выйти?
|
termora.plugins.editor.not-save=Файл не сохранён. Вы уверены, что хотите выйти?
|
||||||
|
termora.plugins.editor.save=Сохранить
|
||||||
|
termora.plugins.editor.first-line=Перейти на первую строку
|
||||||
|
termora.plugins.editor.last-line=Перейти на последнюю строку
|
||||||
|
termora.plugins.editor.soft-wrap=Мягкий перенос
|
||||||
|
termora.plugins.editor.format=Формат
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
termora.plugins.editor.not-save=文件尚未保存,你确定要退出吗?
|
termora.plugins.editor.not-save=文件尚未保存,你确定要退出吗?
|
||||||
|
termora.plugins.editor.save=保存
|
||||||
|
termora.plugins.editor.first-line=跳转到第一行
|
||||||
|
termora.plugins.editor.last-line=跳转到最后一行
|
||||||
|
termora.plugins.editor.soft-wrap=自动换行
|
||||||
|
termora.plugins.editor.format=格式化
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
termora.plugins.editor.not-save=檔案尚未儲存,你確定要退出嗎?
|
termora.plugins.editor.not-save=檔案尚未儲存,你確定要退出嗎?
|
||||||
|
termora.plugins.editor.save=儲存
|
||||||
|
termora.plugins.editor.first-line=跳到第一行
|
||||||
|
termora.plugins.editor.last-line=跳到最後一行
|
||||||
|
termora.plugins.editor.soft-wrap=自動換行
|
||||||
|
termora.plugins.editor.format=格式化
|
||||||
|
|||||||
4
plugins/editor/src/main/resources/icons/save.svg
Normal file
4
plugins/editor/src/main/resources/icons/save.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5.5 3V5.5H10.5V3M4.5 13V9.5H11.5V13M2.5 13.5V2.5H11.5L13.5 4.5V13.5H2.5Z" stroke="#6C707E" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 357 B |
4
plugins/editor/src/main/resources/icons/save_dark.svg
Normal file
4
plugins/editor/src/main/resources/icons/save_dark.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5.5 3V5.5H10.5V3M4.5 13V9.5H11.5V13M2.5 13.5V2.5H11.5L13.5 4.5V13.5H2.5Z" stroke="#CED0D6" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 357 B |
@@ -7,7 +7,7 @@ project.version = "0.0.2"
|
|||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
compileOnly(project(":"))
|
compileOnly(project(":"))
|
||||||
implementation("org.apache.commons:commons-pool2:2.12.1")
|
implementation("org.apache.commons:commons-pool2:2.13.0")
|
||||||
testImplementation(project(":"))
|
testImplementation(project(":"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ project.version = "0.0.8"
|
|||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
compileOnly(project(":"))
|
compileOnly(project(":"))
|
||||||
implementation("com.maxmind.geoip2:geoip2:4.3.1")
|
implementation("com.maxmind.geoip2:geoip2:5.0.0")
|
||||||
// https://github.com/hstyi/geolite2
|
// https://github.com/hstyi/geolite2
|
||||||
implementation("com.github.hstyi:geolite2:v1.0-202508110059")
|
implementation("com.github.hstyi:geolite2:v1.0-202510270056")
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ project.version = "0.0.3"
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
implementation("com.aliyun.oss:aliyun-sdk-oss:3.18.3")
|
implementation("com.aliyun.oss:aliyun-sdk-oss:3.18.4")
|
||||||
implementation("javax.xml.bind:jaxb-api:2.3.1")
|
implementation("javax.xml.bind:jaxb-api:2.3.1")
|
||||||
implementation("javax.activation:activation:1.1.1")
|
implementation("javax.activation:activation:1.1.1")
|
||||||
implementation("org.glassfish.jaxb:jaxb-runtime:2.3.3")
|
implementation("org.glassfish.jaxb:jaxb-runtime:2.3.3")
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ dependencies {
|
|||||||
testImplementation(libs.testcontainers.junit.jupiter)
|
testImplementation(libs.testcontainers.junit.jupiter)
|
||||||
testImplementation(project(":"))
|
testImplementation(project(":"))
|
||||||
|
|
||||||
implementation("io.minio:minio:8.5.17")
|
implementation("io.minio:minio:8.6.0")
|
||||||
compileOnly(project(":"))
|
compileOnly(project(":"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ project.version = "0.0.5"
|
|||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
compileOnly(project(":"))
|
compileOnly(project(":"))
|
||||||
implementation("com.fazecast:jSerialComm:2.11.2")
|
implementation("com.fazecast:jSerialComm:2.11.4")
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
}
|
}
|
||||||
|
|
||||||
project.version = "0.0.3"
|
project.version = "0.0.4"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class SMBHostOptionsPane : OptionsPane() {
|
|||||||
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||||
extras = mutableMapOf(
|
extras = mutableMapOf(
|
||||||
"smb.share" to generalOption.shareTextField.text,
|
"smb.share" to generalOption.shareTextField.text,
|
||||||
|
"smb.domain" to generalOption.domainTextField.text,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ class SMBHostOptionsPane : OptionsPane() {
|
|||||||
generalOption.remarkTextArea.text = host.remark
|
generalOption.remarkTextArea.text = host.remark
|
||||||
generalOption.passwordTextField.text = host.authentication.password
|
generalOption.passwordTextField.text = host.authentication.password
|
||||||
generalOption.shareTextField.text = host.options.extras["smb.share"] ?: StringUtils.EMPTY
|
generalOption.shareTextField.text = host.options.extras["smb.share"] ?: StringUtils.EMPTY
|
||||||
|
generalOption.domainTextField.text = host.options.extras["smb.domain"] ?: StringUtils.EMPTY
|
||||||
|
|
||||||
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||||
}
|
}
|
||||||
@@ -114,6 +116,7 @@ class SMBHostOptionsPane : OptionsPane() {
|
|||||||
val nameTextField = OutlineTextField(128)
|
val nameTextField = OutlineTextField(128)
|
||||||
val shareTextField = OutlineTextField(256)
|
val shareTextField = OutlineTextField(256)
|
||||||
val usernameTextField = OutlineComboBox<String>()
|
val usernameTextField = OutlineComboBox<String>()
|
||||||
|
val domainTextField = OutlineTextField(128)
|
||||||
val hostTextField = OutlineTextField(255)
|
val hostTextField = OutlineTextField(255)
|
||||||
val passwordTextField = OutlinePasswordField(255)
|
val passwordTextField = OutlinePasswordField(255)
|
||||||
val remarkTextArea = FixedLengthTextArea(512)
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
@@ -188,7 +191,9 @@ class SMBHostOptionsPane : OptionsPane() {
|
|||||||
.add(portTextField).xy(7, rows).apply { rows += step }
|
.add(portTextField).xy(7, rows).apply { rows += step }
|
||||||
|
|
||||||
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||||
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
.add(usernameTextField).xy(3, rows)
|
||||||
|
.add("${SMBI18n.getString("termora.plugins.smb.domain")}:").xy(5, rows)
|
||||||
|
.add(domainTextField).xy(7, rows).apply { rows += step }
|
||||||
|
|
||||||
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||||
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class SMBProtocolProvider private constructor() : TransferProtocolProvider {
|
|||||||
val client = SMBClient()
|
val client = SMBClient()
|
||||||
val host = requester.host
|
val host = requester.host
|
||||||
val connection = client.connect(host.host, host.port)
|
val connection = client.connect(host.host, host.port)
|
||||||
|
val domain = host.options.extras["smb.domain"] ?: StringUtils.EMPTY
|
||||||
val session = when (host.username) {
|
val session = when (host.username) {
|
||||||
"Guest" -> connection.authenticate(AuthenticationContext.guest())
|
"Guest" -> connection.authenticate(AuthenticationContext.guest())
|
||||||
"Anonymous" -> connection.authenticate(AuthenticationContext.anonymous())
|
"Anonymous" -> connection.authenticate(AuthenticationContext.anonymous())
|
||||||
@@ -37,7 +38,7 @@ class SMBProtocolProvider private constructor() : TransferProtocolProvider {
|
|||||||
AuthenticationContext(
|
AuthenticationContext(
|
||||||
host.username,
|
host.username,
|
||||||
host.authentication.password.toCharArray(),
|
host.authentication.password.toCharArray(),
|
||||||
null
|
domain.ifBlank { null }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
termora.plugins.smb.share=Share name
|
termora.plugins.smb.share=Share name
|
||||||
|
termora.plugins.smb.domain=Domain
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
termora.plugins.smb.share=共享名称
|
termora.plugins.smb.share=共享名称
|
||||||
|
termora.plugins.smb.domain=域名
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
termora.plugins.smb.share=共享名稱
|
termora.plugins.smb.share=共享名稱
|
||||||
|
termora.plugins.smb.domain=網域
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,6 @@ termora.plugins.sync.disabled-sync=You are already logged in and cannot use this
|
|||||||
|
|
||||||
termora.settings.sync=Sync
|
termora.settings.sync=Sync
|
||||||
termora.settings.sync.done=Synchronized data successfully
|
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=Range
|
||||||
termora.settings.sync.range.keys=My keys
|
termora.settings.sync.range.keys=My keys
|
||||||
termora.settings.sync.range.keyword-highlights=${termora.highlight}
|
termora.settings.sync.range.keyword-highlights=${termora.highlight}
|
||||||
|
|||||||
@@ -2,15 +2,10 @@ termora.plugins.sync.disabled-sync=你已登录,无法使用此功能
|
|||||||
|
|
||||||
|
|
||||||
termora.settings.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=范围
|
||||||
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=类型
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
termora.plugins.sync.disabled-sync=你已登錄,無法使用此功能
|
termora.plugins.sync.disabled-sync=你已登錄,無法使用此功能
|
||||||
|
|
||||||
termora.settings.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=範圍
|
||||||
termora.settings.sync.range.keys=我的密鑰
|
termora.settings.sync.range.keys=我的密鑰
|
||||||
termora.settings.sync.last-sync-time=最後同步時間
|
termora.settings.sync.last-sync-time=最後同步時間
|
||||||
|
|||||||
@@ -50,6 +50,17 @@ class ApplicationInitializr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/TermoraDev/termora/issues/1254
|
||||||
|
if (System.getProperty(FlatSystemProperties.UI_SCALE).isNullOrBlank()) {
|
||||||
|
val scale = System.getenv("TERMORA_SCALE")
|
||||||
|
if (scale.isNullOrBlank().not()) {
|
||||||
|
if (NumberUtils.toDouble(scale, -1.0) > 0) {
|
||||||
|
System.setProperty(FlatSystemProperties.UI_SCALE_ENABLED, "true")
|
||||||
|
System.setProperty(FlatSystemProperties.UI_SCALE, scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 启动
|
// 启动
|
||||||
val runtime = measureTimeMillis { ApplicationRunner().run() }
|
val runtime = measureTimeMillis { ApplicationRunner().run() }
|
||||||
val log = LoggerFactory.getLogger(javaClass)
|
val log = LoggerFactory.getLogger(javaClass)
|
||||||
|
|||||||
@@ -10,15 +10,11 @@ import com.formdev.flatlaf.extras.FlatInspector
|
|||||||
import com.formdev.flatlaf.ui.FlatTableCellBorder
|
import com.formdev.flatlaf.ui.FlatTableCellBorder
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.jthemedetecor.OsThemeDetector
|
import com.jthemedetecor.OsThemeDetector
|
||||||
import com.mixpanel.mixpanelapi.ClientDelivery
|
|
||||||
import com.mixpanel.mixpanelapi.MessageBuilder
|
|
||||||
import com.mixpanel.mixpanelapi.MixpanelAPI
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.apache.commons.lang3.LocaleUtils
|
import org.apache.commons.lang3.LocaleUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.json.JSONObject
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
import java.awt.desktop.AppReopenedEvent
|
import java.awt.desktop.AppReopenedEvent
|
||||||
@@ -369,61 +365,8 @@ class ApplicationRunner {
|
|||||||
if (Application.isUnknownVersion()) {
|
if (Application.isUnknownVersion()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
MixpanelService.getInstance().push("launch")
|
||||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val properties = JSONObject()
|
|
||||||
properties.put("os", SystemUtils.OS_NAME)
|
|
||||||
if (SystemInfo.isLinux) {
|
|
||||||
properties.put("platform", "Linux")
|
|
||||||
} else if (SystemInfo.isWindows) {
|
|
||||||
properties.put("platform", "Windows")
|
|
||||||
} else if (SystemInfo.isMacOS) {
|
|
||||||
properties.put("platform", "macOS")
|
|
||||||
}
|
|
||||||
properties.put("version", Application.getVersion())
|
|
||||||
properties.put("language", Locale.getDefault().toString())
|
|
||||||
val message = MessageBuilder("0871335f59ee6d0eb246b008a20f9d1c")
|
|
||||||
.event(getAnalyticsUserID(), "launch", properties)
|
|
||||||
val delivery = ClientDelivery()
|
|
||||||
delivery.addMessage(message)
|
|
||||||
val endpoints = listOf(
|
|
||||||
"https://api-eu.mixpanel.com",
|
|
||||||
"https://api-in.mixpanel.com",
|
|
||||||
"https://api.mixpanel.com",
|
|
||||||
"http://api.mixpanel.com",
|
|
||||||
)
|
|
||||||
for (endpoint in endpoints) {
|
|
||||||
try {
|
|
||||||
MixpanelAPI(
|
|
||||||
"$endpoint/track",
|
|
||||||
"$endpoint/engage",
|
|
||||||
"$endpoint/groups"
|
|
||||||
).deliver(delivery, true)
|
|
||||||
break
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (log.isErrorEnabled) {
|
|
||||||
log.error(e.message, e)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (log.isErrorEnabled) {
|
|
||||||
log.error(e.message, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAnalyticsUserID(): String {
|
|
||||||
val properties = DatabaseManager.getInstance().properties
|
|
||||||
var id = properties.getString("AnalyticsUserID")
|
|
||||||
if (id.isNullOrBlank()) {
|
|
||||||
id = randomUUID()
|
|
||||||
properties.putString("AnalyticsUserID", id)
|
|
||||||
}
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -874,6 +874,8 @@ class NordDarkLaf : FlatPropertiesLaf("Nord Dark", Properties().apply {
|
|||||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
TerminalColor.Cursor.BACKGROUND -> 0xeceff4
|
TerminalColor.Cursor.BACKGROUND -> 0xeceff4
|
||||||
|
|
||||||
|
TerminalColor.Basic.SELECTION_FOREGROUND -> 0x3b4252
|
||||||
|
|
||||||
TerminalColor.Basic.FOREGROUND -> 0xd8dee9
|
TerminalColor.Basic.FOREGROUND -> 0xd8dee9
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
85
src/main/kotlin/app/termora/MixpanelService.kt
Normal file
85
src/main/kotlin/app/termora/MixpanelService.kt
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.database.DatabaseManager
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import com.mixpanel.mixpanelapi.ClientDelivery
|
||||||
|
import com.mixpanel.mixpanelapi.MessageBuilder
|
||||||
|
import com.mixpanel.mixpanelapi.MixpanelAPI
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.apache.commons.lang3.SystemUtils
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
internal class MixpanelService private constructor() {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(MixpanelService::class.java)
|
||||||
|
|
||||||
|
fun getInstance(): MixpanelService {
|
||||||
|
return ApplicationScope.forApplicationScope().getOrCreate(MixpanelService::class) { MixpanelService() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun push(event: String, extras: Map<String, String> = emptyMap()) {
|
||||||
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val properties = JSONObject()
|
||||||
|
for (entry in extras) {
|
||||||
|
properties.put(entry.key, entry.value)
|
||||||
|
}
|
||||||
|
properties.put("os", SystemUtils.OS_NAME)
|
||||||
|
if (SystemInfo.isLinux) {
|
||||||
|
properties.put("platform", "Linux")
|
||||||
|
} else if (SystemInfo.isWindows) {
|
||||||
|
properties.put("platform", "Windows")
|
||||||
|
} else if (SystemInfo.isMacOS) {
|
||||||
|
properties.put("platform", "macOS")
|
||||||
|
}
|
||||||
|
properties.put("version", Application.getVersion())
|
||||||
|
properties.put("language", Locale.getDefault().toString())
|
||||||
|
val message = MessageBuilder("0871335f59ee6d0eb246b008a20f9d1c")
|
||||||
|
.event(getAnalyticsUserID(), event, properties)
|
||||||
|
val delivery = ClientDelivery()
|
||||||
|
delivery.addMessage(message)
|
||||||
|
val endpoints = listOf(
|
||||||
|
"https://api-eu.mixpanel.com",
|
||||||
|
"https://api-in.mixpanel.com",
|
||||||
|
"https://api.mixpanel.com",
|
||||||
|
"http://api.mixpanel.com",
|
||||||
|
)
|
||||||
|
for (endpoint in endpoints) {
|
||||||
|
try {
|
||||||
|
MixpanelAPI(
|
||||||
|
"$endpoint/track",
|
||||||
|
"$endpoint/engage",
|
||||||
|
"$endpoint/groups"
|
||||||
|
).deliver(delivery, true)
|
||||||
|
break
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getAnalyticsUserID(): String {
|
||||||
|
val properties = DatabaseManager.getInstance().properties
|
||||||
|
var id = properties.getString("AnalyticsUserID")
|
||||||
|
if (id.isNullOrBlank()) {
|
||||||
|
id = randomUUID()
|
||||||
|
properties.putString("AnalyticsUserID", id)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,5 +3,5 @@ package app.termora
|
|||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class OpenHostActionEvent(source: Any, val host: Host, event: EventObject) :
|
class OpenHostActionEvent(source: Any, val host: Host, event: EventObject, val tabIndex: Int = -1) :
|
||||||
AnActionEvent(source, String(), event)
|
AnActionEvent(source, String(), event)
|
||||||
@@ -179,7 +179,7 @@ abstract class PtyHostTerminalTab(
|
|||||||
|
|
||||||
val tab = createReconnectTerminalTab()
|
val tab = createReconnectTerminalTab()
|
||||||
manager.addTerminalTab(index, tab, true)
|
manager.addTerminalTab(index, tab, true)
|
||||||
manager.closeTerminalTab(this, true)
|
manager.closeTerminalTab(this, disposable = true, reconnect = true)
|
||||||
|
|
||||||
if (tab is HostTerminalTab) {
|
if (tab is HostTerminalTab) {
|
||||||
tab.start()
|
tab.start()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
|
import com.formdev.flatlaf.util.UIScale
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
@@ -14,7 +15,10 @@ internal class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
private val properties get() = DatabaseManager.getInstance().properties
|
private val properties get() = DatabaseManager.getInstance().properties
|
||||||
|
|
||||||
init {
|
init {
|
||||||
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
size = Dimension(
|
||||||
|
UIScale.scale(UIManager.getInt("Dialog.width")),
|
||||||
|
UIScale.scale(UIManager.getInt("Dialog.height"))
|
||||||
|
)
|
||||||
isModal = true
|
isModal = true
|
||||||
title = I18n.getString("termora.setting")
|
title = I18n.getString("termora.setting")
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(null)
|
||||||
|
|||||||
@@ -547,6 +547,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
rightClickComboBox.addItem("Copy")
|
rightClickComboBox.addItem("Copy")
|
||||||
rightClickComboBox.addItem("CopyAndPaste")
|
rightClickComboBox.addItem("CopyAndPaste")
|
||||||
|
rightClickComboBox.addItem("Nothing")
|
||||||
|
|
||||||
rightClickComboBox.selectedItem = terminalSetting.rightClick
|
rightClickComboBox.selectedItem = terminalSetting.rightClick
|
||||||
|
|
||||||
@@ -576,6 +577,8 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
text = I18n.getString("termora.settings.terminal.right-click.copy")
|
text = I18n.getString("termora.settings.terminal.right-click.copy")
|
||||||
} else if (value == "CopyAndPaste") {
|
} else if (value == "CopyAndPaste") {
|
||||||
text = I18n.getString("termora.settings.terminal.right-click.copy-and-paste")
|
text = I18n.getString("termora.settings.terminal.right-click.copy-and-paste")
|
||||||
|
}else if (value == "Nothing") {
|
||||||
|
text = I18n.getString("termora.settings.terminal.right-click.nothing")
|
||||||
}
|
}
|
||||||
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,25 +141,28 @@ class TerminalTabbed(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeTabAt(index: Int, disposable: Boolean = true) {
|
private fun removeTabAt(index: Int, disposable: Boolean = true, reconnect: Boolean = false) {
|
||||||
if (tabbedPane.isTabClosable(index)) {
|
if (tabbedPane.isTabClosable(index)) {
|
||||||
val tab = tabs[index]
|
val tab = tabs[index]
|
||||||
|
|
||||||
// 询问是否可以关闭
|
// 询问是否可以关闭
|
||||||
if (disposable) {
|
if (disposable) {
|
||||||
// 如果开启了关闭确认,那么直接询问用户
|
// 如果是重连接,那么直接关闭不进行任何形式的询问
|
||||||
if (appearance.confirmTabClose) {
|
if (reconnect.not()) {
|
||||||
if (OptionPane.showConfirmDialog(
|
// 如果开启了关闭确认,那么直接询问用户
|
||||||
windowScope.window,
|
if (appearance.confirmTabClose) {
|
||||||
I18n.getString("termora.tabbed.tab.close-prompt"),
|
if (OptionPane.showConfirmDialog(
|
||||||
messageType = JOptionPane.QUESTION_MESSAGE,
|
windowScope.window,
|
||||||
optionType = JOptionPane.OK_CANCEL_OPTION
|
I18n.getString("termora.tabbed.tab.close-prompt"),
|
||||||
) != JOptionPane.OK_OPTION
|
messageType = JOptionPane.QUESTION_MESSAGE,
|
||||||
) {
|
optionType = JOptionPane.OK_CANCEL_OPTION
|
||||||
|
) != JOptionPane.OK_OPTION
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (!tab.willBeClose()) { // 如果没有开启则询问用户
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if (!tab.willBeClose()) { // 如果没有开启则询问用户
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +236,7 @@ class TerminalTabbed(
|
|||||||
if (tab is HostTerminalTab) {
|
if (tab is HostTerminalTab) {
|
||||||
actionManager
|
actionManager
|
||||||
.getAction(OpenHostAction.OPEN_HOST)
|
.getAction(OpenHostAction.OPEN_HOST)
|
||||||
.actionPerformed(OpenHostActionEvent(this, tab.host, evt))
|
.actionPerformed(OpenHostActionEvent(this, tab.host, evt, tabIndex + 1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +364,7 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun indexOfTerminalTab(tab: TerminalTab):Int {
|
override fun indexOfTerminalTab(tab: TerminalTab): Int {
|
||||||
return tabbedPane.indexOfComponent(tab.getJComponent())
|
return tabbedPane.indexOfComponent(tab.getJComponent())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,10 +454,10 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun closeTerminalTab(tab: TerminalTab, disposable: Boolean) {
|
override fun closeTerminalTab(tab: TerminalTab, disposable: Boolean, reconnect: Boolean) {
|
||||||
for (i in 0 until tabs.size) {
|
for (i in 0 until tabs.size) {
|
||||||
if (tabs[i] == tab) {
|
if (tabs[i] == tab) {
|
||||||
removeTabAt(i, disposable)
|
removeTabAt(i, disposable, reconnect)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ interface TerminalTabbedManager {
|
|||||||
fun getSelectedTerminalTab(): TerminalTab?
|
fun getSelectedTerminalTab(): TerminalTab?
|
||||||
fun getTerminalTabs(): List<TerminalTab>
|
fun getTerminalTabs(): List<TerminalTab>
|
||||||
fun setSelectedTerminalTab(tab: TerminalTab)
|
fun setSelectedTerminalTab(tab: TerminalTab)
|
||||||
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true)
|
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true, reconnect: Boolean = false)
|
||||||
fun refreshTerminalTabs()
|
fun refreshTerminalTabs()
|
||||||
fun indexOfTerminalTab(tab: TerminalTab): Int
|
fun indexOfTerminalTab(tab: TerminalTab): Int
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
|
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||||
import app.termora.tree.NewHostTree
|
import app.termora.tree.NewHostTree
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
@@ -9,15 +10,14 @@ import com.formdev.flatlaf.util.SystemInfo
|
|||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Font
|
import java.awt.Font
|
||||||
import java.awt.event.ComponentAdapter
|
import java.awt.event.*
|
||||||
import java.awt.event.ComponentEvent
|
|
||||||
import java.awt.event.KeyEvent
|
|
||||||
import java.awt.event.MouseAdapter
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
import javax.swing.tree.TreePath
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
|
|
||||||
class TermoraFencePanel(
|
class TermoraFencePanel(
|
||||||
|
private val ws: WindowScope,
|
||||||
private val terminalTabbed: TerminalTabbed,
|
private val terminalTabbed: TerminalTabbed,
|
||||||
private val tabbed: FlatTabbedPane,
|
private val tabbed: FlatTabbedPane,
|
||||||
private val moveMouseAdapter: MouseAdapter,
|
private val moveMouseAdapter: MouseAdapter,
|
||||||
@@ -98,6 +98,40 @@ class TermoraFencePanel(
|
|||||||
toggle()
|
toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
DynamicExtensionHandler.getInstance()
|
||||||
|
.register(TerminalTabbedContextMenuExtension::class.java, object : TerminalTabbedContextMenuExtension {
|
||||||
|
override fun createJMenuItem(
|
||||||
|
windowScope: WindowScope,
|
||||||
|
tab: TerminalTab
|
||||||
|
): JMenuItem {
|
||||||
|
if (windowScope != ws) throw UnsupportedOperationException()
|
||||||
|
if (tab !is HostTerminalTab) throw UnsupportedOperationException()
|
||||||
|
if (tab.host.isTemporary) throw UnsupportedOperationException()
|
||||||
|
if (tab.host.id == "local") throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
val item = JMenuItem(I18n.getString("termora.tabbed.contextmenu.select-host"))
|
||||||
|
item.addActionListener(object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val tree = getHostTree()
|
||||||
|
for (node in tree.simpleTreeModel.root.getAllChildren()) {
|
||||||
|
if (node.id == tab.host.id) {
|
||||||
|
tree.selectionPath = TreePath(tree.simpleTreeModel.getPathToRoot(node))
|
||||||
|
tree.requestFocusInWindow()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return Long.MAX_VALUE
|
||||||
|
}
|
||||||
|
|
||||||
|
}).let { Disposer.register(this, it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class LeftTreePanel : JPanel(BorderLayout()), Disposable {
|
private inner class LeftTreePanel : JPanel(BorderLayout()), Disposable {
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (layout == TermoraLayout.Fence) {
|
if (layout == TermoraLayout.Fence) {
|
||||||
val fencePanel = TermoraFencePanel(terminalTabbed, tabbedPane, moveMouseAdapter)
|
val fencePanel = TermoraFencePanel(windowScope, terminalTabbed, tabbedPane, moveMouseAdapter)
|
||||||
add(fencePanel, BorderLayout.CENTER)
|
add(fencePanel, BorderLayout.CENTER)
|
||||||
dataProviderSupport.addData(DataProviders.Welcome.HostTree, fencePanel.getHostTree())
|
dataProviderSupport.addData(DataProviders.Welcome.HostTree, fencePanel.getHostTree())
|
||||||
Disposer.register(windowScope, fencePanel)
|
Disposer.register(windowScope, fencePanel)
|
||||||
|
|||||||
@@ -170,16 +170,18 @@ class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProv
|
|||||||
|
|
||||||
filterableTreeModel.addFilter(object : Filter {
|
filterableTreeModel.addFilter(object : Filter {
|
||||||
override fun filter(node: Any): Boolean {
|
override fun filter(node: Any): Boolean {
|
||||||
val text = searchTextField.text
|
val text = searchTextField.text.trim()
|
||||||
if (text.isBlank()) return true
|
if (text.isBlank()) return true
|
||||||
if (node !is HostTreeNode) return false
|
if (node !is HostTreeNode) return false
|
||||||
if (node is TeamTreeNode || node.id == "0") return true
|
if (node is TeamTreeNode || node.id == "0") return true
|
||||||
return node.host.name.contains(text) || node.host.host.contains(text)
|
return node.host.name.contains(text, ignoreCase = true)
|
||||||
|| node.host.username.contains(text)
|
|| node.host.host.contains(text, ignoreCase = true)
|
||||||
|
|| node.host.username.contains(text, ignoreCase = true)
|
||||||
|
|| node.host.remark.contains(text, ignoreCase = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun canFilter(): Boolean {
|
override fun canFilter(): Boolean {
|
||||||
return searchTextField.text.isNotBlank()
|
return searchTextField.text.trim().isNotBlank()
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
@@ -264,4 +266,4 @@ class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProv
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ object AccountHttp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (cidr == "localhost" || cidr == "127.0.0.1") continue
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug(e.message, e)
|
log.debug(e.message, e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.termora.account
|
package app.termora.account
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.OptionsPane.Companion.FORM_MARGIN
|
import app.termora.OptionsPane.Companion.FORM_MARGIN
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
@@ -8,21 +9,36 @@ import app.termora.database.DatabaseManager
|
|||||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import com.sun.net.httpserver.HttpServer
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
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.Serializable
|
||||||
|
import org.apache.commons.codec.binary.Hex
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils
|
import org.apache.commons.lang3.time.DateFormatUtils
|
||||||
|
import org.jdesktop.swingx.JXBusyLabel
|
||||||
import org.jdesktop.swingx.JXHyperlink
|
import org.jdesktop.swingx.JXHyperlink
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.CardLayout
|
||||||
|
import java.net.InetSocketAddress
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
import java.net.URLEncoder
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
||||||
class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
|
class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(AccountOption::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||||
private val databaseManager get() = DatabaseManager.getInstance()
|
private val databaseManager get() = DatabaseManager.getInstance()
|
||||||
@@ -30,18 +46,31 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
|
|||||||
private val accountProperties get() = AccountProperties.getInstance()
|
private val accountProperties get() = AccountProperties.getInstance()
|
||||||
private val userInfoPanel = JPanel(BorderLayout())
|
private val userInfoPanel = JPanel(BorderLayout())
|
||||||
private val lastSynchronizationOnLabel = JLabel()
|
private val lastSynchronizationOnLabel = JLabel()
|
||||||
|
private val serverManager get() = ServerManager.getInstance()
|
||||||
|
private val cardLayout = CardLayout()
|
||||||
|
private val contentPanel = JPanel(cardLayout)
|
||||||
|
private val loginPanel = JPanel(BorderLayout())
|
||||||
|
private val busyLabel = JXBusyLabel()
|
||||||
|
private var httpServer: HttpServer? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
initEvents()
|
initEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
refreshUserInfoPanel()
|
refreshUserInfoPanel()
|
||||||
add(userInfoPanel, BorderLayout.CENTER)
|
refreshLoginPanel()
|
||||||
}
|
|
||||||
|
|
||||||
|
contentPanel.add(userInfoPanel, "UserInfo")
|
||||||
|
contentPanel.add(loginPanel, "Login")
|
||||||
|
|
||||||
|
cardLayout.show(contentPanel, "UserInfo")
|
||||||
|
|
||||||
|
add(contentPanel, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
// 服务器签名发生变更
|
// 服务器签名发生变更
|
||||||
@@ -99,11 +128,7 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
|
|||||||
planBox.add(Box.createHorizontalStrut(16))
|
planBox.add(Box.createHorizontalStrut(16))
|
||||||
val upgrade = JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.upgrade")) {
|
val upgrade = JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.upgrade")) {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
if (I18n.isChinaMainland()) {
|
Application.browse(URI.create("${accountManager.getServer()}/v1/client/redirect?to=upgrade&version=${Application.getVersion()}"))
|
||||||
Application.browse(URI.create("https://www.termora.cn/pricing?version=${Application.getVersion()}"))
|
|
||||||
} else {
|
|
||||||
Application.browse(URI.create("https://www.termora.app/pricing?version=${Application.getVersion()}"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
upgrade.isFocusable = false
|
upgrade.isFocusable = false
|
||||||
@@ -145,6 +170,29 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getLoginComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"default:grow",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
val cancelBtn = JXHyperlink(object : AnAction(I18n.getString("termora.cancel")) {
|
||||||
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
httpServer?.stop(0)
|
||||||
|
cardLayout.show(contentPanel, "UserInfo")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
val tipLabel = JLabel(I18n.getString("termora.settings.account.wait-login"))
|
||||||
|
tipLabel.foreground = UIManager.getColor("TextField.placeholderForeground")
|
||||||
|
|
||||||
|
return FormBuilder.create().layout(layout).debug(false).padding("10dlu,0,0,0")
|
||||||
|
.add(busyLabel).xy(1, 1, "center, fill")
|
||||||
|
.add(tipLabel).xy(1, 3, "center, fill")
|
||||||
|
.add(cancelBtn).xy(1, 5, "center, fill")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
private fun createActionPanel(isFreePlan: Boolean): JComponent {
|
private fun createActionPanel(isFreePlan: Boolean): JComponent {
|
||||||
val actionBox = Box.createHorizontalBox()
|
val actionBox = Box.createHorizontalBox()
|
||||||
actionBox.add(Box.createHorizontalGlue())
|
actionBox.add(Box.createHorizontalGlue())
|
||||||
@@ -219,11 +267,139 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
|
|||||||
return actionBox
|
return actionBox
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showLoginPanel() {
|
||||||
|
refreshLoginPanel()
|
||||||
|
busyLabel.isBusy = true
|
||||||
|
cardLayout.show(contentPanel, "Login")
|
||||||
|
}
|
||||||
|
|
||||||
private fun onLogin() {
|
private fun onLogin() {
|
||||||
|
httpServer?.stop(0)
|
||||||
|
|
||||||
val dialog = LoginServerDialog(owner)
|
val dialog = LoginServerDialog(owner)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
|
val server = dialog.server ?: return
|
||||||
|
|
||||||
|
showLoginPanel()
|
||||||
|
|
||||||
|
onLogin(server)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun onLogin(server: Server) {
|
||||||
|
|
||||||
|
val httpServer = HttpServer.create(InetSocketAddress("127.0.0.1", 0), 0)
|
||||||
|
.apply { httpServer = this }
|
||||||
|
val future = processLogin(server, httpServer)
|
||||||
|
|
||||||
|
val loginJob = swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val loginResult = future.get(5, TimeUnit.MINUTES)
|
||||||
|
serverManager.login(server, loginResult.refreshToken, loginResult.password)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) log.error(e.message, e)
|
||||||
|
withContext(Dispatchers.Swing) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
StringUtils.defaultIfBlank(
|
||||||
|
e.message ?: StringUtils.EMPTY,
|
||||||
|
I18n.getString("termora.settings.account.login-failed")
|
||||||
|
),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
withContext(Dispatchers.Swing) { cardLayout.show(contentPanel, "UserInfo") }
|
||||||
|
httpServer.stop(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Disposer.register(this, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
loginJob.cancel()
|
||||||
|
httpServer.stop(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
busyLabel.isBusy = false
|
||||||
|
super.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processLogin(server: Server, httpServer: HttpServer): CompletableFuture<LoginResult> {
|
||||||
|
val keypair = RSA.generateKeyPair(2048)
|
||||||
|
val future = CompletableFuture<LoginResult>()
|
||||||
|
|
||||||
|
httpServer.createContext("/callback") { exchange ->
|
||||||
|
val method = exchange.requestMethod
|
||||||
|
if (method.equals("OPTIONS", ignoreCase = true)) {
|
||||||
|
exchange.responseHeaders.add("Access-Control-Allow-Origin", "*")
|
||||||
|
exchange.responseHeaders.add("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||||
|
exchange.responseHeaders.add("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
exchange.sendResponseHeaders(204, -1)
|
||||||
|
} else {
|
||||||
|
var loginResult: LoginResult? = null
|
||||||
|
|
||||||
|
if (method.equals("POST", ignoreCase = true)) {
|
||||||
|
try {
|
||||||
|
val text = String(exchange.requestBody.readAllBytes())
|
||||||
|
loginResult = ohMyJson.decodeFromString<LoginResult>(text)
|
||||||
|
|
||||||
|
val secretKey = RSA.decrypt(keypair.private, Hex.decodeHex(loginResult.secretKey))
|
||||||
|
val secretIv = RSA.decrypt(keypair.private, Hex.decodeHex(loginResult.secretIv))
|
||||||
|
val password = AES.CBC.decrypt(secretKey, secretIv, Hex.decodeHex(loginResult.password))
|
||||||
|
val refreshToken = AES.CBC.decrypt(
|
||||||
|
secretKey, secretIv, Hex.decodeHex(loginResult.refreshToken)
|
||||||
|
)
|
||||||
|
loginResult = loginResult.copy(
|
||||||
|
password = String(password),
|
||||||
|
refreshToken = String(refreshToken)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = "OK".toByteArray()
|
||||||
|
exchange.responseHeaders.add("Access-Control-Allow-Origin", "*")
|
||||||
|
exchange.sendResponseHeaders(200, response.size.toLong())
|
||||||
|
exchange.responseBody.use { it.write(response) }
|
||||||
|
|
||||||
|
if (loginResult != null) {
|
||||||
|
future.complete(loginResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IOUtils.closeQuietly { exchange.close() }
|
||||||
|
}
|
||||||
|
httpServer.start()
|
||||||
|
|
||||||
|
val sb = StringBuilder()
|
||||||
|
val redirect = StringBuilder()
|
||||||
|
redirect.append("/device?callback=").append("http://127.0.0.1:${httpServer.address.port}/callback")
|
||||||
|
redirect.append("&from=device&publicKey=").append(keypair.public.encoded.toHexString())
|
||||||
|
redirect.append("&format=hex&device=termora&device-version=").append(Application.getVersion())
|
||||||
|
|
||||||
|
sb.append(server.server)
|
||||||
|
sb.append("/v1/client/redirect?to=login&from=device")
|
||||||
|
sb.append("&redirect=").append(URLEncoder.encode(redirect.toString(), Charsets.UTF_8))
|
||||||
|
|
||||||
|
Application.browse(URI.create(sb.toString()))
|
||||||
|
|
||||||
|
return future
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class LoginResult(
|
||||||
|
val password: String,
|
||||||
|
val refreshToken: String,
|
||||||
|
val secretKey: String,
|
||||||
|
val secretIv: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
private fun refreshUserInfoPanel() {
|
private fun refreshUserInfoPanel() {
|
||||||
userInfoPanel.removeAll()
|
userInfoPanel.removeAll()
|
||||||
userInfoPanel.add(getCenterComponent(), BorderLayout.CENTER)
|
userInfoPanel.add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
@@ -231,6 +407,13 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
|
|||||||
userInfoPanel.repaint()
|
userInfoPanel.repaint()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun refreshLoginPanel() {
|
||||||
|
loginPanel.removeAll()
|
||||||
|
loginPanel.add(getLoginComponent(), BorderLayout.CENTER)
|
||||||
|
loginPanel.revalidate()
|
||||||
|
loginPanel.repaint()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getIcon(isSelected: Boolean): Icon {
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
return Icons.user
|
return Icons.user
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,6 @@ import app.termora.database.DatabaseManager
|
|||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.swing.Swing
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.boolean
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.jdesktop.swingx.JXHyperlink
|
import org.jdesktop.swingx.JXHyperlink
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -24,12 +18,10 @@ import java.awt.Window
|
|||||||
import java.awt.event.WindowAdapter
|
import java.awt.event.WindowAdapter
|
||||||
import java.awt.event.WindowEvent
|
import java.awt.event.WindowEvent
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.event.ListDataEvent
|
import javax.swing.event.ListDataEvent
|
||||||
import javax.swing.event.ListDataListener
|
import javax.swing.event.ListDataListener
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
|
|
||||||
class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -37,18 +29,14 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val serverComboBox = OutlineComboBox<Server>()
|
private val serverComboBox = OutlineComboBox<Server>()
|
||||||
private val usernameTextField = OutlineTextField(128)
|
|
||||||
private val passwordField = OutlinePasswordField()
|
|
||||||
private val mfaTextField = OutlineTextField(128)
|
|
||||||
private val okAction = OkAction(I18n.getString("termora.settings.account.login"))
|
private val okAction = OkAction(I18n.getString("termora.settings.account.login"))
|
||||||
private val cancelAction = super.createCancelAction()
|
private val cancelAction = super.createCancelAction()
|
||||||
private val cancelButton = super.createJButtonForAction(cancelAction)
|
private val cancelButton = super.createJButtonForAction(cancelAction)
|
||||||
private val isLoggingIn = AtomicBoolean(false)
|
|
||||||
private val singaporeServer =
|
private val singaporeServer =
|
||||||
Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app")
|
Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app")
|
||||||
private val chinaServer =
|
private val chinaServer =
|
||||||
Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn")
|
Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn")
|
||||||
private val serverManager get() = ServerManager.getInstance()
|
var server: Server? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
isModal = true
|
isModal = true
|
||||||
@@ -60,12 +48,10 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
size = Dimension(max(preferredSize.width, UIManager.getInt("Dialog.width") - 250), preferredSize.height)
|
size = Dimension(max(preferredSize.width, UIManager.getInt("Dialog.width") - 250), preferredSize.height)
|
||||||
setLocationRelativeTo(owner)
|
setLocationRelativeTo(owner)
|
||||||
|
|
||||||
passwordField.putClientProperty(FlatClientProperties.STYLE, mapOf("showCapsLock" to true))
|
|
||||||
|
|
||||||
addWindowListener(object : WindowAdapter() {
|
addWindowListener(object : WindowAdapter() {
|
||||||
override fun windowOpened(e: WindowEvent) {
|
override fun windowOpened(e: WindowEvent) {
|
||||||
removeWindowListener(this)
|
removeWindowListener(this)
|
||||||
usernameTextField.requestFocus()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -73,7 +59,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
override fun createCenterPanel(): JComponent {
|
override fun createCenterPanel(): JComponent {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref",
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref",
|
||||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN"
|
"pref, $FORM_MARGIN"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rows = 1
|
var rows = 1
|
||||||
@@ -90,7 +76,6 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
serverComboBox.addItem(Server(server.name, server.server))
|
serverComboBox.addItem(Server(server.name, server.server))
|
||||||
}
|
}
|
||||||
|
|
||||||
mfaTextField.placeholderText = I18n.getString("termora.settings.account.mfa")
|
|
||||||
|
|
||||||
serverComboBox.renderer = object : DefaultListCellRenderer() {
|
serverComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
override fun getListCellRendererComponent(
|
override fun getListCellRendererComponent(
|
||||||
@@ -153,40 +138,6 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val registerAction = object : AnAction(I18n.getString("termora.settings.account.register")) {
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
|
||||||
val server = serverComboBox.selectedItem as Server?
|
|
||||||
if (server == null) {
|
|
||||||
serverComboBox.outline = FlatClientProperties.OUTLINE_ERROR
|
|
||||||
serverComboBox.requestFocusInWindow()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val text = AccountHttp.execute(
|
|
||||||
AccountHttp.client, Request.Builder()
|
|
||||||
.get().url("${server.server}/v1/client/system").build()
|
|
||||||
)
|
|
||||||
val json = runCatching { ohMyJson.decodeFromString<JsonObject>(text) }.getOrNull()
|
|
||||||
val allowRegister = json?.get("register")?.jsonPrimitive?.boolean ?: false
|
|
||||||
if (allowRegister.not()) {
|
|
||||||
throw IllegalStateException(I18n.getString("termora.settings.account.not-support-register"))
|
|
||||||
}
|
|
||||||
Application.browse(URI.create("${server.server}/v1/client/redirect?to=register&from=${Application.getName()}"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (log.isErrorEnabled) {
|
|
||||||
log.error(e.message, e)
|
|
||||||
}
|
|
||||||
OptionPane.showMessageDialog(
|
|
||||||
dialog,
|
|
||||||
e.message ?: I18n.getString("termora.settings.account.not-support-register"),
|
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun refreshButton() {
|
fun refreshButton() {
|
||||||
if (serverComboBox.selectedItem == singaporeServer || serverComboBox.selectedItem == chinaServer || serverComboBox.itemCount < 1) {
|
if (serverComboBox.selectedItem == singaporeServer || serverComboBox.selectedItem == chinaServer || serverComboBox.itemCount < 1) {
|
||||||
newAction.name = I18n.getString("termora.welcome.contextmenu.new")
|
newAction.name = I18n.getString("termora.welcome.contextmenu.new")
|
||||||
@@ -214,21 +165,11 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
val registerLink = JXHyperlink(registerAction)
|
|
||||||
registerLink.isFocusable = false
|
|
||||||
|
|
||||||
|
|
||||||
return FormBuilder.create().layout(layout).debug(false).padding("0dlu, $FORM_MARGIN, 0dlu, $FORM_MARGIN")
|
return FormBuilder.create().layout(layout).debug(false).padding("0dlu, $FORM_MARGIN, 0dlu, $FORM_MARGIN")
|
||||||
.add("${I18n.getString("termora.settings.account.server")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.account.server")}:").xy(1, rows)
|
||||||
.add(serverComboBox).xy(3, rows)
|
.add(serverComboBox).xy(3, rows)
|
||||||
.add(newServer).xy(5, rows).apply { rows += step }
|
.add(newServer).xy(5, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.account")}:").xy(1, rows)
|
|
||||||
.add(usernameTextField).xy(3, rows)
|
|
||||||
.add(registerLink).xy(5, rows).apply { rows += step }
|
|
||||||
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
|
||||||
.add(passwordField).xy(3, rows).apply { rows += step }
|
|
||||||
.add("MFA:").xy(1, rows)
|
|
||||||
.add(mfaTextField).xy(3, rows).apply { rows += step }
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,95 +256,21 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun doOKAction() {
|
override fun doOKAction() {
|
||||||
if (isLoggingIn.get()) return
|
|
||||||
|
|
||||||
val server = serverComboBox.selectedItem as? Server
|
server = serverComboBox.selectedItem as? Server
|
||||||
if (server == null) {
|
if (server == null) {
|
||||||
serverComboBox.outline = FlatClientProperties.OUTLINE_ERROR
|
serverComboBox.outline = FlatClientProperties.OUTLINE_ERROR
|
||||||
serverComboBox.requestFocusInWindow()
|
serverComboBox.requestFocusInWindow()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (usernameTextField.text.isBlank()) {
|
|
||||||
usernameTextField.outline = FlatClientProperties.OUTLINE_ERROR
|
|
||||||
usernameTextField.requestFocusInWindow()
|
|
||||||
return
|
|
||||||
} else if (passwordField.password.isEmpty()) {
|
|
||||||
passwordField.outline = FlatClientProperties.OUTLINE_ERROR
|
|
||||||
passwordField.requestFocusInWindow()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoggingIn.compareAndSet(false, true)) {
|
|
||||||
okAction.isEnabled = false
|
|
||||||
usernameTextField.isEnabled = false
|
|
||||||
passwordField.isEnabled = false
|
|
||||||
mfaTextField.isEnabled = false
|
|
||||||
serverComboBox.isEnabled = false
|
|
||||||
cancelButton.isVisible = false
|
|
||||||
onLogin(server)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
super.doOKAction()
|
super.doOKAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onLogin(server: Server) {
|
|
||||||
val job = swingCoroutineScope.launch(Dispatchers.IO) {
|
|
||||||
var c = 0
|
|
||||||
while (isActive) {
|
|
||||||
if (++c > 3) c = 0
|
|
||||||
okAction.name = I18n.getString("termora.settings.account.login") + ".".repeat(c)
|
|
||||||
delay(350.milliseconds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val loginJob = swingCoroutineScope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
serverManager.login(
|
|
||||||
server, usernameTextField.text,
|
|
||||||
String(passwordField.password), mfaTextField.text.trim()
|
|
||||||
)
|
|
||||||
withContext(Dispatchers.Swing) {
|
|
||||||
super.doOKAction()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (log.isErrorEnabled) log.error(e.message, e)
|
|
||||||
withContext(Dispatchers.Swing) {
|
|
||||||
OptionPane.showMessageDialog(
|
|
||||||
this@LoginServerDialog,
|
|
||||||
StringUtils.defaultIfBlank(
|
|
||||||
e.message ?: StringUtils.EMPTY,
|
|
||||||
I18n.getString("termora.settings.account.login-failed")
|
|
||||||
),
|
|
||||||
messageType = JOptionPane.ERROR_MESSAGE,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
job.cancel()
|
|
||||||
withContext(Dispatchers.Swing) {
|
|
||||||
okAction.name = I18n.getString("termora.settings.account.login")
|
|
||||||
okAction.isEnabled = true
|
|
||||||
usernameTextField.isEnabled = true
|
|
||||||
passwordField.isEnabled = true
|
|
||||||
serverComboBox.isEnabled = true
|
|
||||||
cancelButton.isVisible = true
|
|
||||||
mfaTextField.isEnabled = true
|
|
||||||
}
|
|
||||||
isLoggingIn.compareAndSet(true, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Disposer.register(disposable, object : Disposable {
|
|
||||||
override fun dispose() {
|
|
||||||
if (loginJob.isActive)
|
|
||||||
loginJob.cancel()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun doCancelAction() {
|
override fun doCancelAction() {
|
||||||
if (isLoggingIn.get()) return
|
server = null
|
||||||
super.doCancelAction()
|
super.doCancelAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,11 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
|||||||
private var lastChangeHash = StringUtils.EMPTY
|
private var lastChangeHash = StringUtils.EMPTY
|
||||||
|
|
||||||
private fun pullChanges() {
|
private fun pullChanges() {
|
||||||
if (isFreePlan) return
|
|
||||||
|
if (accountManager.isLocally()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val hash: String
|
val hash: String
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package app.termora.account
|
package app.termora.account
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.AES
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
|
import app.termora.ApplicationScope
|
||||||
|
import app.termora.PBKDF2
|
||||||
|
import app.termora.RSA
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
@@ -9,7 +12,6 @@ import okhttp3.MediaType.Companion.toMediaType
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import org.apache.commons.codec.binary.Base64
|
import org.apache.commons.codec.binary.Base64
|
||||||
import org.apache.commons.codec.digest.DigestUtils
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class ServerManager private constructor() {
|
class ServerManager private constructor() {
|
||||||
@@ -28,7 +30,7 @@ class ServerManager private constructor() {
|
|||||||
/**
|
/**
|
||||||
* 登录,不报错就是登录成功
|
* 登录,不报错就是登录成功
|
||||||
*/
|
*/
|
||||||
fun login(server: Server, username: String, password: String, mfa: String) {
|
fun login(server: Server, refreshToken: String, password: String) {
|
||||||
|
|
||||||
if (accountManager.isLocally().not()) {
|
if (accountManager.isLocally().not()) {
|
||||||
throw IllegalStateException("Already logged in")
|
throw IllegalStateException("Already logged in")
|
||||||
@@ -39,25 +41,25 @@ class ServerManager private constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
doLogin(server, username, password, mfa)
|
doLogin(server, refreshToken, password)
|
||||||
} finally {
|
} finally {
|
||||||
isLoggingIn.compareAndSet(true, false)
|
isLoggingIn.compareAndSet(true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doLogin(server: Server, username: String, password: String, mfa: String) {
|
private fun doLogin(server: Server, refreshToken: String, password: String) {
|
||||||
// 服务器信息
|
// 服务器信息
|
||||||
val serverInfo = getServerInfo(server)
|
val serverInfo = getServerInfo(server)
|
||||||
|
|
||||||
// call login
|
// call login
|
||||||
val loginResponse = callLogin(serverInfo, server, username, password, mfa)
|
val loginResponse = callToken(server, refreshToken)
|
||||||
|
|
||||||
// call me
|
// call me
|
||||||
val meResponse = callMe(server.server, loginResponse.accessToken)
|
val meResponse = callMe(server.server, loginResponse.accessToken)
|
||||||
|
|
||||||
// 解密
|
// 解密
|
||||||
val salt = "${serverInfo.salt}:${username}".toByteArray()
|
val salt = "${serverInfo.salt}:${meResponse.email}".toByteArray()
|
||||||
val privateKeySecureKey = PBKDF2.hash(salt, password.toCharArray(), 1024, 256)
|
val privateKeySecureKey = PBKDF2.hash(salt, password.toCharArray(), 1024, 256)
|
||||||
val privateKeySecureIv = PBKDF2.hash(salt, password.toCharArray(), 1024, 128)
|
val privateKeySecureIv = PBKDF2.hash(salt, password.toCharArray(), 1024, 128)
|
||||||
val privateKeyEncoded = AES.CBC.decrypt(
|
val privateKeyEncoded = AES.CBC.decrypt(
|
||||||
@@ -106,29 +108,19 @@ class ServerManager private constructor() {
|
|||||||
return ohMyJson.decodeFromString<ServerInfo>(AccountHttp.execute(request = request))
|
return ohMyJson.decodeFromString<ServerInfo>(AccountHttp.execute(request = request))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun callLogin(
|
private fun callToken(
|
||||||
serverInfo: ServerInfo,
|
|
||||||
server: Server,
|
server: Server,
|
||||||
username: String,
|
refreshToken: String,
|
||||||
password: String,
|
|
||||||
mfa: String
|
|
||||||
): LoginResponse {
|
): LoginResponse {
|
||||||
|
val body = ohMyJson.encodeToString(mapOf("refreshToken" to refreshToken))
|
||||||
val passwordHex = DigestUtils.sha256Hex("${serverInfo.salt}:${username}:${password}")
|
|
||||||
val requestBody = ohMyJson.encodeToString(mapOf("email" to username, "password" to passwordHex, "mfa" to mfa))
|
|
||||||
.toRequestBody("application/json".toMediaType())
|
.toRequestBody("application/json".toMediaType())
|
||||||
|
val request = Request.Builder().url("${server.server}/v1/token")
|
||||||
val request = Request.Builder()
|
.header("Authorization", "Bearer $refreshToken")
|
||||||
.url("${server.server}/v1/login")
|
.post(body)
|
||||||
.post(requestBody)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val response = AccountHttp.client.newCall(request).execute()
|
val response = AccountHttp.client.newCall(request).execute()
|
||||||
val text = response.use { response.body.use { it?.string() } }
|
val text = response.use { response.body.use { it.string() } }
|
||||||
|
|
||||||
if (text == null) {
|
|
||||||
throw ResponseException(response.code, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.isSuccessful.not()) {
|
if (response.isSuccessful.not()) {
|
||||||
val message = ohMyJson.parseToJsonElement(text).jsonObject["message"]?.jsonPrimitive?.content
|
val message = ohMyJson.parseToJsonElement(text).jsonObject["message"]?.jsonPrimitive?.content
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.apache.commons.codec.binary.Base64
|
|||||||
import org.apache.commons.codec.digest.DigestUtils
|
import org.apache.commons.codec.digest.DigestUtils
|
||||||
import org.apache.commons.lang3.ObjectUtils
|
import org.apache.commons.lang3.ObjectUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
|
|||||||
@@ -56,7 +56,12 @@ class OpenHostAction : AnAction() {
|
|||||||
|
|
||||||
if (tab == null) return
|
if (tab == null) return
|
||||||
|
|
||||||
terminalTabbedManager.addTerminalTab(tab)
|
if (evt.tabIndex >= 0) {
|
||||||
|
terminalTabbedManager.addTerminalTab(evt.tabIndex, tab)
|
||||||
|
} else {
|
||||||
|
terminalTabbedManager.addTerminalTab(tab)
|
||||||
|
}
|
||||||
|
|
||||||
if (tab is PtyHostTerminalTab) {
|
if (tab is PtyHostTerminalTab) {
|
||||||
tab.start()
|
tab.start()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ class TerminalCopyAction : AnAction() {
|
|||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val terminalPanel = evt.getData(DataProviders.TerminalPanel) ?: return
|
val terminalPanel = evt.getData(DataProviders.TerminalPanel) ?: return
|
||||||
|
val selectionModel = terminalPanel.terminal.getSelectionModel()
|
||||||
|
if (!selectionModel.hasSelection()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val text = terminalPanel.copy()
|
val text = terminalPanel.copy()
|
||||||
val systemClipboard = terminalPanel.toolkit.systemClipboard
|
val systemClipboard = terminalPanel.toolkit.systemClipboard
|
||||||
|
|
||||||
@@ -53,4 +57,4 @@ class TerminalCopyAction : AnAction() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import app.termora.snippet.SnippetManager
|
|||||||
import app.termora.terminal.CursorStyle
|
import app.termora.terminal.CursorStyle
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.jetbrains.exposed.v1.core.SqlExpressionBuilder.eq
|
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.v1.core.statements.StatementType
|
import org.jetbrains.exposed.v1.core.statements.StatementType
|
||||||
import org.jetbrains.exposed.v1.jdbc.*
|
import org.jetbrains.exposed.v1.jdbc.*
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
||||||
|
|||||||
@@ -27,12 +27,14 @@ class KeyboardInteractiveDialog(
|
|||||||
isModal = true
|
isModal = true
|
||||||
isResizable = true
|
isResizable = true
|
||||||
controlsVisible = false
|
controlsVisible = false
|
||||||
title = I18n.getString("termora.new-host.title")
|
|
||||||
|
|
||||||
init()
|
init()
|
||||||
pack()
|
pack()
|
||||||
size = Dimension(max(300, size.width), size.height)
|
size = Dimension(max(300, size.width), size.height)
|
||||||
|
|
||||||
|
// fix https://github.com/TermoraDev/termora/issues/1311
|
||||||
|
pack()
|
||||||
|
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class TerminalUserInteraction(
|
|||||||
)
|
)
|
||||||
dialog.setLocationRelativeTo(owner)
|
dialog.setLocationRelativeTo(owner)
|
||||||
dialog.title = instruction ?: name ?: "OTP"
|
dialog.title = instruction ?: name ?: "OTP"
|
||||||
|
dialog.title = StringUtils.defaultIfBlank(dialog.title, "OTP")
|
||||||
passwords[i] = dialog.getText()
|
passwords[i] = dialog.getText()
|
||||||
if (passwords[i].isBlank()) {
|
if (passwords[i].isBlank()) {
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ class KeymapPanel : JPanel(BorderLayout()) {
|
|||||||
private val copyBtn = JButton(Icons.copy)
|
private val copyBtn = JButton(Icons.copy)
|
||||||
private val renameBtn = JButton(Icons.edit)
|
private val renameBtn = JButton(Icons.edit)
|
||||||
private val deleteBtn = JButton(Icons.delete)
|
private val deleteBtn = JButton(Icons.delete)
|
||||||
|
private val infoBtn = JButton(Icons.questionMark)
|
||||||
private val database get() = DatabaseManager.getInstance()
|
private val database get() = DatabaseManager.getInstance()
|
||||||
private val allowKeyCodes = mutableSetOf<Int>()
|
private val allowKeyCodes = mutableSetOf<Int>()
|
||||||
|
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
@@ -89,8 +91,8 @@ class KeymapPanel : JPanel(BorderLayout()) {
|
|||||||
box.add(copyBtn)
|
box.add(copyBtn)
|
||||||
box.add(renameBtn)
|
box.add(renameBtn)
|
||||||
box.add(deleteBtn)
|
box.add(deleteBtn)
|
||||||
|
box.add(infoBtn)
|
||||||
box.add(Box.createHorizontalGlue())
|
box.add(Box.createHorizontalGlue())
|
||||||
box.border = BorderFactory.createEmptyBorder(0, 0, 6, 0)
|
|
||||||
|
|
||||||
add(box, BorderLayout.NORTH)
|
add(box, BorderLayout.NORTH)
|
||||||
add(scrollPane, BorderLayout.CENTER)
|
add(scrollPane, BorderLayout.CENTER)
|
||||||
@@ -105,6 +107,12 @@ class KeymapPanel : JPanel(BorderLayout()) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
infoBtn.addActionListener {
|
||||||
|
val color = UIManager.getColor("TextField.placeholderForeground")
|
||||||
|
val msg = I18n.getString("termora.settings.keymap.question", color.red, color.green, color.blue)
|
||||||
|
OptionPane.showMessageDialog(owner, msg)
|
||||||
|
}
|
||||||
|
|
||||||
copyBtn.addActionListener {
|
copyBtn.addActionListener {
|
||||||
val keymap = getCurrentKeymap()
|
val keymap = getCurrentKeymap()
|
||||||
if (keymap != null) {
|
if (keymap != null) {
|
||||||
|
|||||||
@@ -262,8 +262,8 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
|
|||||||
|
|
||||||
OptionPane.openFileInFolder(
|
OptionPane.openFileInFolder(
|
||||||
SwingUtilities.getWindowAncestor(this),
|
SwingUtilities.getWindowAncestor(this),
|
||||||
file, I18n.getString("termora.settings.sync.export-done-open-folder"),
|
file, I18n.getString("termora.keymgr.export-done-open-folder"),
|
||||||
I18n.getString("termora.settings.sync.export-done")
|
I18n.getString("termora.keymgr.export-done")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,6 +287,9 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
|
|||||||
|
|
||||||
typeComboBox.addItem("RSA")
|
typeComboBox.addItem("RSA")
|
||||||
typeComboBox.addItem("ED25519")
|
typeComboBox.addItem("ED25519")
|
||||||
|
typeComboBox.addItem("ECDSA-SHA2-NISTP256")
|
||||||
|
typeComboBox.addItem("ECDSA-SHA2-NISTP384")
|
||||||
|
typeComboBox.addItem("ECDSA-SHA2-NISTP521")
|
||||||
|
|
||||||
// 默认 RSA
|
// 默认 RSA
|
||||||
lengthComboBox.addItem(1024)
|
lengthComboBox.addItem(1024)
|
||||||
@@ -396,6 +399,12 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
|
|||||||
lengthComboBox.addItem(1024 * 4)
|
lengthComboBox.addItem(1024 * 4)
|
||||||
lengthComboBox.addItem(1024 * 8)
|
lengthComboBox.addItem(1024 * 8)
|
||||||
lengthComboBox.selectedItem = 1024 * 2
|
lengthComboBox.selectedItem = 1024 * 2
|
||||||
|
} else if (typeComboBox.selectedItem == "ECDSA-SHA2-NISTP256") {
|
||||||
|
lengthComboBox.addItem(256)
|
||||||
|
} else if (typeComboBox.selectedItem == "ECDSA-SHA2-NISTP384") {
|
||||||
|
lengthComboBox.addItem(384)
|
||||||
|
} else if (typeComboBox.selectedItem == "ECDSA-SHA2-NISTP521") {
|
||||||
|
lengthComboBox.addItem(521)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,6 +422,17 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
|
|||||||
super.doCancelAction()
|
super.doCancelAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun genKeyPair(): KeyPair {
|
||||||
|
val keyType = when (typeComboBox.selectedItem) {
|
||||||
|
"ED25519" -> KeyPairProvider.SSH_ED25519
|
||||||
|
"ECDSA-SHA2-NISTP256" -> KeyPairProvider.ECDSA_SHA2_NISTP256
|
||||||
|
"ECDSA-SHA2-NISTP384" -> KeyPairProvider.ECDSA_SHA2_NISTP384
|
||||||
|
"ECDSA-SHA2-NISTP521" -> KeyPairProvider.ECDSA_SHA2_NISTP521
|
||||||
|
else -> KeyPairProvider.SSH_RSA
|
||||||
|
}
|
||||||
|
return KeyUtils.generateKeyPair(keyType, lengthComboBox.selectedItem as Int)
|
||||||
|
}
|
||||||
|
|
||||||
override fun doOKAction() {
|
override fun doOKAction() {
|
||||||
|
|
||||||
if (ohKeyPair == OhKeyPair.empty) {
|
if (ohKeyPair == OhKeyPair.empty) {
|
||||||
@@ -422,9 +442,7 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val keyType = if (typeComboBox.selectedItem == "RSA")
|
val keyPair = genKeyPair()
|
||||||
KeyPairProvider.SSH_RSA else KeyPairProvider.SSH_ED25519
|
|
||||||
val keyPair = KeyUtils.generateKeyPair(keyType, lengthComboBox.selectedItem as Int)
|
|
||||||
ohKeyPair = OhKeyPair(
|
ohKeyPair = OhKeyPair(
|
||||||
id = randomUUID(),
|
id = randomUUID(),
|
||||||
name = nameTextField.text,
|
name = nameTextField.text,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.termora.keymgr
|
|||||||
|
|
||||||
import app.termora.AES.decodeBase64
|
import app.termora.AES.decodeBase64
|
||||||
import app.termora.RSA
|
import app.termora.RSA
|
||||||
|
import org.apache.sshd.common.config.keys.impl.ECDSAPublicKeyEntryDecoder
|
||||||
import org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider
|
import org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider
|
||||||
import org.apache.sshd.common.session.SessionContext
|
import org.apache.sshd.common.session.SessionContext
|
||||||
import org.apache.sshd.common.util.security.eddsa.Ed25519PublicKeyDecoder
|
import org.apache.sshd.common.util.security.eddsa.Ed25519PublicKeyDecoder
|
||||||
@@ -25,6 +26,8 @@ class OhKeyPairKeyPairProvider(private val id: String) : AbstractResourceKeyPair
|
|||||||
when (ohKeyPair.type) {
|
when (ohKeyPair.type) {
|
||||||
"RSA" -> RSA.generatePublic(ohKeyPair.publicKey.decodeBase64())
|
"RSA" -> RSA.generatePublic(ohKeyPair.publicKey.decodeBase64())
|
||||||
"ED25519" -> Ed25519PublicKeyDecoder.INSTANCE.generatePublicKey((X509EncodedKeySpec(ohKeyPair.publicKey.decodeBase64())))
|
"ED25519" -> Ed25519PublicKeyDecoder.INSTANCE.generatePublicKey((X509EncodedKeySpec(ohKeyPair.publicKey.decodeBase64())))
|
||||||
|
"ECDSA-SHA2-NISTP256","ECDSA-SHA2-NISTP384","ECDSA-SHA2-NISTP521" ->
|
||||||
|
ECDSAPublicKeyEntryDecoder.INSTANCE.generatePublicKey(X509EncodedKeySpec(ohKeyPair.publicKey.decodeBase64()))
|
||||||
else -> throw UnsupportedOperationException("${ohKeyPair.type} is not supported")
|
else -> throw UnsupportedOperationException("${ohKeyPair.type} is not supported")
|
||||||
}
|
}
|
||||||
} as PublicKey
|
} as PublicKey
|
||||||
@@ -33,6 +36,8 @@ class OhKeyPairKeyPairProvider(private val id: String) : AbstractResourceKeyPair
|
|||||||
when (ohKeyPair.type) {
|
when (ohKeyPair.type) {
|
||||||
"RSA" -> RSA.generatePrivate(ohKeyPair.privateKey.decodeBase64())
|
"RSA" -> RSA.generatePrivate(ohKeyPair.privateKey.decodeBase64())
|
||||||
"ED25519" -> Ed25519PublicKeyDecoder.INSTANCE.generatePrivateKey(PKCS8EncodedKeySpec(ohKeyPair.privateKey.decodeBase64()))
|
"ED25519" -> Ed25519PublicKeyDecoder.INSTANCE.generatePrivateKey(PKCS8EncodedKeySpec(ohKeyPair.privateKey.decodeBase64()))
|
||||||
|
"ECDSA-SHA2-NISTP256","ECDSA-SHA2-NISTP384","ECDSA-SHA2-NISTP521" ->
|
||||||
|
ECDSAPublicKeyEntryDecoder.INSTANCE.generatePrivateKey(PKCS8EncodedKeySpec(ohKeyPair.privateKey.decodeBase64()))
|
||||||
else -> throw UnsupportedOperationException("${ohKeyPair.type} is not supported")
|
else -> throw UnsupportedOperationException("${ohKeyPair.type} is not supported")
|
||||||
}
|
}
|
||||||
} as PrivateKey
|
} as PrivateKey
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class MacroManager private constructor() {
|
|||||||
|
|
||||||
val accountId = AccountManager.getInstance().getAccountId()
|
val accountId = AccountManager.getInstance().getAccountId()
|
||||||
|
|
||||||
database.save(
|
database.saveAndIncrementVersion(
|
||||||
Data(
|
Data(
|
||||||
id = macro.id,
|
id = macro.id,
|
||||||
ownerId = accountId,
|
ownerId = accountId,
|
||||||
|
|||||||
@@ -185,6 +185,13 @@ class PluginPanel(val descriptor: PluginPluginDescriptor) : JPanel(), Disposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MixpanelService.getInstance().push(
|
||||||
|
"uninstall-plugin", mapOf(
|
||||||
|
"pluginName" to descriptor.plugin.getName(),
|
||||||
|
"pluginVersion" to descriptor.version.toString(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// 询问是否重启
|
// 询问是否重启
|
||||||
TermoraRestarter.getInstance().scheduleRestart(owner)
|
TermoraRestarter.getInstance().scheduleRestart(owner)
|
||||||
} else {
|
} else {
|
||||||
@@ -227,6 +234,13 @@ class PluginPanel(val descriptor: PluginPluginDescriptor) : JPanel(), Disposable
|
|||||||
}
|
}
|
||||||
}, button == updateButton)
|
}, button == updateButton)
|
||||||
|
|
||||||
|
MixpanelService.getInstance().push(
|
||||||
|
"${if (button == installButton) "install" else "update"}-plugin", mapOf(
|
||||||
|
"pluginName" to descriptor.plugin.getName(),
|
||||||
|
"pluginVersion" to descriptor.version.toString(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
installed.add(descriptor.id)
|
installed.add(descriptor.id)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import kotlinx.coroutines.launch
|
|||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
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.Strings
|
||||||
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import java.awt.datatransfer.DataFlavor
|
import java.awt.datatransfer.DataFlavor
|
||||||
import java.awt.datatransfer.StringSelection
|
import java.awt.datatransfer.StringSelection
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@@ -64,7 +66,15 @@ internal class RDPProtocolProvider private constructor() : GenericProtocolProvid
|
|||||||
}
|
}
|
||||||
|
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
sb.append("full address:s:").append(host.host).append(':').append(host.port).appendLine()
|
sb.append("full address:s:")
|
||||||
|
if (SystemUtils.IS_OS_WINDOWS && Strings.CI.contains(host.host, ":")) {
|
||||||
|
var newHost = Strings.CI.removeStart(host.host, "[")
|
||||||
|
newHost = Strings.CI.removeEnd(newHost, "]")
|
||||||
|
sb.append('[').append(newHost).append(']')
|
||||||
|
} else {
|
||||||
|
sb.append(host.host)
|
||||||
|
}
|
||||||
|
sb.append(':').append(host.port).appendLine()
|
||||||
sb.append("username:s:").append(host.username).appendLine()
|
sb.append("username:s:").append(host.username).appendLine()
|
||||||
val desktop = host.options.extras["desktop"]
|
val desktop = host.options.extras["desktop"]
|
||||||
if (desktop.isNullOrBlank().not()) {
|
if (desktop.isNullOrBlank().not()) {
|
||||||
|
|||||||
@@ -27,9 +27,14 @@ class CloneSessionTerminalTabbedContextMenuExtension private constructor() : Ter
|
|||||||
cloneSession.addActionListener(object : AnAction() {
|
cloneSession.addActionListener(object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||||
|
val index = terminalTabbedManager.indexOfTerminalTab(tab)
|
||||||
val handler = c.copy(channel = null)
|
val handler = c.copy(channel = null)
|
||||||
val newTab = SSHTerminalTab(windowScope, tab.host, handler)
|
val newTab = SSHTerminalTab(windowScope, tab.host, handler)
|
||||||
terminalTabbedManager.addTerminalTab(newTab)
|
if (index >= 0) {
|
||||||
|
terminalTabbedManager.addTerminalTab(index + 1, newTab)
|
||||||
|
} else {
|
||||||
|
terminalTabbedManager.addTerminalTab(newTab)
|
||||||
|
}
|
||||||
newTab.start()
|
newTab.start()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import app.termora.keymgr.KeyManagerDialog
|
|||||||
import app.termora.plugin.internal.AltKeyModifier
|
import app.termora.plugin.internal.AltKeyModifier
|
||||||
import app.termora.plugin.internal.BasicProxyOption
|
import app.termora.plugin.internal.BasicProxyOption
|
||||||
import app.termora.plugin.internal.BasicTerminalOption
|
import app.termora.plugin.internal.BasicTerminalOption
|
||||||
|
import app.termora.plugin.internal.telnet.TelnetHostOptionsPane.Backspace
|
||||||
import app.termora.tree.Filter
|
import app.termora.tree.Filter
|
||||||
import app.termora.tree.HostTreeNode
|
import app.termora.tree.HostTreeNode
|
||||||
import app.termora.tree.NewHostTreeDialog
|
import app.termora.tree.NewHostTreeDialog
|
||||||
@@ -36,6 +37,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
|||||||
private val terminalOption = BasicTerminalOption().apply {
|
private val terminalOption = BasicTerminalOption().apply {
|
||||||
showCharsetComboBox = true
|
showCharsetComboBox = true
|
||||||
showLoginScripts = true
|
showLoginScripts = true
|
||||||
|
showBackspaceComboBox = true
|
||||||
showEnvironmentTextArea = true
|
showEnvironmentTextArea = true
|
||||||
showStartupCommandTextField = true
|
showStartupCommandTextField = true
|
||||||
showHeartbeatIntervalTextField = true
|
showHeartbeatIntervalTextField = true
|
||||||
@@ -112,6 +114,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
|||||||
x11Forwarding = tunnelingOption.x11ServerTextField.text,
|
x11Forwarding = tunnelingOption.x11ServerTextField.text,
|
||||||
loginScripts = terminalOption.loginScripts,
|
loginScripts = terminalOption.loginScripts,
|
||||||
extras = mutableMapOf(
|
extras = mutableMapOf(
|
||||||
|
"backspace" to (terminalOption.backspaceComboBox.selectedItem as Backspace).name,
|
||||||
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
|
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
|
||||||
?: AltKeyModifier.EightBit.name),
|
?: AltKeyModifier.EightBit.name),
|
||||||
"keywordHighlightSetId" to ((terminalOption.highlightSetComboBox.selectedItem as? KeywordHighlight)?.id
|
"keywordHighlightSetId" to ((terminalOption.highlightSetComboBox.selectedItem as? KeywordHighlight)?.id
|
||||||
@@ -169,6 +172,9 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
|||||||
.getOrNull() ?: AltKeyModifier.EightBit
|
.getOrNull() ?: AltKeyModifier.EightBit
|
||||||
|
|
||||||
|
|
||||||
|
terminalOption.backspaceComboBox.selectedItem =
|
||||||
|
Backspace.valueOf(host.options.extras["backspace"] ?: Backspace.Delete.name)
|
||||||
|
|
||||||
val timeout = host.options.extras["timeout"] ?: "60"
|
val timeout = host.options.extras["timeout"] ?: "60"
|
||||||
terminalOption.timeoutTextField.value = timeout.toIntOrNull() ?: 60
|
terminalOption.timeoutTextField.value = timeout.toIntOrNull() ?: 60
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ import app.termora.addons.zmodem.ZModemPtyConnectorAdaptor
|
|||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
import app.termora.keymap.KeyShortcut
|
import app.termora.keymap.KeyShortcut
|
||||||
import app.termora.keymap.KeymapManager
|
import app.termora.keymap.KeymapManager
|
||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.plugin.internal.telnet.TelnetHostOptionsPane
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.*
|
||||||
import app.termora.terminal.PtyConnector
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
@@ -20,6 +19,7 @@ import org.apache.sshd.client.session.ClientSession
|
|||||||
import org.apache.sshd.common.future.CloseFuture
|
import org.apache.sshd.common.future.CloseFuture
|
||||||
import org.apache.sshd.common.future.SshFutureListener
|
import org.apache.sshd.common.future.SshFutureListener
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
@@ -110,7 +110,18 @@ class SSHTerminalTab(
|
|||||||
// clear screen
|
// clear screen
|
||||||
terminal.clearScreen()
|
terminal.clearScreen()
|
||||||
// show cursor
|
// show cursor
|
||||||
terminalModel.setData(DataKey.Companion.ShowCursor, true)
|
terminalModel.setData(DataKey.ShowCursor, true)
|
||||||
|
|
||||||
|
val encoder = terminal.getKeyEncoder()
|
||||||
|
if (encoder is KeyEncoderImpl) {
|
||||||
|
val backspace = host.options.extras["backspace"]
|
||||||
|
if (backspace == TelnetHostOptionsPane.Backspace.Backspace.name) {
|
||||||
|
encoder.putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_BACK_SPACE), String(byteArrayOf(0x08)))
|
||||||
|
} else if (backspace == TelnetHostOptionsPane.Backspace.VT220.name) {
|
||||||
|
encoder.putCode(TerminalKeyEvent(keyCode = KeyEvent.VK_BACK_SPACE), "${ControlCharacters.ESC}[3~")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ptyConnectorFactory.decorate(
|
return ptyConnectorFactory.decorate(
|
||||||
|
|||||||
@@ -69,4 +69,7 @@ class SftpCommandTerminalTabbedContextMenuExtension private constructor() : Term
|
|||||||
openHostAction.actionPerformed(OpenHostActionEvent(evt.source, host, evt))
|
openHostAction.actionPerformed(OpenHostActionEvent(evt.source, host, evt))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -425,8 +425,11 @@ object SshClients {
|
|||||||
|
|
||||||
|
|
||||||
val heartbeatInterval = max(host.options.heartbeatInterval, 3)
|
val heartbeatInterval = max(host.options.heartbeatInterval, 3)
|
||||||
|
val timeout = Duration.ofSeconds(host.options.extras["timeout"]?.toLongOrNull() ?: 60)
|
||||||
|
|
||||||
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
|
CoreModuleProperties.HEARTBEAT_INTERVAL.set(sshClient, Duration.ofSeconds(heartbeatInterval.toLong()))
|
||||||
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
|
CoreModuleProperties.ALLOW_DHG1_KEX_FALLBACK.set(sshClient, true)
|
||||||
|
CoreModuleProperties.IO_CONNECT_TIMEOUT.set(sshClient, timeout)
|
||||||
|
|
||||||
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) }
|
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) }
|
||||||
|
|
||||||
|
|||||||
@@ -332,6 +332,12 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
|||||||
var top = sr.getOrElse(0) { 1 }
|
var top = sr.getOrElse(0) { 1 }
|
||||||
var bottom = sr.getOrElse(1) { terminalModel.getRows() }
|
var bottom = sr.getOrElse(1) { terminalModel.getRows() }
|
||||||
|
|
||||||
|
// ";r" https://vt100.net/docs/vt510-rm/DECSTBM.html
|
||||||
|
if (sr.size == 1 && args.startsWith(';')) {
|
||||||
|
bottom = top
|
||||||
|
top = 1
|
||||||
|
}
|
||||||
|
|
||||||
if (bottom <= top) {
|
if (bottom <= top) {
|
||||||
if (log.isWarnEnabled) {
|
if (log.isWarnEnabled) {
|
||||||
log.warn("Set Scrolling Region Error. top: $top , bottom: $bottom")
|
log.warn("Set Scrolling Region Error. top: $top , bottom: $bottom")
|
||||||
@@ -520,9 +526,13 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
|||||||
|
|
||||||
val writer = terminalModel.getData(DataKey.TerminalWriter)
|
val writer = terminalModel.getData(DataKey.TerminalWriter)
|
||||||
|
|
||||||
// VT102_RESPONSE
|
if (args.startsWith('>')) {
|
||||||
val bytes = "${ControlCharacters.ESC}[?6c".toByteArray(writer.getCharset())
|
val bytes = "${ControlCharacters.ESC}[>0;276;0c".toByteArray(writer.getCharset())
|
||||||
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
|
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
|
||||||
|
} else {
|
||||||
|
val bytes = "${ControlCharacters.ESC}[?1;2c".toByteArray(writer.getCharset())
|
||||||
|
writer.write(TerminalWriter.WriteRequest.fromBytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
package app.termora.terminal
|
package app.termora.terminal
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
||||||
|
|
||||||
private var startPosition = Position.unknown
|
private var startPosition = Position.unknown
|
||||||
private var endPosition = Position.unknown
|
private var endPosition = Position.unknown
|
||||||
private var block = false
|
private var block = false
|
||||||
private val document = terminal.getDocument()
|
private val document = terminal.getDocument()
|
||||||
|
|
||||||
internal companion object {
|
internal companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(SelectionModelImpl::class.java)
|
||||||
|
|
||||||
fun isPointInsideArea(start: Position, end: Position, x: Int, y: Int, cols: Int): Boolean {
|
fun isPointInsideArea(start: Position, end: Position, x: Int, y: Int, cols: Int): Boolean {
|
||||||
val top = min(start.y, end.y)
|
val top = min(start.y, end.y)
|
||||||
val bottom = max(start.y, end.y)
|
val bottom = max(start.y, end.y)
|
||||||
@@ -49,7 +53,13 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 设置新的选择区域
|
// 设置新的选择区域
|
||||||
setSelection(startPosition, endPosition)
|
try {
|
||||||
|
setSelection(startPosition, endPosition)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isTraceEnabled) {
|
||||||
|
log.trace(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ class TerminalFindPanel(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (index - 1 <= 0) {
|
if (index - 1 <= 0) {
|
||||||
index = 0
|
index = kinds.size - 1
|
||||||
} else {
|
} else {
|
||||||
index--
|
index--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.terminal.panel
|
package app.termora.terminal.panel
|
||||||
|
|
||||||
|
import app.termora.actions.TerminalCopyAction
|
||||||
import app.termora.keymap.KeyShortcut
|
import app.termora.keymap.KeyShortcut
|
||||||
import app.termora.keymap.KeymapManager
|
import app.termora.keymap.KeymapManager
|
||||||
import app.termora.plugin.internal.AltKeyModifier
|
import app.termora.plugin.internal.AltKeyModifier
|
||||||
@@ -71,6 +72,7 @@ class TerminalPanelKeyAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val keyStroke = KeyStroke.getKeyStrokeForEvent(e)
|
val keyStroke = KeyStroke.getKeyStrokeForEvent(e)
|
||||||
|
val keymapActions = activeKeymap.getActionIds(KeyShortcut(keyStroke))
|
||||||
for (action in terminalPanel.getTerminalActions()) {
|
for (action in terminalPanel.getTerminalActions()) {
|
||||||
if (action.test(keyStroke, e)) {
|
if (action.test(keyStroke, e)) {
|
||||||
action.actionPerformed(e)
|
action.actionPerformed(e)
|
||||||
@@ -104,7 +106,9 @@ class TerminalPanelKeyAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果命中了全局快捷键,那么不处理
|
// 如果命中了全局快捷键,那么不处理
|
||||||
if (keyStroke.modifiers != 0 && activeKeymap.getActionIds(KeyShortcut(keyStroke)).isNotEmpty()) {
|
val copyShortcutWithoutSelection =
|
||||||
|
keymapActions.contains(TerminalCopyAction.COPY) && terminal.getSelectionModel().hasSelection().not()
|
||||||
|
if (keyStroke.modifiers != 0 && keymapActions.isNotEmpty() && !copyShortcutWithoutSelection) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,4 +163,4 @@ class TerminalPanelKeyAdapter(
|
|||||||
return Character.toLowerCase(e.keyCode.toChar())
|
return Character.toLowerCase(e.keyCode.toChar())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,30 +53,31 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
|
|||||||
if (SwingUtilities.isRightMouseButton(e)) {
|
if (SwingUtilities.isRightMouseButton(e)) {
|
||||||
// 如果有选中并且开启了选中复制,那么右键直接是粘贴
|
// 如果有选中并且开启了选中复制,那么右键直接是粘贴
|
||||||
if (selectionModel.hasSelection() && isSelectCopy.not()) {
|
if (selectionModel.hasSelection() && isSelectCopy.not()) {
|
||||||
triggerCopyAction(
|
if (rightClickMode != "Nothing") {
|
||||||
KeyEvent(
|
triggerCopyAction(
|
||||||
e.component,
|
|
||||||
KeyEvent.KEY_PRESSED,
|
|
||||||
e.`when`,
|
|
||||||
e.modifiersEx,
|
|
||||||
KeyEvent.VK_C,
|
|
||||||
'C'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (rightClickMode == "CopyAndPaste") {
|
|
||||||
triggerPasteAction(
|
|
||||||
KeyEvent(
|
KeyEvent(
|
||||||
e.component,
|
e.component,
|
||||||
KeyEvent.KEY_PRESSED,
|
KeyEvent.KEY_PRESSED,
|
||||||
e.`when`,
|
e.`when`,
|
||||||
e.modifiersEx,
|
e.modifiersEx,
|
||||||
KeyEvent.VK_V,
|
KeyEvent.VK_C,
|
||||||
'V'
|
'C'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
|
if (rightClickMode == "CopyAndPaste") {
|
||||||
|
triggerPasteAction(
|
||||||
|
KeyEvent(
|
||||||
|
e.component,
|
||||||
|
KeyEvent.KEY_PRESSED,
|
||||||
|
e.`when`,
|
||||||
|
e.modifiersEx,
|
||||||
|
KeyEvent.VK_V,
|
||||||
|
'V'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// paste
|
// paste
|
||||||
triggerPasteAction(
|
triggerPasteAction(
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import java.nio.file.Path
|
|||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
import javax.swing.event.PopupMenuEvent
|
||||||
|
import javax.swing.event.PopupMenuListener
|
||||||
import kotlin.io.path.absolutePathString
|
import kotlin.io.path.absolutePathString
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.reflect.cast
|
import kotlin.reflect.cast
|
||||||
@@ -58,12 +60,24 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
|||||||
private val connectFailedPanel = ConnectFailedPanel()
|
private val connectFailedPanel = ConnectFailedPanel()
|
||||||
private val transferManager = TransferTableModel(coroutineScope)
|
private val transferManager = TransferTableModel(coroutineScope)
|
||||||
private val disposable = Disposer.newDisposable()
|
private val disposable = Disposer.newDisposable()
|
||||||
|
private val focusedWindow get() = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
|
||||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||||
private val questionBtn = JButton(Icons.questionMark)
|
private val questionBtn = JButton(Icons.questionMark)
|
||||||
private val downloadBtn = JButton(Icons.download)
|
private val downloadBtn = JButton(Icons.download)
|
||||||
private val badgePresentation = Badge.getInstance(tab.windowScope)
|
private val badgePresentation = Badge.getInstance(tab.windowScope)
|
||||||
.addBadge(downloadBtn).apply { visible = false }
|
.addBadge(downloadBtn).apply { visible = false }
|
||||||
private val support = DataProviderSupport()
|
private val support = DataProviderSupport()
|
||||||
|
private var isShowPopupMenu = false
|
||||||
|
|
||||||
|
override var isStickHover: Boolean
|
||||||
|
get() = super.isStickHover
|
||||||
|
set(value) {
|
||||||
|
if (isShowPopupMenu || owner != focusedWindow) {
|
||||||
|
super.isStickHover = true
|
||||||
|
} else {
|
||||||
|
super.isStickHover = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initViews()
|
initViews()
|
||||||
@@ -135,6 +149,8 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
questionBtn.toolTipText = I18n.getString("termora.visual-window.transport.question")
|
||||||
|
|
||||||
// 立即连接
|
// 立即连接
|
||||||
connect()
|
connect()
|
||||||
}
|
}
|
||||||
@@ -151,7 +167,7 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
|||||||
val support = DefaultTransportSupport(fileSystem, fileSystem.defaultDir)
|
val support = DefaultTransportSupport(fileSystem, fileSystem.defaultDir)
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
val internalTransferManager = MyInternalTransferManager()
|
val internalTransferManager = MyInternalTransferManager()
|
||||||
val transportPanel = TransportPanel(
|
val transportPanel = object : TransportPanel(
|
||||||
internalTransferManager, tab.host,
|
internalTransferManager, tab.host,
|
||||||
object : TransportSupportLoader {
|
object : TransportSupportLoader {
|
||||||
override suspend fun getTransportSupport(): TransportSupport {
|
override suspend fun getTransportSupport(): TransportSupport {
|
||||||
@@ -165,7 +181,27 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
|||||||
override fun isLoaded(): Boolean {
|
override fun isLoaded(): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
})
|
}) {
|
||||||
|
override fun customizeContextmenu(
|
||||||
|
rows: Array<Int>,
|
||||||
|
e: MouseEvent,
|
||||||
|
popupMenu: TransportPopupMenu
|
||||||
|
) {
|
||||||
|
popupMenu.addPopupMenuListener(object : PopupMenuListener {
|
||||||
|
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent?) {
|
||||||
|
isShowPopupMenu = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent?) {
|
||||||
|
isShowPopupMenu = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popupMenuCanceled(e: PopupMenuEvent?) {
|
||||||
|
isShowPopupMenu = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
internalTransferManager.setTransferPanel(transportPanel)
|
internalTransferManager.setTransferPanel(transportPanel)
|
||||||
Disposer.register(transportPanel, object : Disposable {
|
Disposer.register(transportPanel, object : Disposable {
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
@@ -240,6 +276,10 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
|||||||
super.dispose()
|
super.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun reassemble() {
|
||||||
|
super.reassemble()
|
||||||
|
}
|
||||||
|
|
||||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
return support.getData(dataKey)
|
return support.getData(dataKey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
|||||||
private var dialog: VisualWindowDialog? = null
|
private var dialog: VisualWindowDialog? = null
|
||||||
private var oldBounds = Rectangle()
|
private var oldBounds = Rectangle()
|
||||||
private var toggleWindowBtn = JButton(Icons.openInNewWindow)
|
private var toggleWindowBtn = JButton(Icons.openInNewWindow)
|
||||||
|
private val closeBtn = JButton(Icons.close)
|
||||||
private var isAlwaysTop
|
private var isAlwaysTop
|
||||||
get() = properties.getString("VisualWindow.${id}.dialog.isAlwaysTop", "false").toBoolean()
|
get() = properties.getString("VisualWindow.${id}.dialog.isAlwaysTop", "false").toBoolean()
|
||||||
set(value) = properties.putString("VisualWindow.${id}.dialog.isAlwaysTop", value.toString())
|
set(value) = properties.putString("VisualWindow.${id}.dialog.isAlwaysTop", value.toString())
|
||||||
@@ -47,8 +48,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected var isStickHover = false
|
protected open var isStickHover = false
|
||||||
private set(value) {
|
set(value) {
|
||||||
if (value == field) return
|
if (value == field) return
|
||||||
field = value
|
field = value
|
||||||
reassemble()
|
reassemble()
|
||||||
@@ -92,6 +93,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
|||||||
oldBounds = bounds
|
oldBounds = bounds
|
||||||
alwaysTopBtn.isSelected = isAlwaysTop
|
alwaysTopBtn.isSelected = isAlwaysTop
|
||||||
alwaysTopBtn.isVisible = false
|
alwaysTopBtn.isVisible = false
|
||||||
|
|
||||||
|
closeBtn.toolTipText = I18n.getString("termora.tabbed.contextmenu.close")
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun toolbarButtons(): List<Pair<JButton, Position>> {
|
protected open fun toolbarButtons(): List<Pair<JButton, Position>> {
|
||||||
@@ -134,6 +137,7 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
|||||||
addMouseListener(object : MouseAdapter() {})
|
addMouseListener(object : MouseAdapter() {})
|
||||||
|
|
||||||
toggleWindowBtn.addActionListener { toggleWindow() }
|
toggleWindowBtn.addActionListener { toggleWindow() }
|
||||||
|
toggleWindowBtn.toolTipText = I18n.getString("termora.visual-window.toggle-window")
|
||||||
|
|
||||||
addPropertyChangeListener("isWindow") {
|
addPropertyChangeListener("isWindow") {
|
||||||
if (isWindow) {
|
if (isWindow) {
|
||||||
@@ -165,6 +169,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
|||||||
dialog?.isAlwaysOnTop = isAlwaysTop
|
dialog?.isAlwaysOnTop = isAlwaysTop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeBtn.addActionListener { if (beforeClose()) Disposer.dispose(visualWindow) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initToolBar() {
|
private fun initToolBar() {
|
||||||
@@ -180,7 +186,7 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
|
|||||||
buttons.filter { it.second == Position.Right }.forEach { toolbar.add(it.first) }
|
buttons.filter { it.second == Position.Right }.forEach { toolbar.add(it.first) }
|
||||||
|
|
||||||
toolbar.add(toggleWindowBtn)
|
toolbar.add(toggleWindowBtn)
|
||||||
toolbar.add(JButton(Icons.close).apply { addActionListener { if (beforeClose()) Disposer.dispose(visualWindow) } })
|
toolbar.add(closeBtn)
|
||||||
toolbar.border = BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor)
|
toolbar.border = BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor)
|
||||||
add(toolbar, BorderLayout.NORTH)
|
add(toolbar, BorderLayout.NORTH)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ class BookmarkButton : JButton(Icons.bookmarks) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
isBookmark = false
|
isBookmark = false
|
||||||
|
|
||||||
|
toolTipText = I18n.getString("termora.transport.bookmarks")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showBookmarks(e: MouseEvent) {
|
private fun showBookmarks(e: MouseEvent) {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class TransferAnAction : AnAction(I18n.getString("termora.transport.sftp"), Icon
|
|||||||
val panel = tabbed.getTransportPanel(i) ?: continue
|
val panel = tabbed.getTransportPanel(i) ?: continue
|
||||||
if (panel.host.id == host.id) {
|
if (panel.host.id == host.id) {
|
||||||
tabbed.selectedIndex = i
|
tabbed.selectedIndex = i
|
||||||
|
terminalTabbedManager.setSelectedTerminalTab(sftpTab)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package app.termora.transfer
|
package app.termora.transfer
|
||||||
|
|
||||||
import app.termora.WindowScope
|
|
||||||
import app.termora.plugin.Extension
|
import app.termora.plugin.Extension
|
||||||
|
import java.awt.Window
|
||||||
import java.nio.file.FileSystem
|
import java.nio.file.FileSystem
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import javax.swing.JMenuItem
|
import javax.swing.JMenuItem
|
||||||
@@ -14,7 +14,7 @@ internal interface TransportContextMenuExtension : Extension {
|
|||||||
* @param fileSystem 为 null 表示可能已经断线,处于不可用状态
|
* @param fileSystem 为 null 表示可能已经断线,处于不可用状态
|
||||||
*/
|
*/
|
||||||
fun createJMenuItem(
|
fun createJMenuItem(
|
||||||
windowScope: WindowScope,
|
window: Window,
|
||||||
fileSystem: FileSystem?,
|
fileSystem: FileSystem?,
|
||||||
popupMenu: TransportPopupMenu,
|
popupMenu: TransportPopupMenu,
|
||||||
files: List<Pair<Path, TransportTableModel.Attributes>>
|
files: List<Pair<Path, TransportTableModel.Attributes>>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ import kotlin.io.path.*
|
|||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
internal class TransportPanel(
|
internal open class TransportPanel(
|
||||||
private val internalTransferManager: InternalTransferManager,
|
private val internalTransferManager: InternalTransferManager,
|
||||||
val host: Host,
|
val host: Host,
|
||||||
val loader: TransportSupportLoader,
|
val loader: TransportSupportLoader,
|
||||||
@@ -131,10 +131,10 @@ internal class TransportPanel(
|
|||||||
* 工作目录
|
* 工作目录
|
||||||
*/
|
*/
|
||||||
override var workdir: Path? = null
|
override var workdir: Path? = null
|
||||||
private set
|
protected set
|
||||||
|
|
||||||
override var loading = false
|
override var loading = false
|
||||||
private set(value) {
|
protected set(value) {
|
||||||
val oldValue = field
|
val oldValue = field
|
||||||
field = value
|
field = value
|
||||||
if (oldValue != value) {
|
if (oldValue != value) {
|
||||||
@@ -165,6 +165,14 @@ internal class TransportPanel(
|
|||||||
toolbar.add(eyeBtn)
|
toolbar.add(eyeBtn)
|
||||||
toolbar.add(refreshBtn)
|
toolbar.add(refreshBtn)
|
||||||
|
|
||||||
|
prevBtn.toolTipText = I18n.getString("termora.transport.toolbar.prev")
|
||||||
|
homeBtn.toolTipText = I18n.getString("termora.transport.toolbar.home")
|
||||||
|
nextBtn.toolTipText = I18n.getString("termora.transport.toolbar.next")
|
||||||
|
|
||||||
|
parentBtn.toolTipText = I18n.getString("termora.transport.toolbar.parent")
|
||||||
|
eyeBtn.toolTipText = I18n.getString("termora.transport.toolbar.show-hide")
|
||||||
|
refreshBtn.toolTipText = I18n.getString("termora.transport.toolbar.refresh")
|
||||||
|
|
||||||
sorter.maxSortKeys = 1
|
sorter.maxSortKeys = 1
|
||||||
table.setRowSorter(sorter)
|
table.setRowSorter(sorter)
|
||||||
table.setAutoCreateRowSorter(false)
|
table.setAutoCreateRowSorter(false)
|
||||||
@@ -411,7 +419,7 @@ internal class TransportPanel(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
addPropertyChangeListener("workdir") { evt -> reload() }
|
addPropertyChangeListener("workdir") { _ -> reload() }
|
||||||
|
|
||||||
reload()
|
reload()
|
||||||
}
|
}
|
||||||
@@ -507,6 +515,29 @@ internal class TransportPanel(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
table.actionMap.put("Delete", object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
val rows = table.selectedRows.map { sorter.convertRowIndexToModel(it) }.toTypedArray()
|
||||||
|
val files = rows.map { model.getPath(it) to model.getAttributes(it) }
|
||||||
|
// 排除父目录
|
||||||
|
val validFiles = files.filter { !it.second.isParent }
|
||||||
|
if (validFiles.isNotEmpty()) {
|
||||||
|
// 显示删除确认对话框
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
owner,
|
||||||
|
I18n.getString("termora.keymgr.delete-warning"),
|
||||||
|
messageType = JOptionPane.WARNING_MESSAGE
|
||||||
|
) == JOptionPane.YES_OPTION
|
||||||
|
) {
|
||||||
|
// 直接执行删除操作
|
||||||
|
val future =
|
||||||
|
internalTransferManager.addTransfer(validFiles, InternalTransferManager.TransferMode.Delete)
|
||||||
|
mountFuture(future)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 快速导航
|
// 快速导航
|
||||||
table.addKeyListener(object : KeyAdapter() {
|
table.addKeyListener(object : KeyAdapter() {
|
||||||
override fun keyPressed(e: KeyEvent) {
|
override fun keyPressed(e: KeyEvent) {
|
||||||
@@ -530,12 +561,25 @@ internal class TransportPanel(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 重写全选行为,排除".."父目录
|
||||||
|
table.actionMap.put("selectAll", object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
table.clearSelection()
|
||||||
|
val startRow = if (hasParent) 1 else 0 // 跳过".."行
|
||||||
|
if (startRow < table.rowCount) {
|
||||||
|
table.setRowSelectionInterval(startRow, table.rowCount - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
val inputMap = table.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
|
val inputMap = table.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
|
||||||
if (SystemInfo.isMacOS.not()) {
|
if (SystemInfo.isMacOS.not()) {
|
||||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), "Reload")
|
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), "Reload")
|
||||||
}
|
}
|
||||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "EnterSelectionFolder")
|
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "EnterSelectionFolder")
|
||||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, toolkit.menuShortcutKeyMaskEx), "Reload")
|
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, toolkit.menuShortcutKeyMaskEx), "Reload")
|
||||||
|
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "Delete")
|
||||||
|
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), "Delete")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initTransferHandler() {
|
private fun initTransferHandler() {
|
||||||
@@ -881,13 +925,18 @@ internal class TransportPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showContextmenu(rows: Array<Int>, e: MouseEvent) {
|
protected open fun showContextmenu(rows: Array<Int>, e: MouseEvent) {
|
||||||
val files = rows.map { model.getPath(it) to model.getAttributes(it) }
|
val files = rows.map { model.getPath(it) to model.getAttributes(it) }
|
||||||
val popupMenu = TransportPopupMenu(owner, model, internalTransferManager, loader, files)
|
val popupMenu = TransportPopupMenu(owner, model, internalTransferManager, loader, files)
|
||||||
popupMenu.addActionListener(PopupMenuActionListener(files))
|
popupMenu.addActionListener(PopupMenuActionListener(files))
|
||||||
|
customizeContextmenu(rows, e, popupMenu)
|
||||||
popupMenu.show(table, e.x, e.y)
|
popupMenu.show(table, e.x, e.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected open fun customizeContextmenu(rows: Array<Int>, e: MouseEvent, popupMenu: TransportPopupMenu) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
return support.getData(dataKey)
|
return support.getData(dataKey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import app.termora.*
|
|||||||
import app.termora.plugin.ExtensionManager
|
import app.termora.plugin.ExtensionManager
|
||||||
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
|
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
@@ -41,7 +42,14 @@ internal class TransportPopupMenu(
|
|||||||
private val copyPathMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.copy-path"))
|
private val copyPathMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.copy-path"))
|
||||||
private val copyMenu = JMenuItem(I18n.getString("termora.copy"))
|
private val copyMenu = JMenuItem(I18n.getString("termora.copy"))
|
||||||
private val pasteMenu = JMenuItem(I18n.getString("termora.paste"))
|
private val pasteMenu = JMenuItem(I18n.getString("termora.paste"))
|
||||||
private val openInFinderMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.open-in-folder"))
|
private val openInFinderMenu = JMenuItem(
|
||||||
|
I18n.getString(
|
||||||
|
"termora.transport.table.contextmenu.open-in-folder",
|
||||||
|
if (SystemInfo.isMacOS) I18n.getString("termora.finder")
|
||||||
|
else if (SystemInfo.isWindows) I18n.getString("termora.explorer")
|
||||||
|
else I18n.getString("termora.folder")
|
||||||
|
)
|
||||||
|
)
|
||||||
private val renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename"))
|
private val renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename"))
|
||||||
private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete"))
|
private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete"))
|
||||||
|
|
||||||
@@ -100,7 +108,7 @@ internal class TransportPopupMenu(
|
|||||||
for (extension in extensionManager.getExtensions(TransportContextMenuExtension::class.java)) {
|
for (extension in extensionManager.getExtensions(TransportContextMenuExtension::class.java)) {
|
||||||
try {
|
try {
|
||||||
val menu = extension.createJMenuItem(
|
val menu = extension.createJMenuItem(
|
||||||
ApplicationScope.forWindowScope(owner),
|
owner,
|
||||||
fileSystem,
|
fileSystem,
|
||||||
this,
|
this,
|
||||||
files
|
files
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.termora.transfer.internal.sftp
|
package app.termora.transfer.internal.sftp
|
||||||
|
|
||||||
import app.termora.I18n
|
import app.termora.I18n
|
||||||
import app.termora.WindowScope
|
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.randomUUID
|
import app.termora.randomUUID
|
||||||
@@ -9,6 +8,7 @@ import app.termora.transfer.*
|
|||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.sshd.common.file.util.MockPath
|
import org.apache.sshd.common.file.util.MockPath
|
||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
||||||
|
import java.awt.Window
|
||||||
import java.nio.file.FileSystem
|
import java.nio.file.FileSystem
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import javax.swing.JMenu
|
import javax.swing.JMenu
|
||||||
@@ -23,7 +23,7 @@ internal class CompressTransportContextMenuExtension private constructor() : Tra
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun createJMenuItem(
|
override fun createJMenuItem(
|
||||||
windowScope: WindowScope,
|
window: Window,
|
||||||
fileSystem: FileSystem?,
|
fileSystem: FileSystem?,
|
||||||
popupMenu: TransportPopupMenu,
|
popupMenu: TransportPopupMenu,
|
||||||
files: List<Pair<Path, TransportTableModel.Attributes>>
|
files: List<Pair<Path, TransportTableModel.Attributes>>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package app.termora.transfer.internal.sftp
|
package app.termora.transfer.internal.sftp
|
||||||
|
|
||||||
import app.termora.I18n
|
import app.termora.I18n
|
||||||
import app.termora.WindowScope
|
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.randomUUID
|
import app.termora.randomUUID
|
||||||
import app.termora.transfer.*
|
import app.termora.transfer.*
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
||||||
|
import java.awt.Window
|
||||||
import java.nio.file.FileSystem
|
import java.nio.file.FileSystem
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import javax.swing.JMenu
|
import javax.swing.JMenu
|
||||||
@@ -21,7 +21,7 @@ internal class ExtractTransportContextMenuExtension private constructor() : Tran
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun createJMenuItem(
|
override fun createJMenuItem(
|
||||||
windowScope: WindowScope,
|
window: Window,
|
||||||
fileSystem: FileSystem?,
|
fileSystem: FileSystem?,
|
||||||
popupMenu: TransportPopupMenu,
|
popupMenu: TransportPopupMenu,
|
||||||
files: List<Pair<Path, TransportTableModel.Attributes>>
|
files: List<Pair<Path, TransportTableModel.Attributes>>
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package app.termora.transfer.internal.sftp
|
|||||||
import app.termora.I18n
|
import app.termora.I18n
|
||||||
import app.termora.Icons
|
import app.termora.Icons
|
||||||
import app.termora.OptionPane
|
import app.termora.OptionPane
|
||||||
import app.termora.WindowScope
|
|
||||||
import app.termora.transfer.TransportContextMenuExtension
|
import app.termora.transfer.TransportContextMenuExtension
|
||||||
import app.termora.transfer.TransportPopupMenu
|
import app.termora.transfer.TransportPopupMenu
|
||||||
import app.termora.transfer.TransportTableModel
|
import app.termora.transfer.TransportTableModel
|
||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
||||||
|
import java.awt.Window
|
||||||
import java.nio.file.FileSystem
|
import java.nio.file.FileSystem
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import javax.swing.JMenuItem
|
import javax.swing.JMenuItem
|
||||||
@@ -19,7 +19,7 @@ internal class RmrfTransportContextMenuExtension private constructor() : Transpo
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun createJMenuItem(
|
override fun createJMenuItem(
|
||||||
windowScope: WindowScope,
|
window: Window,
|
||||||
fileSystem: FileSystem?,
|
fileSystem: FileSystem?,
|
||||||
popupMenu: TransportPopupMenu,
|
popupMenu: TransportPopupMenu,
|
||||||
files: List<Pair<Path, TransportTableModel.Attributes>>
|
files: List<Pair<Path, TransportTableModel.Attributes>>
|
||||||
@@ -31,7 +31,7 @@ internal class RmrfTransportContextMenuExtension private constructor() : Transpo
|
|||||||
val rmrfMenu = JMenuItem("rm -rf", Icons.warningIntroduction)
|
val rmrfMenu = JMenuItem("rm -rf", Icons.warningIntroduction)
|
||||||
rmrfMenu.addActionListener {
|
rmrfMenu.addActionListener {
|
||||||
if (OptionPane.showConfirmDialog(
|
if (OptionPane.showConfirmDialog(
|
||||||
windowScope.window,
|
window,
|
||||||
I18n.getString("termora.transport.table.contextmenu.rm-warning"),
|
I18n.getString("termora.transport.table.contextmenu.rm-warning"),
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
) == JOptionPane.YES_OPTION
|
) == JOptionPane.YES_OPTION
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import org.apache.commons.io.IOUtils
|
|||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.sshd.client.SshClient
|
import org.apache.sshd.client.SshClient
|
||||||
import org.apache.sshd.client.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
|
import org.apache.sshd.core.CoreModuleProperties
|
||||||
|
import org.apache.sshd.sftp.SftpModuleProperties
|
||||||
import org.apache.sshd.sftp.client.SftpClientFactory
|
import org.apache.sshd.sftp.client.SftpClientFactory
|
||||||
|
|
||||||
internal class SFTPTransferProtocolProvider : TransferProtocolProvider {
|
internal class SFTPTransferProtocolProvider : TransferProtocolProvider {
|
||||||
@@ -32,6 +34,11 @@ internal class SFTPTransferProtocolProvider : TransferProtocolProvider {
|
|||||||
client = if (owner == null) SshClients.openClient(requester.host)
|
client = if (owner == null) SshClients.openClient(requester.host)
|
||||||
else SshClients.openClient(requester.host, owner)
|
else SshClients.openClient(requester.host, owner)
|
||||||
session = SshClients.openSession(requester.host, client)
|
session = SshClients.openSession(requester.host, client)
|
||||||
|
|
||||||
|
CoreModuleProperties.IO_CONNECT_TIMEOUT.get(client).ifPresent { e ->
|
||||||
|
SftpModuleProperties.SFTP_CHANNEL_OPEN_TIMEOUT.set(session, e)
|
||||||
|
}
|
||||||
|
|
||||||
val fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
val fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
||||||
|
|
||||||
val host = requester.host
|
val host = requester.host
|
||||||
|
|||||||
@@ -146,6 +146,26 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 开启 ToolTip 功能
|
||||||
|
ToolTipManager.sharedInstance().registerComponent(this)
|
||||||
|
|
||||||
|
// 设置鼠标移动提示
|
||||||
|
addMouseMotionListener(object : java.awt.event.MouseMotionAdapter() {
|
||||||
|
override fun mouseMoved(e: MouseEvent) {
|
||||||
|
val path: TreePath? = getPathForLocation(e.x, e.y)
|
||||||
|
if (path != null) {
|
||||||
|
val node: HostTreeNode = path.lastPathComponent as HostTreeNode
|
||||||
|
if (node.host.remark.isNotEmpty()){
|
||||||
|
toolTipText = node.host.remark
|
||||||
|
}else{
|
||||||
|
toolTipText = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toolTipText = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
actionMap.put("copy", object : AnAction() {
|
actionMap.put("copy", object : AnAction() {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
toolkit.systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
|
toolkit.systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
|
||||||
@@ -193,6 +213,9 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
|
// 销毁
|
||||||
|
ToolTipManager.sharedInstance().unregisterComponent(this)
|
||||||
|
|
||||||
val name = super.getName()
|
val name = super.getName()
|
||||||
if (name.isNullOrBlank().not()) {
|
if (name.isNullOrBlank().not()) {
|
||||||
properties.putString("${name}.state", TreeUtils.saveExpansionState(this))
|
properties.putString("${name}.state", TreeUtils.saveExpansionState(this))
|
||||||
@@ -1151,4 +1174,4 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ termora.settings.terminal.hyperlink=Hyperlink
|
|||||||
termora.settings.terminal.select-copy=Select copy
|
termora.settings.terminal.select-copy=Select copy
|
||||||
termora.settings.terminal.right-click=Right click
|
termora.settings.terminal.right-click=Right click
|
||||||
termora.settings.terminal.right-click.copy-and-paste=Copy and Paste
|
termora.settings.terminal.right-click.copy-and-paste=Copy and Paste
|
||||||
|
termora.settings.terminal.right-click.nothing=Nothing
|
||||||
termora.settings.terminal.right-click.copy=${termora.copy}
|
termora.settings.terminal.right-click.copy=${termora.copy}
|
||||||
termora.settings.terminal.cursor-style=Cursor type
|
termora.settings.terminal.cursor-style=Cursor type
|
||||||
termora.settings.terminal.cursor-blink=Cursor blink
|
termora.settings.terminal.cursor-blink=Cursor blink
|
||||||
@@ -89,11 +90,9 @@ termora.settings.plugin.install-from-disk-warning=<b>{0}</b> plugin will have ac
|
|||||||
termora.settings.plugin.not-compatible=The plugin <b>{0}</b> is incompatible with the current version. Please reinstall <b>{0}</b>
|
termora.settings.plugin.not-compatible=The plugin <b>{0}</b> is incompatible with the current version. Please reinstall <b>{0}</b>
|
||||||
|
|
||||||
termora.settings.account=Account
|
termora.settings.account=Account
|
||||||
termora.settings.account.register=Register
|
|
||||||
termora.settings.account.not-support-register=This server does not support account registration
|
|
||||||
termora.settings.account.login=Log in
|
termora.settings.account.login=Log in
|
||||||
termora.settings.account.server=Server
|
termora.settings.account.server=Server
|
||||||
termora.settings.account.mfa=MFA is optional
|
termora.settings.account.wait-login=Waiting for login in the default browser...
|
||||||
termora.settings.account.locally=locally
|
termora.settings.account.locally=locally
|
||||||
termora.settings.account.lifetime=Lifetime
|
termora.settings.account.lifetime=Lifetime
|
||||||
termora.settings.account.upgrade=Upgrade
|
termora.settings.account.upgrade=Upgrade
|
||||||
@@ -116,7 +115,7 @@ termora.settings.keymap=Keymap
|
|||||||
termora.settings.keymap.shortcut=Shortcut
|
termora.settings.keymap.shortcut=Shortcut
|
||||||
termora.settings.keymap.action=Action
|
termora.settings.keymap.action=Action
|
||||||
termora.settings.keymap.already-exists=The shortcut [{0}] is already in use by [{1}]
|
termora.settings.keymap.already-exists=The shortcut [{0}] is already in use by [{1}]
|
||||||
|
termora.settings.keymap.question=Select a line, press the <font color=rgb({0},{1},{2})> ⌫ (Backspace)</font> key to remove the shortcut
|
||||||
|
|
||||||
termora.settings.sftp.edit-command=Edit Command
|
termora.settings.sftp.edit-command=Edit Command
|
||||||
termora.settings.sftp.db-click-behavior=Double-click
|
termora.settings.sftp.db-click-behavior=Double-click
|
||||||
@@ -239,6 +238,8 @@ termora.keymgr.table.name=Name
|
|||||||
termora.keymgr.table.type=Type
|
termora.keymgr.table.type=Type
|
||||||
termora.keymgr.table.length=Length
|
termora.keymgr.table.length=Length
|
||||||
termora.keymgr.table.remark=Description
|
termora.keymgr.table.remark=Description
|
||||||
|
termora.keymgr.export-done=The export was successful
|
||||||
|
termora.keymgr.export-done-open-folder=The export was successful. Do you want to open the folder?
|
||||||
|
|
||||||
termora.keymgr.ssh-copy-id.number=Number of hosts [{0}] Number of public keys [{1}]
|
termora.keymgr.ssh-copy-id.number=Number of hosts [{0}] Number of public keys [{1}]
|
||||||
termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied}
|
termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied}
|
||||||
@@ -248,6 +249,7 @@ termora.keymgr.ssh-copy-id.end=End of public key copying
|
|||||||
|
|
||||||
# Tabbed
|
# Tabbed
|
||||||
termora.tabbed.contextmenu.rename=Rename
|
termora.tabbed.contextmenu.rename=Rename
|
||||||
|
termora.tabbed.contextmenu.select-host=Select Host
|
||||||
termora.tabbed.contextmenu.sftp-command=SFTP Command
|
termora.tabbed.contextmenu.sftp-command=SFTP Command
|
||||||
termora.tabbed.contextmenu.sftp-not-install=SFTP programme not found, please install and try again
|
termora.tabbed.contextmenu.sftp-not-install=SFTP programme not found, please install and try again
|
||||||
termora.tabbed.contextmenu.clone=Clone
|
termora.tabbed.contextmenu.clone=Clone
|
||||||
@@ -308,6 +310,14 @@ termora.tools.multiple=Send command to the current window sessions
|
|||||||
termora.transport.local=Local
|
termora.transport.local=Local
|
||||||
termora.transport.file-already-exists=The file {0} already exists
|
termora.transport.file-already-exists=The file {0} already exists
|
||||||
|
|
||||||
|
|
||||||
|
termora.transport.toolbar.prev=Backward
|
||||||
|
termora.transport.toolbar.home=Home Folder
|
||||||
|
termora.transport.toolbar.next=Forward
|
||||||
|
termora.transport.toolbar.parent=Parent Folder
|
||||||
|
termora.transport.toolbar.show-hide=Show/Hide Folders
|
||||||
|
termora.transport.toolbar.refresh=Refresh Folder
|
||||||
|
|
||||||
termora.transport.bookmarks=Bookmarks Manager
|
termora.transport.bookmarks=Bookmarks Manager
|
||||||
termora.transport.bookmarks.up=Up
|
termora.transport.bookmarks.up=Up
|
||||||
termora.transport.bookmarks.down=Down
|
termora.transport.bookmarks.down=Down
|
||||||
@@ -324,7 +334,7 @@ termora.transport.table.owner=Owner
|
|||||||
termora.transport.table.contextmenu.transfer=Transfer
|
termora.transport.table.contextmenu.transfer=Transfer
|
||||||
termora.transport.table.contextmenu.edit=${termora.keymgr.edit}
|
termora.transport.table.contextmenu.edit=${termora.keymgr.edit}
|
||||||
termora.transport.table.contextmenu.copy-path=Copy Path
|
termora.transport.table.contextmenu.copy-path=Copy Path
|
||||||
termora.transport.table.contextmenu.open-in-folder=Open in ${termora.finder}
|
termora.transport.table.contextmenu.open-in-folder=Open in {0}
|
||||||
termora.transport.table.contextmenu.rename=${termora.welcome.contextmenu.rename}
|
termora.transport.table.contextmenu.rename=${termora.welcome.contextmenu.rename}
|
||||||
termora.transport.table.contextmenu.delete=${termora.remove}
|
termora.transport.table.contextmenu.delete=${termora.remove}
|
||||||
termora.transport.table.contextmenu.rm-warning=Using the rm -rf command to delete a file is very dangerous
|
termora.transport.table.contextmenu.rm-warning=Using the rm -rf command to delete a file is very dangerous
|
||||||
@@ -430,6 +440,8 @@ termora.visual-window.system-information.mem=Mem
|
|||||||
termora.visual-window.system-information.swap=Swap
|
termora.visual-window.system-information.swap=Swap
|
||||||
termora.visual-window.system-information.filesystem=Filesystem
|
termora.visual-window.system-information.filesystem=Filesystem
|
||||||
termora.visual-window.system-information.used-total=Used / Total
|
termora.visual-window.system-information.used-total=Used / Total
|
||||||
|
termora.visual-window.toggle-window=Toggle window
|
||||||
|
termora.visual-window.transport.question=More Features
|
||||||
|
|
||||||
|
|
||||||
termora.visual-window.nvidia-smi=NVIDIA SMI
|
termora.visual-window.nvidia-smi=NVIDIA SMI
|
||||||
|
|||||||
@@ -49,7 +49,26 @@ termora.setting.security.enter-password-again=Повторите пароль
|
|||||||
termora.setting.security.password-is-different=Пароли отличаются
|
termora.setting.security.password-is-different=Пароли отличаются
|
||||||
termora.setting.security.mnemonic-note=Сохраните мнемоническую фразу в надежном месте, она может помочь восстановить данные, если вы забудете пароль
|
termora.setting.security.mnemonic-note=Сохраните мнемоническую фразу в надежном месте, она может помочь восстановить данные, если вы забудете пароль
|
||||||
|
|
||||||
|
termora.settings.account=Учётная запись
|
||||||
|
termora.settings.account.login=Войти
|
||||||
|
termora.settings.account.server=Сервер
|
||||||
|
termora.settings.account.wait-login=Ожидание входа в браузере по умолчанию...
|
||||||
|
termora.settings.account.locally=локально
|
||||||
|
termora.settings.account.lifetime=Время действия
|
||||||
|
termora.settings.account.upgrade=Обновить
|
||||||
|
termora.settings.account.verify=Подтвердить
|
||||||
|
termora.settings.account.subscription=Подписка
|
||||||
|
termora.settings.account.valid-to=Действительна до
|
||||||
|
termora.settings.account.synchronization-on=Синхронизация вкл
|
||||||
|
termora.settings.account.sync-now = Синхронизировать сейчас
|
||||||
|
termora.settings.account.logout = Выйти
|
||||||
|
termora.settings.account.logout-confirm = Вы уверены, что хотите выйти?
|
||||||
|
termora.settings.account.unsynced-logout-confirm = Несинхронизировано Вы уверены, что хотите выйти?
|
||||||
|
termora.settings.account.server-singapore = Сингапур
|
||||||
|
termora.settings.account.server-china = Материковый Китай
|
||||||
|
termora.settings.account.new-server = Новый сервер
|
||||||
|
termora.settings.account.deploy-server = Развернуть
|
||||||
|
termora.settings.account.login-failed = Не удалось войти, повторите попытку позже
|
||||||
|
|
||||||
|
|
||||||
termora.settings.terminal=Терминал
|
termora.settings.terminal=Терминал
|
||||||
@@ -62,6 +81,7 @@ termora.settings.terminal.hyperlink=Ссылки
|
|||||||
termora.settings.terminal.select-copy=Копировать выделенное
|
termora.settings.terminal.select-copy=Копировать выделенное
|
||||||
termora.settings.terminal.right-click=правой кнопкой мыши
|
termora.settings.terminal.right-click=правой кнопкой мыши
|
||||||
termora.settings.terminal.right-click.copy-and-paste=Копировать и вставить
|
termora.settings.terminal.right-click.copy-and-paste=Копировать и вставить
|
||||||
|
termora.settings.terminal.right-click.nothing=никто
|
||||||
termora.settings.terminal.cursor-style=Вид курсора
|
termora.settings.terminal.cursor-style=Вид курсора
|
||||||
termora.settings.terminal.cursor-blink=Мигать курсором
|
termora.settings.terminal.cursor-blink=Мигать курсором
|
||||||
termora.settings.terminal.local-shell=Локальный терминал
|
termora.settings.terminal.local-shell=Локальный терминал
|
||||||
@@ -69,27 +89,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.done=Синхронизация успешна
|
|
||||||
termora.settings.sync.export=${termora.keymgr.export}
|
|
||||||
termora.settings.sync.import=${termora.keymgr.import}
|
|
||||||
termora.settings.sync.import.file-too-large=Файл слишком большой
|
|
||||||
termora.settings.sync.import.successful=Импортировано успешно
|
|
||||||
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.range.keyword-highlights=${termora.highlight}
|
|
||||||
termora.settings.sync.last-sync-time=Последняя синхронизация
|
|
||||||
termora.settings.sync.gist=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=Ссылка
|
||||||
@@ -101,7 +100,7 @@ termora.settings.keymap=Горяиче клавиши
|
|||||||
termora.settings.keymap.shortcut=Комбинация
|
termora.settings.keymap.shortcut=Комбинация
|
||||||
termora.settings.keymap.action=Команда
|
termora.settings.keymap.action=Команда
|
||||||
termora.settings.keymap.already-exists=Комбинация [{0}] уже используется [{1}]
|
termora.settings.keymap.already-exists=Комбинация [{0}] уже используется [{1}]
|
||||||
|
termora.settings.keymap.question=Выберите строку и нажмите <font color=rgb({0},{1},{2})> ⌫ (клавишу Backspace)</font>, чтобы удалить сочетание клавиш
|
||||||
|
|
||||||
termora.settings.sftp.edit-command=Редактировать команду
|
termora.settings.sftp.edit-command=Редактировать команду
|
||||||
termora.settings.sftp.db-click-behavior=двойной щелчок
|
termora.settings.sftp.db-click-behavior=двойной щелчок
|
||||||
@@ -204,6 +203,8 @@ termora.keymgr.table.name=Название
|
|||||||
termora.keymgr.table.type=Тип
|
termora.keymgr.table.type=Тип
|
||||||
termora.keymgr.table.length=Длина
|
termora.keymgr.table.length=Длина
|
||||||
termora.keymgr.table.remark=Описание
|
termora.keymgr.table.remark=Описание
|
||||||
|
termora.keymgr.export-done=Экспорт выполнен успешно
|
||||||
|
termora.keymgr.export-done-open-folder=Экспорт выполнен успешно. Открыть папку?
|
||||||
|
|
||||||
termora.keymgr.ssh-copy-id.number=Кол-во хостов [{0}] Кол-во публичных ключей [{1}]
|
termora.keymgr.ssh-copy-id.number=Кол-во хостов [{0}] Кол-во публичных ключей [{1}]
|
||||||
termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied}
|
termora.keymgr.ssh-copy-id.successful=${termora.terminal.copied}
|
||||||
@@ -212,6 +213,7 @@ termora.keymgr.ssh-copy-id.end=Копирования открытого клю
|
|||||||
|
|
||||||
# Tabbed
|
# Tabbed
|
||||||
termora.tabbed.contextmenu.rename=Переименовать
|
termora.tabbed.contextmenu.rename=Переименовать
|
||||||
|
termora.tabbed.contextmenu.select-host=Выбрать хост
|
||||||
termora.tabbed.contextmenu.sftp-command=SFTP Команда
|
termora.tabbed.contextmenu.sftp-command=SFTP Команда
|
||||||
termora.tabbed.contextmenu.sftp-not-install=Программа SFTP не найдена, пожалуйста, установите и повторите попытку.
|
termora.tabbed.contextmenu.sftp-not-install=Программа SFTP не найдена, пожалуйста, установите и повторите попытку.
|
||||||
termora.tabbed.contextmenu.clone=Дублировать
|
termora.tabbed.contextmenu.clone=Дублировать
|
||||||
@@ -269,6 +271,14 @@ termora.transport.bookmarks=Менеджер закладок
|
|||||||
termora.transport.bookmarks.up=Вверх
|
termora.transport.bookmarks.up=Вверх
|
||||||
termora.transport.bookmarks.down=Вниз
|
termora.transport.bookmarks.down=Вниз
|
||||||
|
|
||||||
|
termora.transport.toolbar.prev=Назад
|
||||||
|
termora.transport.toolbar.home=Домашняя папка
|
||||||
|
termora.transport.toolbar.next=Вперёд
|
||||||
|
termora.transport.toolbar.parent=Родительская папка
|
||||||
|
termora.transport.toolbar.show-hide=Показать/Скрыть папки
|
||||||
|
termora.transport.toolbar.refresh=Обновить
|
||||||
|
|
||||||
|
|
||||||
termora.transport.table.filename=Имя файла
|
termora.transport.table.filename=Имя файла
|
||||||
termora.transport.table.type=Тип
|
termora.transport.table.type=Тип
|
||||||
termora.transport.table.type.symbolic-link=Символьная Ссылка
|
termora.transport.table.type.symbolic-link=Символьная Ссылка
|
||||||
@@ -376,6 +386,8 @@ termora.visual-window.system-information.mem=Память
|
|||||||
termora.visual-window.system-information.swap=Подкачка
|
termora.visual-window.system-information.swap=Подкачка
|
||||||
termora.visual-window.system-information.filesystem=Файловая система
|
termora.visual-window.system-information.filesystem=Файловая система
|
||||||
termora.visual-window.system-information.used-total=Использовано / Всего
|
termora.visual-window.system-information.used-total=Использовано / Всего
|
||||||
|
termora.visual-window.toggle-window=Переключить окно
|
||||||
|
termora.visual-window.transport.question=Больше возможностей
|
||||||
|
|
||||||
|
|
||||||
termora.visual-window.nvidia-smi=NVIDIA SMI
|
termora.visual-window.nvidia-smi=NVIDIA SMI
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ termora.settings.terminal.hyperlink=超链接
|
|||||||
termora.settings.terminal.select-copy=选中复制
|
termora.settings.terminal.select-copy=选中复制
|
||||||
termora.settings.terminal.right-click=右键点击
|
termora.settings.terminal.right-click=右键点击
|
||||||
termora.settings.terminal.right-click.copy-and-paste=复制 & 粘贴
|
termora.settings.terminal.right-click.copy-and-paste=复制 & 粘贴
|
||||||
|
termora.settings.terminal.right-click.nothing=无操作
|
||||||
termora.settings.terminal.cursor-style=光标样式
|
termora.settings.terminal.cursor-style=光标样式
|
||||||
termora.settings.terminal.cursor-blink=光标闪烁
|
termora.settings.terminal.cursor-blink=光标闪烁
|
||||||
termora.settings.terminal.local-shell=本地终端
|
termora.settings.terminal.local-shell=本地终端
|
||||||
@@ -103,10 +104,8 @@ termora.settings.plugin.not-compatible=插件 <b>{0}</b> 与当前版本不兼
|
|||||||
|
|
||||||
termora.settings.account=账号
|
termora.settings.account=账号
|
||||||
termora.settings.account.login=登录
|
termora.settings.account.login=登录
|
||||||
termora.settings.account.register=注册
|
|
||||||
termora.settings.account.not-support-register=该服务器不支持注册账号
|
|
||||||
termora.settings.account.server=服务器
|
termora.settings.account.server=服务器
|
||||||
termora.settings.account.mfa=多因素验证是可选的
|
termora.settings.account.wait-login=正在等待默认浏览器中登录...
|
||||||
termora.settings.account.locally=本地的
|
termora.settings.account.locally=本地的
|
||||||
termora.settings.account.lifetime=长期
|
termora.settings.account.lifetime=长期
|
||||||
termora.settings.account.verify=验证
|
termora.settings.account.verify=验证
|
||||||
@@ -127,7 +126,7 @@ termora.settings.keymap=键盘
|
|||||||
termora.settings.keymap.shortcut=快捷键
|
termora.settings.keymap.shortcut=快捷键
|
||||||
termora.settings.keymap.action=操作
|
termora.settings.keymap.action=操作
|
||||||
termora.settings.keymap.already-exists=快捷键 [{0}] 已经被 [{1}] 占用
|
termora.settings.keymap.already-exists=快捷键 [{0}] 已经被 [{1}] 占用
|
||||||
|
termora.settings.keymap.question=选中一行,按下 <font color=rgb({0},{1},{2})> ⌫ (退格键)</font> 移除快捷键
|
||||||
|
|
||||||
termora.settings.sftp.edit-command=编辑命令
|
termora.settings.sftp.edit-command=编辑命令
|
||||||
termora.settings.sftp.db-click-behavior=双击行为
|
termora.settings.sftp.db-click-behavior=双击行为
|
||||||
@@ -232,6 +231,8 @@ termora.keymgr.table.name=名称
|
|||||||
termora.keymgr.table.type=类型
|
termora.keymgr.table.type=类型
|
||||||
termora.keymgr.table.length=长度
|
termora.keymgr.table.length=长度
|
||||||
termora.keymgr.table.remark=备注
|
termora.keymgr.table.remark=备注
|
||||||
|
termora.keymgr.export-done=导出成功
|
||||||
|
termora.keymgr.export-done-open-folder=导出成功,是否需要打开所在文件夹?
|
||||||
|
|
||||||
termora.keymgr.ssh-copy-id.number=主机数量 [{0}] 公钥数量 [{1}]
|
termora.keymgr.ssh-copy-id.number=主机数量 [{0}] 公钥数量 [{1}]
|
||||||
termora.keymgr.ssh-copy-id.failed=复制失败
|
termora.keymgr.ssh-copy-id.failed=复制失败
|
||||||
@@ -244,6 +245,7 @@ termora.tools.multiple=将命令发送到当前窗口会话
|
|||||||
|
|
||||||
# Tabbed
|
# Tabbed
|
||||||
termora.tabbed.contextmenu.rename=重命名
|
termora.tabbed.contextmenu.rename=重命名
|
||||||
|
termora.tabbed.contextmenu.select-host=选中主机
|
||||||
termora.tabbed.contextmenu.sftp-command=SFTP 终端
|
termora.tabbed.contextmenu.sftp-command=SFTP 终端
|
||||||
termora.tabbed.contextmenu.sftp-not-install=没有找到 SFTP 程序,请安装后重试
|
termora.tabbed.contextmenu.sftp-not-install=没有找到 SFTP 程序,请安装后重试
|
||||||
termora.tabbed.contextmenu.clone=克隆
|
termora.tabbed.contextmenu.clone=克隆
|
||||||
@@ -309,6 +311,15 @@ termora.transport.bookmarks=书签管理
|
|||||||
termora.transport.bookmarks.up=上移
|
termora.transport.bookmarks.up=上移
|
||||||
termora.transport.bookmarks.down=下移
|
termora.transport.bookmarks.down=下移
|
||||||
|
|
||||||
|
|
||||||
|
termora.transport.toolbar.prev=返回
|
||||||
|
termora.transport.toolbar.home=默认目录
|
||||||
|
termora.transport.toolbar.next=前进
|
||||||
|
termora.transport.toolbar.parent=父目录
|
||||||
|
termora.transport.toolbar.show-hide=显示/隐藏目录
|
||||||
|
termora.transport.toolbar.refresh=刷新
|
||||||
|
|
||||||
|
|
||||||
termora.transport.table.filename=文件名
|
termora.transport.table.filename=文件名
|
||||||
termora.transport.table.type=类型
|
termora.transport.table.type=类型
|
||||||
termora.transport.table.size=大小
|
termora.transport.table.size=大小
|
||||||
@@ -320,7 +331,7 @@ termora.transport.table.owner=所有者
|
|||||||
# contextmenu
|
# contextmenu
|
||||||
termora.transport.table.contextmenu.transfer=传输
|
termora.transport.table.contextmenu.transfer=传输
|
||||||
termora.transport.table.contextmenu.copy-path=复制路径
|
termora.transport.table.contextmenu.copy-path=复制路径
|
||||||
termora.transport.table.contextmenu.open-in-folder=在${termora.finder}中打开
|
termora.transport.table.contextmenu.open-in-folder=在{0}中打开
|
||||||
termora.transport.table.contextmenu.change-permissions=更改权限...
|
termora.transport.table.contextmenu.change-permissions=更改权限...
|
||||||
termora.transport.table.contextmenu.refresh=刷新
|
termora.transport.table.contextmenu.refresh=刷新
|
||||||
termora.transport.table.contextmenu.compress=压缩
|
termora.transport.table.contextmenu.compress=压缩
|
||||||
@@ -426,6 +437,8 @@ termora.visual-window.system-information.mem=内存
|
|||||||
termora.visual-window.system-information.swap=交换
|
termora.visual-window.system-information.swap=交换
|
||||||
termora.visual-window.system-information.filesystem=文件系统
|
termora.visual-window.system-information.filesystem=文件系统
|
||||||
termora.visual-window.system-information.used-total=使用 / 大小
|
termora.visual-window.system-information.used-total=使用 / 大小
|
||||||
|
termora.visual-window.toggle-window=切换窗口
|
||||||
|
termora.visual-window.transport.question=更多功能
|
||||||
|
|
||||||
termora.floating-toolbar.close-in-current-tab=在当前标签页关闭
|
termora.floating-toolbar.close-in-current-tab=在当前标签页关闭
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ termora.settings.keymap=鍵盤
|
|||||||
termora.settings.keymap.shortcut=快捷鍵
|
termora.settings.keymap.shortcut=快捷鍵
|
||||||
termora.settings.keymap.action=操作
|
termora.settings.keymap.action=操作
|
||||||
termora.settings.keymap.already-exists=快捷鍵 [{0}] 已經被 [{1}] 占用
|
termora.settings.keymap.already-exists=快捷鍵 [{0}] 已經被 [{1}] 占用
|
||||||
|
termora.settings.keymap.question=選取一行,按下 <font color=rgb({0},{1},{2})> ⌫ (退格鍵)</font> 移除快速鍵
|
||||||
|
|
||||||
termora.settings.sftp.edit-command=編輯命令
|
termora.settings.sftp.edit-command=編輯命令
|
||||||
termora.settings.sftp.db-click-behavior=按兩下行為
|
termora.settings.sftp.db-click-behavior=按兩下行為
|
||||||
@@ -84,6 +85,7 @@ termora.settings.terminal.hyperlink=超連結
|
|||||||
termora.settings.terminal.select-copy=選取複製
|
termora.settings.terminal.select-copy=選取複製
|
||||||
termora.settings.terminal.right-click=右鍵點擊
|
termora.settings.terminal.right-click=右鍵點擊
|
||||||
termora.settings.terminal.right-click.copy-and-paste=複製 & 貼上
|
termora.settings.terminal.right-click.copy-and-paste=複製 & 貼上
|
||||||
|
termora.settings.terminal.right-click.nothing=無操作
|
||||||
termora.settings.terminal.cursor-style=遊標風格
|
termora.settings.terminal.cursor-style=遊標風格
|
||||||
termora.settings.terminal.cursor-blink=遊標閃爍
|
termora.settings.terminal.cursor-blink=遊標閃爍
|
||||||
termora.settings.terminal.local-shell=本地端
|
termora.settings.terminal.local-shell=本地端
|
||||||
@@ -114,10 +116,8 @@ termora.settings.plugin.not-compatible=插件 <b>{0}</b> 與目前版本不相
|
|||||||
|
|
||||||
termora.settings.account=帳號
|
termora.settings.account=帳號
|
||||||
termora.settings.account.login=登入
|
termora.settings.account.login=登入
|
||||||
termora.settings.account.register=註冊
|
|
||||||
termora.settings.account.not-support-register=此伺服器不支援註冊帳號
|
|
||||||
termora.settings.account.server=伺服器
|
termora.settings.account.server=伺服器
|
||||||
termora.settings.account.mfa=多因素驗證是可選的
|
termora.settings.account.wait-login=正在等待預設瀏覽器登入...
|
||||||
termora.settings.account.locally=本地的
|
termora.settings.account.locally=本地的
|
||||||
termora.settings.account.lifetime=長期
|
termora.settings.account.lifetime=長期
|
||||||
termora.settings.account.verify=驗證
|
termora.settings.account.verify=驗證
|
||||||
@@ -228,6 +228,8 @@ termora.keymgr.table.name=名稱
|
|||||||
termora.keymgr.table.type=型別
|
termora.keymgr.table.type=型別
|
||||||
termora.keymgr.table.length=長度
|
termora.keymgr.table.length=長度
|
||||||
termora.keymgr.table.remark=備註
|
termora.keymgr.table.remark=備註
|
||||||
|
termora.keymgr.export-done=匯出成功
|
||||||
|
termora.keymgr.export-done-open-folder=匯出成功,是否需要打開所在資料夾?
|
||||||
|
|
||||||
termora.keymgr.ssh-copy-id.number=主機數量 [{0}] 公鑰數量 [{1}]
|
termora.keymgr.ssh-copy-id.number=主機數量 [{0}] 公鑰數量 [{1}]
|
||||||
termora.keymgr.ssh-copy-id.failed=複製失敗
|
termora.keymgr.ssh-copy-id.failed=複製失敗
|
||||||
@@ -239,6 +241,7 @@ termora.tools.multiple=將命令傳送到目前視窗會話
|
|||||||
|
|
||||||
# Tabbed
|
# Tabbed
|
||||||
termora.tabbed.contextmenu.rename=重新命名
|
termora.tabbed.contextmenu.rename=重新命名
|
||||||
|
termora.tabbed.contextmenu.select-host=選取主機
|
||||||
termora.tabbed.contextmenu.sftp-command=SFTP 終端
|
termora.tabbed.contextmenu.sftp-command=SFTP 終端
|
||||||
termora.tabbed.contextmenu.sftp-not-install=沒有找到 SFTP 程序,請安裝後重試
|
termora.tabbed.contextmenu.sftp-not-install=沒有找到 SFTP 程序,請安裝後重試
|
||||||
termora.tabbed.contextmenu.clone=克隆
|
termora.tabbed.contextmenu.clone=克隆
|
||||||
@@ -304,6 +307,13 @@ termora.transport.bookmarks=書籤管理
|
|||||||
termora.transport.bookmarks.up=上移
|
termora.transport.bookmarks.up=上移
|
||||||
termora.transport.bookmarks.down=下移
|
termora.transport.bookmarks.down=下移
|
||||||
|
|
||||||
|
termora.transport.toolbar.prev=返回
|
||||||
|
termora.transport.toolbar.home=預設目錄
|
||||||
|
termora.transport.toolbar.next=前進
|
||||||
|
termora.transport.toolbar.parent=父目錄
|
||||||
|
termora.transport.toolbar.show-hide=顯示/隱藏目錄
|
||||||
|
termora.transport.toolbar.refresh=重新整理
|
||||||
|
|
||||||
termora.transport.table.filename=檔名
|
termora.transport.table.filename=檔名
|
||||||
termora.transport.table.type=類型
|
termora.transport.table.type=類型
|
||||||
termora.transport.table.size=大小
|
termora.transport.table.size=大小
|
||||||
@@ -315,7 +325,7 @@ termora.transport.table.owner=所有者
|
|||||||
# contextmenu
|
# contextmenu
|
||||||
termora.transport.table.contextmenu.transfer=傳輸
|
termora.transport.table.contextmenu.transfer=傳輸
|
||||||
termora.transport.table.contextmenu.copy-path=複製路徑
|
termora.transport.table.contextmenu.copy-path=複製路徑
|
||||||
termora.transport.table.contextmenu.open-in-folder=在${termora.finder}中打開
|
termora.transport.table.contextmenu.open-in-folder=在{0}中打開
|
||||||
termora.transport.table.contextmenu.change-permissions=更改權限...
|
termora.transport.table.contextmenu.change-permissions=更改權限...
|
||||||
termora.transport.table.contextmenu.refresh=刷新
|
termora.transport.table.contextmenu.refresh=刷新
|
||||||
termora.transport.table.contextmenu.compress=壓縮
|
termora.transport.table.contextmenu.compress=壓縮
|
||||||
@@ -413,6 +423,8 @@ termora.visual-window.system-information.mem=內存
|
|||||||
termora.visual-window.system-information.swap=交換
|
termora.visual-window.system-information.swap=交換
|
||||||
termora.visual-window.system-information.filesystem=檔案系統
|
termora.visual-window.system-information.filesystem=檔案系統
|
||||||
termora.visual-window.system-information.used-total=使用 / 大小
|
termora.visual-window.system-information.used-total=使用 / 大小
|
||||||
|
termora.visual-window.toggle-window=切換視窗
|
||||||
|
termora.visual-window.transport.question=更多功能
|
||||||
|
|
||||||
termora.floating-toolbar.close-in-current-tab=在目前標籤頁關閉
|
termora.floating-toolbar.close-in-current-tab=在目前標籤頁關閉
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ DisableProgramGroupPage=yes
|
|||||||
;PrivilegesRequired=lowest
|
;PrivilegesRequired=lowest
|
||||||
OutputDir={#MyOutputDir}
|
OutputDir={#MyOutputDir}
|
||||||
OutputBaseFilename={#MyAppName}-{#MyAppVersion}
|
OutputBaseFilename={#MyAppName}-{#MyAppVersion}
|
||||||
|
Compression=lzma2/max
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
WizardStyle=classic
|
WizardStyle=classic
|
||||||
;WizardStyle=modern
|
;WizardStyle=modern
|
||||||
|
|||||||
@@ -15,6 +15,19 @@ class KeyUtilsTest {
|
|||||||
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair("ssh-rsa", 1024).public), 1024)
|
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair("ssh-rsa", 1024).public), 1024)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_ECDSA() {
|
||||||
|
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP256, 256).private), 256)
|
||||||
|
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP256, 256).public), 256)
|
||||||
|
|
||||||
|
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP384, 384).private), 384)
|
||||||
|
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP384, 384).public), 384)
|
||||||
|
|
||||||
|
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP521, 521).private), 521)
|
||||||
|
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP521, 521).public), 521)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test_ed25519() {
|
fun test_ed25519() {
|
||||||
val keyPair = KeyUtils.generateKeyPair(KeyPairProvider.SSH_ED25519, 256)
|
val keyPair = KeyUtils.generateKeyPair(KeyPairProvider.SSH_ED25519, 256)
|
||||||
|
|||||||
12
src/test/resources/issue-1055/Dockerfile
Normal file
12
src/test/resources/issue-1055/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
FROM debian:bookworm
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
RUN sed -i 's|http://deb.debian.org/debian|http://mirrors.aliyun.com/debian|g' /etc/apt/sources.list.d/debian.sources \
|
||||||
|
&& sed -i 's|http://security.debian.org/debian-security|http://mirrors.aliyun.com/debian-security|g' /etc/apt/sources.list.d/debian.sources
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates autoconf libevent-dev bison automake libtool pkg-config build-essential libncurses-dev
|
||||||
|
|
||||||
|
RUN git clone https://github.com/tmux/tmux.git && cd tmux && sh autogen.sh && ./configure && make && make install
|
||||||
|
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
FROM linuxserver/openssh-server
|
FROM linuxserver/openssh-server:9.3_p2-r1-ls147
|
||||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
|
||||||
&& apk update && apk add wget tmux gcc zip p7zip g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
|
&& apk update && apk add wget tmux gcc zip p7zip g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
|
||||||
&& tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \
|
&& tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \
|
||||||
&& ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz
|
&& ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz
|
||||||
@@ -7,3 +7,6 @@ RUN sed -i 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/ssh
|
|||||||
RUN sed -i 's/AllowTcpForwarding no/AllowTcpForwarding yes/g' /etc/ssh/sshd_config
|
RUN sed -i 's/AllowTcpForwarding no/AllowTcpForwarding yes/g' /etc/ssh/sshd_config
|
||||||
RUN sed -i 's/GatewayPorts no/GatewayPorts yes/g' /etc/ssh/sshd_config
|
RUN sed -i 's/GatewayPorts no/GatewayPorts yes/g' /etc/ssh/sshd_config
|
||||||
RUN sed -i 's/X11Forwarding no/X11Forwarding yes/g' /etc/ssh/sshd_config
|
RUN sed -i 's/X11Forwarding no/X11Forwarding yes/g' /etc/ssh/sshd_config
|
||||||
|
|
||||||
|
# docker build -t sshd .
|
||||||
|
# docker run --rm -it -e SUDO_ACCESS=true -e PASSWORD_ACCESS=true -e USER_PASSWORD=123456 -p 2222:2222 sshd
|
||||||
Reference in New Issue
Block a user