mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
139 Commits
2.0.0-beta
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5c524f761 | ||
|
|
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 | ||
|
|
bd9b73ad6a | ||
|
|
dbea769994 | ||
|
|
9cd83c4025 | ||
|
|
d4cc080e7b | ||
|
|
a324bc3d96 | ||
|
|
36929e9ea3 | ||
|
|
dd73b933d9 | ||
|
|
117a9ea692 | ||
|
|
2f932de295 | ||
|
|
679b24a74d | ||
|
|
c6b33ea828 | ||
|
|
a4ea8f2491 | ||
|
|
1c2315b5e9 | ||
|
|
d48e412580 | ||
|
|
3b3fb41384 | ||
|
|
190ac697fb | ||
|
|
8cdbf24cdc | ||
|
|
6e182b6813 | ||
|
|
3fa4064655 | ||
|
|
a77a03d8b3 | ||
|
|
5f8b9d36e2 | ||
|
|
1ed5e164de | ||
|
|
c67d5b0276 | ||
|
|
9646a98f6d | ||
|
|
aee34415a7 | ||
|
|
e4e70cc72c | ||
|
|
49779fe8f2 | ||
|
|
969ddc3662 | ||
|
|
de9b418c75 | ||
|
|
f8588745cd | ||
|
|
7c0cbab187 | ||
|
|
176fa64de0 | ||
|
|
495ab69195 | ||
|
|
93c28242fb | ||
|
|
57662f717b | ||
|
|
3669bd1f88 | ||
|
|
00e695b7d5 | ||
|
|
02c92e6019 | ||
|
|
8ba74f0846 | ||
|
|
79ed6d3858 | ||
|
|
8a66606275 | ||
|
|
3ebdf73fbf | ||
|
|
d249e5da5a | ||
|
|
7243e933e6 | ||
|
|
f92e43ee41 | ||
|
|
f6243e33da | ||
|
|
72f334d572 | ||
|
|
68cbb10dec | ||
|
|
45f5c4ee91 | ||
|
|
48c511613e | ||
|
|
c94063d459 | ||
|
|
c26aafd831 | ||
|
|
a5638329e7 | ||
|
|
8323f8eb5d | ||
|
|
35199ed094 | ||
|
|
b5d53cf416 | ||
|
|
39e26a6e3d | ||
|
|
15cb06af0f | ||
|
|
1e0bbb5a00 | ||
|
|
fb6fdbc14c | ||
|
|
96df53ce40 | ||
|
|
42f86dc3a3 | ||
|
|
32b11c6063 | ||
|
|
b2f43ba439 |
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:
|
||||||
|
|||||||
17
.github/workflows/osx.yml
vendored
17
.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:
|
||||||
@@ -81,6 +81,10 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
- name: Install create-dmg
|
||||||
|
shell: bash
|
||||||
|
run: brew install create-dmg
|
||||||
|
|
||||||
- name: Compile
|
- name: Compile
|
||||||
shell: bash
|
shell: bash
|
||||||
run: ./gradlew :check-license && ./gradlew classes -x test
|
run: ./gradlew :check-license && ./gradlew classes -x test
|
||||||
@@ -93,13 +97,6 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: ./gradlew :jpackage && ./gradlew :dist
|
run: ./gradlew :jpackage && ./gradlew :dist
|
||||||
|
|
||||||
- name: Upload zip artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: termora-osx-zip-${{ runner.arch }}
|
|
||||||
path: |
|
|
||||||
build/distributions/*.zip
|
|
||||||
|
|
||||||
- name: Upload dmg artifact
|
- name: Upload dmg artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@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:
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ Termora is developed using [**Kotlin/JVM**](https://kotlinlang.org/) and partial
|
|||||||
- 🧾 [Latest Release](https://github.com/TermoraDev/termora/releases/latest)
|
- 🧾 [Latest Release](https://github.com/TermoraDev/termora/releases/latest)
|
||||||
- 🍺 **Homebrew**: `brew install --cask termora`
|
- 🍺 **Homebrew**: `brew install --cask termora`
|
||||||
- 🔨 **WinGet**: `winget install termora`
|
- 🔨 **WinGet**: `winget install termora`
|
||||||
|
- <img src="https://apps.microsoft.com/assets/icons/logo-16x16.png" alt="microsoft logo"/> <b>Microsoft Store</b>: <a href="https://apps.microsoft.com/store/detail/9NRZBHG43SB9?cid=DevShareMCLPCS">Visit Termora in the Microsoft Store</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ Termora 使用 [**Kotlin/JVM**](https://kotlinlang.org/) 开发,支持(正
|
|||||||
- 🧾 [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
- 🧾 [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||||
- 🍺 **Homebrew**:`brew install --cask termora`
|
- 🍺 **Homebrew**:`brew install --cask termora`
|
||||||
- 🪟 **WinGet**:`winget install termora`
|
- 🪟 **WinGet**:`winget install termora`
|
||||||
|
- <img src="https://apps.microsoft.com/assets/icons/logo-16x16.png" alt="microsoft logo"/> <b>Microsoft Store</b>: <a href="https://apps.microsoft.com/store/detail/9NRZBHG43SB9?cid=DevShareMCLPCS">Termora</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ val appVersion = project.version.toString().split("-")[0]
|
|||||||
val makeAppx = if (os.isWindows) StringUtils.defaultString(System.getenv("MAKEAPPX_PATH")) else StringUtils.EMPTY
|
val makeAppx = if (os.isWindows) StringUtils.defaultString(System.getenv("MAKEAPPX_PATH")) else StringUtils.EMPTY
|
||||||
val isDeb = os.isLinux && System.getenv("TERMORA_TYPE") == "deb"
|
val isDeb = os.isLinux && System.getenv("TERMORA_TYPE") == "deb"
|
||||||
val isAppx = os.isWindows && makeAppx.isNotBlank() && System.getenv("TERMORA_TYPE") == "appx"
|
val isAppx = os.isWindows && makeAppx.isNotBlank() && System.getenv("TERMORA_TYPE") == "appx"
|
||||||
|
val isBeta = project.version.toString().contains("beta", ignoreCase = true)
|
||||||
|
|
||||||
// macOS 签名信息
|
// macOS 签名信息
|
||||||
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
||||||
@@ -173,10 +174,12 @@ publishing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.processResources {
|
tasks.processResources {
|
||||||
|
val betaVersion = project.version.toString().substringAfterLast('.')
|
||||||
filesMatching("**/AppxManifest.xml") {
|
filesMatching("**/AppxManifest.xml") {
|
||||||
filter<ReplaceTokens>(
|
filter<ReplaceTokens>(
|
||||||
"tokens" to mapOf(
|
"tokens" to mapOf(
|
||||||
"version" to appVersion,
|
"version" to appVersion,
|
||||||
|
"betaVersion" to if (isBeta) betaVersion else "0",
|
||||||
"architecture" to if (arch.isArm64) "arm64" else "x64",
|
"architecture" to if (arch.isArm64) "arm64" else "x64",
|
||||||
"projectDir" to project.projectDir.absolutePath,
|
"projectDir" to project.projectDir.absolutePath,
|
||||||
)
|
)
|
||||||
@@ -229,9 +232,11 @@ tasks.register<Copy>("copy-dependencies") {
|
|||||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/dragonflybsd-*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/dragonflybsd-*") }
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
|
||||||
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
|
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
|
||||||
val targetDir = FileUtils.getFile(dylib, pty4j.name, if (os.isWindows) "win32" else "linux")
|
val osName = if (os.isWindows) "win32" else if (os.isMacOsX) "darwin" else "linux"
|
||||||
FileUtils.forceMkdir(targetDir)
|
|
||||||
val myArchName = if (arch.isArm) "aarch64" else "x86-64"
|
val myArchName = if (arch.isArm) "aarch64" else "x86-64"
|
||||||
|
val targetDir = if (os.isMacOsX) FileUtils.getFile(dylib, pty4j.name, osName)
|
||||||
|
else FileUtils.getFile(dylib, pty4j.name, osName, myArchName)
|
||||||
|
FileUtils.forceMkdir(targetDir)
|
||||||
if (os.isWindows) {
|
if (os.isWindows) {
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/*win/${myArchName}/*", "-d", targetDir.absolutePath) }
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/*win/${myArchName}/*", "-d", targetDir.absolutePath) }
|
||||||
@@ -334,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(
|
||||||
@@ -379,6 +385,7 @@ tasks.register<Exec>("jpackage") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (os.isLinux) {
|
if (os.isLinux) {
|
||||||
|
options.add("--add-opens=java.desktop/sun.awt.X11=ALL-UNNAMED")
|
||||||
if (isDeb) {
|
if (isDeb) {
|
||||||
options.add("-Djpackage.app-layout=deb")
|
options.add("-Djpackage.app-layout=deb")
|
||||||
}
|
}
|
||||||
@@ -398,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"))
|
||||||
@@ -441,7 +436,7 @@ tasks.register<Exec>("jpackage") {
|
|||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (os.isMacOsX && macOSSign) {
|
if (macOSSign) {
|
||||||
arguments.add("--mac-sign")
|
arguments.add("--mac-sign")
|
||||||
arguments.add("--mac-signing-key-user-name")
|
arguments.add("--mac-signing-key-user-name")
|
||||||
arguments.add(macOSSignUsername)
|
arguments.add(macOSSignUsername)
|
||||||
@@ -548,7 +543,16 @@ fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: Str
|
|||||||
"/DMyAppVersion=${appVersion}",
|
"/DMyAppVersion=${appVersion}",
|
||||||
"/DMyOutputDir=${distributionDir.asFile.absolutePath}",
|
"/DMyOutputDir=${distributionDir.asFile.absolutePath}",
|
||||||
"/DMySetupIconFile=${FileUtils.getFile(projectDir, "src", "main", "resources", "icons", "termora.ico")}",
|
"/DMySetupIconFile=${FileUtils.getFile(projectDir, "src", "main", "resources", "icons", "termora.ico")}",
|
||||||
"/DMyWizardSmallImageFile=${FileUtils.getFile(projectDir, "src", "main", "resources", "icons", "termora_128x128.bmp")}",
|
"/DMyWizardSmallImageFile=${
|
||||||
|
FileUtils.getFile(
|
||||||
|
projectDir,
|
||||||
|
"src",
|
||||||
|
"main",
|
||||||
|
"resources",
|
||||||
|
"icons",
|
||||||
|
"termora_128x128.bmp"
|
||||||
|
)
|
||||||
|
}",
|
||||||
"/DMySourceDir=${FileUtils.getFile(dir, projectName).absolutePath}",
|
"/DMySourceDir=${FileUtils.getFile(dir, projectName).absolutePath}",
|
||||||
"/F${finalFilenameWithoutExtension}",
|
"/F${finalFilenameWithoutExtension}",
|
||||||
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
|
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
|
||||||
@@ -658,7 +662,7 @@ fun packOnLinux(distributionDir: Directory, finalFilenameWithoutExtension: Strin
|
|||||||
commandLine(
|
commandLine(
|
||||||
"wget",
|
"wget",
|
||||||
"-O", appimagetool.absolutePath,
|
"-O", appimagetool.absolutePath,
|
||||||
"https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-${if (arch.isArm) "aarch64" else "x86_64"}.AppImage"
|
"https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${if (arch.isArm) "aarch64" else "x86_64"}.AppImage"
|
||||||
)
|
)
|
||||||
workingDir = distributionDir.asFile
|
workingDir = distributionDir.asFile
|
||||||
}
|
}
|
||||||
@@ -667,17 +671,24 @@ fun packOnLinux(distributionDir: Directory, finalFilenameWithoutExtension: Strin
|
|||||||
exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
|
exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Desktop file
|
// Desktop file
|
||||||
val termoraName = project.name.uppercaseFirstChar()
|
val termoraName = project.name.uppercaseFirstChar()
|
||||||
|
|
||||||
|
// copy icon
|
||||||
|
FileUtils.copyFile(
|
||||||
|
File("${projectDir.absolutePath}/src/main/resources/icons/termora_256x256.png"),
|
||||||
|
distributionDir.file(termoraName + File.separator + termoraName + ".png").asFile
|
||||||
|
)
|
||||||
|
|
||||||
val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile
|
val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile
|
||||||
desktopFile.writeText(
|
desktopFile.writeText(
|
||||||
"""[Desktop Entry]
|
"""[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=${termoraName}
|
Name=${termoraName}
|
||||||
Comment=Terminal emulator and SSH client
|
Comment=Terminal emulator and SSH client
|
||||||
Icon=/lib/${termoraName}
|
Icon=${termoraName}
|
||||||
Categories=Development;
|
Categories=Development;
|
||||||
|
StartupWMClass=${termoraName}
|
||||||
Terminal=false
|
Terminal=false
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,50 +1,50 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.2.0"
|
kotlin = "2.2.21"
|
||||||
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.6.2"
|
||||||
kotlinx-serialization-json = "1.9.0"
|
kotlinx-serialization-json = "1.9.0"
|
||||||
commons-codec = "1.18.0"
|
commons-codec = "1.20.0"
|
||||||
commons-lang3 = "3.18.0"
|
commons-lang3 = "3.20.0"
|
||||||
commons-csv = "1.14.0"
|
commons-csv = "1.14.1"
|
||||||
commons-net = "3.11.1"
|
commons-net = "3.12.0"
|
||||||
commons-text = "1.13.1"
|
commons-text = "1.14.0"
|
||||||
commons-compress = "1.27.1"
|
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.19.0"
|
commons-io = "2.20.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.7"
|
||||||
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.0"
|
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.1"
|
||||||
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-4"
|
exposed = "1.0.0-rc-2"
|
||||||
h2 = "2.3.232"
|
h2 = "2.3.232"
|
||||||
sqlite = "3.50.2.0"
|
sqlite = "3.50.3.0"
|
||||||
jug = "5.1.0"
|
jug = "5.1.1"
|
||||||
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" }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
project.version = "0.0.5"
|
project.version = "0.0.6"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,4 +18,8 @@ object Appearance {
|
|||||||
set(value) {
|
set(value) {
|
||||||
enableManager.setFlag("Plugins.bg.interval", value)
|
enableManager.setFlag("Plugins.bg.interval", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fillMode: String
|
||||||
|
get() = enableManager.getFlag("Plugins.bg.fillMode", FillMode.STRETCH.name)
|
||||||
|
set(value) = enableManager.setFlag("Plugins.bg.fillMode", value)
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,8 @@ package app.termora.plugins.bg
|
|||||||
|
|
||||||
import app.termora.GlassPaneExtension
|
import app.termora.GlassPaneExtension
|
||||||
import app.termora.WindowScope
|
import app.termora.WindowScope
|
||||||
|
import app.termora.restore
|
||||||
|
import app.termora.save
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import java.awt.AlphaComposite
|
import java.awt.AlphaComposite
|
||||||
import java.awt.Graphics2D
|
import java.awt.Graphics2D
|
||||||
@@ -12,15 +14,52 @@ class BGGlassPaneExtension private constructor() : GlassPaneExtension {
|
|||||||
val instance = BGGlassPaneExtension()
|
val instance = BGGlassPaneExtension()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun paint(scope: WindowScope, c: JComponent, g2d: Graphics2D) {
|
override fun paint(scope: WindowScope, c: JComponent, g2d: Graphics2D) {
|
||||||
|
|
||||||
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
|
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
|
||||||
|
g2d.save()
|
||||||
g2d.composite = AlphaComposite.getInstance(
|
g2d.composite = AlphaComposite.getInstance(
|
||||||
AlphaComposite.SRC_OVER,
|
AlphaComposite.SRC_OVER,
|
||||||
if (FlatLaf.isLafDark()) 0.2f else 0.1f
|
if (FlatLaf.isLafDark()) 0.2f else 0.1f
|
||||||
)
|
)
|
||||||
g2d.drawImage(img, 0, 0, c.width, c.height, null)
|
|
||||||
g2d.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER)
|
when (Appearance.fillMode) {
|
||||||
|
FillMode.STRETCH.name -> {
|
||||||
|
g2d.drawImage(img, 0, 0, c.width, c.height, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
FillMode.CENTER.name -> {
|
||||||
|
val x = (c.width - img.width) / 2
|
||||||
|
val y = (c.height - img.height) / 2
|
||||||
|
g2d.drawImage(img, x, y, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
FillMode.TILE.name -> {
|
||||||
|
val iw = img.width
|
||||||
|
val ih = img.height
|
||||||
|
var y = 0
|
||||||
|
while (y < c.height) {
|
||||||
|
var x = 0
|
||||||
|
while (x < c.width) {
|
||||||
|
g2d.drawImage(img, x, y, null)
|
||||||
|
x += iw
|
||||||
|
}
|
||||||
|
y += ih
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FillMode.FIT.name -> {
|
||||||
|
val scale = maxOf(c.width.toDouble() / img.width, c.height.toDouble() / img.height)
|
||||||
|
val newW = (img.width * scale).toInt()
|
||||||
|
val newH = (img.height * scale).toInt()
|
||||||
|
val x = (c.width - newW) / 2
|
||||||
|
val y = (c.height - newH) / 2
|
||||||
|
g2d.drawImage(img, x, y, newW, newH, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g2d.restore()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,8 @@ import org.apache.commons.lang3.StringUtils
|
|||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.event.ItemEvent
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.StandardCopyOption
|
import java.nio.file.StandardCopyOption
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
@@ -23,6 +25,7 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
|||||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
val backgroundImageTextField = OutlineTextField()
|
val backgroundImageTextField = OutlineTextField()
|
||||||
|
val fillModeComboBox = OutlineComboBox<FillMode>()
|
||||||
val intervalSpinner = NumberSpinner(360, minimum = 30, maximum = 86400)
|
val intervalSpinner = NumberSpinner(360, minimum = 30, maximum = 86400)
|
||||||
|
|
||||||
private val backgroundButton = JButton(Icons.folder)
|
private val backgroundButton = JButton(Icons.folder)
|
||||||
@@ -36,6 +39,38 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
|||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
|
|
||||||
|
fillModeComboBox.addItem(FillMode.STRETCH)
|
||||||
|
fillModeComboBox.addItem(FillMode.FIT)
|
||||||
|
fillModeComboBox.addItem(FillMode.CENTER)
|
||||||
|
fillModeComboBox.addItem(FillMode.TILE)
|
||||||
|
|
||||||
|
fillModeComboBox.selectedItem = runCatching { FillMode.valueOf(Appearance.fillMode) }
|
||||||
|
.getOrNull() ?: FillMode.STRETCH
|
||||||
|
|
||||||
|
fillModeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component? {
|
||||||
|
var text = value?.toString()
|
||||||
|
|
||||||
|
if (value == FillMode.STRETCH) {
|
||||||
|
text = BGI18n.getString("termora.plugins.bg.fill-mode.stretch")
|
||||||
|
} else if (value == FillMode.FIT) {
|
||||||
|
text = BGI18n.getString("termora.plugins.bg.fill-mode.fit")
|
||||||
|
} else if (value == FillMode.CENTER) {
|
||||||
|
text = BGI18n.getString("termora.plugins.bg.fill-mode.center")
|
||||||
|
} else if (value == FillMode.TILE) {
|
||||||
|
text = BGI18n.getString("termora.plugins.bg.fill-mode.tile")
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
backgroundImageTextField.isEditable = false
|
backgroundImageTextField.isEditable = false
|
||||||
backgroundImageTextField.trailingComponent = backgroundButton
|
backgroundImageTextField.trailingComponent = backgroundButton
|
||||||
backgroundImageTextField.text = Appearance.backgroundImage
|
backgroundImageTextField.text = Appearance.backgroundImage
|
||||||
@@ -80,6 +115,15 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
|||||||
Appearance.interval = value
|
Appearance.interval = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fillModeComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
Appearance.fillMode = fillModeComboBox.selectedItem?.toString() ?: FillMode.STRETCH.name
|
||||||
|
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
||||||
|
SwingUtilities.invokeLater { SwingUtilities.updateComponentTreeUI(frame) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSelectedBackgroundImage(file: File) {
|
private fun onSelectedBackgroundImage(file: File) {
|
||||||
@@ -124,7 +168,7 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
|||||||
private fun getFormPanel(): JPanel {
|
private fun getFormPanel(): JPanel {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default",
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default",
|
||||||
"pref, $FORM_MARGIN, pref"
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rows = 1
|
var rows = 1
|
||||||
@@ -138,6 +182,10 @@ class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
|||||||
.add(bgClearBox).xy(5, rows)
|
.add(bgClearBox).xy(5, rows)
|
||||||
.apply { rows += step }
|
.apply { rows += step }
|
||||||
|
|
||||||
|
builder.add("${BGI18n.getString("termora.plugins.bg.fill-mode")}:").xy(1, rows)
|
||||||
|
.add(fillModeComboBox).xy(3, rows)
|
||||||
|
.apply { rows += step }
|
||||||
|
|
||||||
builder.add("${BGI18n.getString("termora.plugins.bg.interval")}:").xy(1, rows)
|
builder.add("${BGI18n.getString("termora.plugins.bg.interval")}:").xy(1, rows)
|
||||||
.add(intervalSpinner).xy(3, rows)
|
.add(intervalSpinner).xy(3, rows)
|
||||||
.apply { rows += step }
|
.apply { rows += step }
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
enum class FillMode {
|
||||||
|
STRETCH, // 拉伸
|
||||||
|
FIT, // 等比例铺满
|
||||||
|
CENTER, // 居中
|
||||||
|
TILE, // 平铺
|
||||||
|
}
|
||||||
@@ -1,2 +1,7 @@
|
|||||||
termora.plugins.bg.interval=Interval
|
termora.plugins.bg.interval=Interval
|
||||||
|
termora.plugins.bg.fill-mode=Fill Mode
|
||||||
|
termora.plugins.bg.fill-mode.stretch=Stretch
|
||||||
|
termora.plugins.bg.fill-mode.fit=Fit
|
||||||
|
termora.plugins.bg.fill-mode.center=Center
|
||||||
|
termora.plugins.bg.fill-mode.tile=Tile
|
||||||
termora.plugins.bg.background-image=Background Image
|
termora.plugins.bg.background-image=Background Image
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
termora.plugins.bg.background-image=背景图
|
termora.plugins.bg.background-image=背景图
|
||||||
termora.plugins.bg.interval=切换间隔
|
termora.plugins.bg.interval=切换间隔
|
||||||
|
|
||||||
|
termora.plugins.bg.fill-mode=填充模式
|
||||||
|
termora.plugins.bg.fill-mode.stretch=拉伸
|
||||||
|
termora.plugins.bg.fill-mode.fit=适合
|
||||||
|
termora.plugins.bg.fill-mode.center=居中
|
||||||
|
termora.plugins.bg.fill-mode.tile=平铺
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
termora.plugins.bg.background-image=背景圖
|
termora.plugins.bg.background-image=背景圖
|
||||||
termora.plugins.bg.interval=切換間隔
|
termora.plugins.bg.interval=切換間隔
|
||||||
|
|
||||||
|
termora.plugins.bg.fill-mode=填充模式
|
||||||
|
termora.plugins.bg.fill-mode.stretch=拉伸
|
||||||
|
termora.plugins.bg.fill-mode.fit=適合
|
||||||
|
termora.plugins.bg.fill-mode.center=居中
|
||||||
|
termora.plugins.bg.fill-mode.tile=平鋪
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ 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"))
|
||||||
implementation("com.qcloud:cos_api:5.6.247")
|
implementation("com.qcloud:cos_api:5.6.259")
|
||||||
compileOnly(project(":"))
|
compileOnly(project(":"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ plugins {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
project.version = "0.0.6"
|
project.version = "0.0.8"
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
package app.termora.plugins.editor
|
|
||||||
|
|
||||||
import app.termora.DialogWrapper
|
|
||||||
import app.termora.Disposable
|
|
||||||
import app.termora.Disposer
|
|
||||||
import app.termora.OptionPane
|
|
||||||
import java.awt.Dimension
|
|
||||||
import java.awt.Window
|
|
||||||
import java.awt.event.WindowAdapter
|
|
||||||
import java.awt.event.WindowEvent
|
|
||||||
import java.io.File
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import javax.swing.JComponent
|
|
||||||
import javax.swing.JOptionPane
|
|
||||||
import javax.swing.UIManager
|
|
||||||
import kotlin.io.path.absolutePathString
|
|
||||||
import kotlin.io.path.name
|
|
||||||
|
|
||||||
|
|
||||||
class EditorDialog(file: Path, owner: Window, private val myDisposable: Disposable) : DialogWrapper(null) {
|
|
||||||
|
|
||||||
private val filename = file.name
|
|
||||||
private val filepath = File(file.absolutePathString())
|
|
||||||
private val editorPanel = EditorPanel(this, filepath)
|
|
||||||
private val disposed = AtomicBoolean()
|
|
||||||
|
|
||||||
init {
|
|
||||||
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
|
||||||
isModal = false
|
|
||||||
controlsVisible = true
|
|
||||||
isResizable = true
|
|
||||||
title = filename
|
|
||||||
iconImages = owner.iconImages
|
|
||||||
escapeDispose = false
|
|
||||||
defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
|
||||||
|
|
||||||
initEvents()
|
|
||||||
|
|
||||||
setLocationRelativeTo(owner)
|
|
||||||
|
|
||||||
init()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun initEvents() {
|
|
||||||
|
|
||||||
addWindowListener(object : WindowAdapter() {
|
|
||||||
override fun windowClosing(e: WindowEvent?) {
|
|
||||||
if (disposed.compareAndSet(false, true)) {
|
|
||||||
doCancelAction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Disposer.register(myDisposable, object : Disposable {
|
|
||||||
override fun dispose() {
|
|
||||||
if (disposed.compareAndSet(false, true)) {
|
|
||||||
doCancelAction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Disposer.register(disposable, object : Disposable {
|
|
||||||
override fun dispose() {
|
|
||||||
if (disposed.compareAndSet(false, true)) {
|
|
||||||
Disposer.dispose(myDisposable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun doCancelAction() {
|
|
||||||
if (editorPanel.changes()) {
|
|
||||||
if (OptionPane.showConfirmDialog(
|
|
||||||
this,
|
|
||||||
"文件尚未保存,你确定要退出吗?",
|
|
||||||
optionType = JOptionPane.OK_CANCEL_OPTION,
|
|
||||||
) != JOptionPane.OK_OPTION
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.doCancelAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
|
||||||
return editorPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createSouthPanel(): JComponent? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package app.termora.plugins.editor
|
||||||
|
|
||||||
|
import app.termora.Disposable
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.EnableManager
|
||||||
|
import app.termora.OptionPane
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import javax.swing.JFrame
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.UIManager
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
import kotlin.io.path.name
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class EditorFrame(private val file: Path, private val owner: Window, private val disposable: Disposable) : JFrame() {
|
||||||
|
private val enableManager get() = EnableManager.getInstance()
|
||||||
|
private val disposed = AtomicBoolean()
|
||||||
|
private val filepath = File(file.absolutePathString())
|
||||||
|
private val frame get() = this
|
||||||
|
private val editorPanel = EditorPanel(this, filepath)
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvent() {
|
||||||
|
|
||||||
|
Disposer.register(disposable, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
if (disposed.compareAndSet(false, true)) frame.dispose()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowClosed(e: WindowEvent) {
|
||||||
|
if (disposed.compareAndSet(false, true)) Disposer.dispose(disposable)
|
||||||
|
enableManager.setFlag("Plugins.editor.dialog.width", width)
|
||||||
|
enableManager.setFlag("Plugins.editor.dialog.height", height)
|
||||||
|
enableManager.setFlag("Plugins.editor.dialog.extendedState", extendedState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun windowClosing(e: WindowEvent?) {
|
||||||
|
if (editorPanel.changes()) {
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
frame,
|
||||||
|
EditorI18n.getString("termora.plugins.editor.not-save"),
|
||||||
|
optionType = JOptionPane.OK_CANCEL_OPTION,
|
||||||
|
) == JOptionPane.OK_OPTION
|
||||||
|
) {
|
||||||
|
frame.dispose()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
frame.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||||
|
val state = enableManager.getFlag("Plugins.editor.dialog.extendedState", 0)
|
||||||
|
|
||||||
|
if ((state and MAXIMIZED_BOTH) == MAXIMIZED_BOTH) {
|
||||||
|
frame.setLocationRelativeTo(null)
|
||||||
|
frame.extendedState = state
|
||||||
|
} else {
|
||||||
|
val mySize = size
|
||||||
|
mySize.width = max(enableManager.getFlag("Plugins.editor.dialog.width", mySize.width), mySize.width)
|
||||||
|
mySize.height = max(enableManager.getFlag("Plugins.editor.dialog.height", mySize.height), mySize.height)
|
||||||
|
size = mySize
|
||||||
|
setLocationRelativeTo(owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
title = file.name
|
||||||
|
iconImages = owner.iconImages
|
||||||
|
defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
||||||
|
|
||||||
|
rootPane.contentPane.layout = BorderLayout()
|
||||||
|
rootPane.contentPane.add(editorPanel, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package app.termora.plugins.editor
|
||||||
|
|
||||||
|
import app.termora.NamedI18n
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
object EditorI18n : NamedI18n("i18n/messages") {
|
||||||
|
private val log = LoggerFactory.getLogger(EditorI18n::class.java)
|
||||||
|
|
||||||
|
override fun getLogger(): Logger {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -35,10 +32,14 @@ import javax.swing.event.DocumentEvent
|
|||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class EditorPanel(private val window: JDialog, private val file: File) : JPanel(BorderLayout()) {
|
class EditorPanel(private val window: JFrame, private val file: File) : JPanel(BorderLayout()) {
|
||||||
|
|
||||||
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: JDialog, private val file: File) : JPanel(
|
|||||||
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: JDialog, private val file: File) : JPanel(
|
|||||||
)
|
)
|
||||||
|
|
||||||
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: JDialog, private val file: File) : JPanel(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class MyTransportEditFileExtension private constructor() : TransportEditFileExte
|
|||||||
|
|
||||||
override fun edit(owner: Window, path: Path): Disposable {
|
override fun edit(owner: Window, path: Path): Disposable {
|
||||||
val disposable = Disposer.newDisposable()
|
val disposable = Disposer.newDisposable()
|
||||||
SwingUtilities.invokeLater { EditorDialog(path, owner, disposable).isVisible = true }
|
SwingUtilities.invokeLater { EditorFrame(path, owner, disposable).isVisible = true }
|
||||||
return disposable
|
return disposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
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=Формат
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
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=格式化
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
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 |
@@ -2,14 +2,14 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
}
|
}
|
||||||
|
|
||||||
project.version = "0.0.7"
|
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-202507070058")
|
implementation("com.github.hstyi:geolite2:v1.0-202510270056")
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ project.version = "0.0.2"
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.5")
|
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.7")
|
||||||
compileOnly(project(":"))
|
compileOnly(project(":"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(":"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ plugins {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
project.version = "0.0.4"
|
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")
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package app.termora.plugins.serial
|
package app.termora.plugins.serial
|
||||||
|
|
||||||
import app.termora.Host
|
import app.termora.*
|
||||||
import app.termora.Icons
|
|
||||||
import app.termora.PtyHostTerminalTab
|
|
||||||
import app.termora.WindowScope
|
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
import org.apache.commons.io.Charsets
|
import org.apache.commons.io.Charsets
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
@@ -11,6 +8,8 @@ import javax.swing.Icon
|
|||||||
|
|
||||||
class SerialTerminalTab(windowScope: WindowScope, host: Host) :
|
class SerialTerminalTab(windowScope: WindowScope, host: Host) :
|
||||||
PtyHostTerminalTab(windowScope, host) {
|
PtyHostTerminalTab(windowScope, host) {
|
||||||
|
|
||||||
|
|
||||||
override suspend fun openPtyConnector(): PtyConnector {
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
val serialPort = Serials.openPort(host)
|
val serialPort = Serials.openPort(host)
|
||||||
return SerialPortPtyConnector(
|
return SerialPortPtyConnector(
|
||||||
@@ -19,6 +18,10 @@ class SerialTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createReconnectTerminalTab(): TerminalTab {
|
||||||
|
return SerialTerminalTab(windowScope, host)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getIcon(): Icon {
|
override fun getIcon(): Icon {
|
||||||
return Icons.plugin
|
return Icons.plugin
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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=網域
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
project.version = "0.0.3"
|
project.version = "0.0.4"
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -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=最後同步時間
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ enum class AppLayout {
|
|||||||
* macOS
|
* macOS
|
||||||
*/
|
*/
|
||||||
App,
|
App,
|
||||||
|
AppStore,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Linux
|
* Linux
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
internal class ApplePressAndHoldEnabledApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = ApplePressAndHoldEnabledApplicationRunnerExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ready() {
|
||||||
|
if (SystemInfo.isMacOS.not()) return
|
||||||
|
|
||||||
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
|
Runtime.getRuntime()
|
||||||
|
.exec(arrayOf("defaults", "write", "app.termora", "ApplePressAndHoldEnabled", "-bool", "false"))
|
||||||
|
.waitFor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import org.apache.commons.lang3.SystemUtils
|
|||||||
import org.apache.commons.lang3.math.NumberUtils
|
import org.apache.commons.lang3.math.NumberUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.tinylog.configuration.Configuration
|
import org.tinylog.configuration.Configuration
|
||||||
|
import java.awt.Toolkit
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
@@ -35,10 +36,31 @@ class ApplicationInitializr {
|
|||||||
// 检查是否单例
|
// 检查是否单例
|
||||||
checkSingleton()
|
checkSingleton()
|
||||||
|
|
||||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
if (SystemInfo.isMacOS) {
|
||||||
System.setProperty("apple.awt.application.name", Application.getName())
|
System.setProperty("apple.awt.application.name", Application.getName())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (SystemInfo.isLinux) {
|
||||||
|
// https://stackoverflow.com/questions/10593075
|
||||||
|
runCatching {
|
||||||
|
val toolkit = Toolkit.getDefaultToolkit()
|
||||||
|
val awtAppClassNameField = toolkit.javaClass.getDeclaredField("awtAppClassName")
|
||||||
|
awtAppClassNameField.setAccessible(true)
|
||||||
|
awtAppClassNameField.set(toolkit, Application.getName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -127,12 +127,19 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected open fun createSouthPanel(): JComponent? {
|
protected open fun createSouthPanel(): JComponent? {
|
||||||
|
val westSourcePanel = createWestSourcePanel()
|
||||||
val box = Box.createHorizontalBox()
|
val box = Box.createHorizontalBox()
|
||||||
|
|
||||||
|
if (westSourcePanel != null) {
|
||||||
|
box.add(westSourcePanel)
|
||||||
|
} else {
|
||||||
|
box.add(Box.createHorizontalGlue())
|
||||||
|
}
|
||||||
|
|
||||||
box.border = BorderFactory.createCompoundBorder(
|
box.border = BorderFactory.createCompoundBorder(
|
||||||
BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor),
|
BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor),
|
||||||
BorderFactory.createEmptyBorder(8, 12, 8, 12)
|
BorderFactory.createEmptyBorder(8, 12, 8, 12)
|
||||||
)
|
)
|
||||||
box.add(Box.createHorizontalGlue())
|
|
||||||
|
|
||||||
val actions = createActions()
|
val actions = createActions()
|
||||||
for (i in actions.size - 1 downTo 0) {
|
for (i in actions.size - 1 downTo 0) {
|
||||||
@@ -145,6 +152,10 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
|
|||||||
return box
|
return box
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected open fun createWestSourcePanel(): JComponent? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
protected open fun createActions(): List<AbstractAction> {
|
protected open fun createActions(): List<AbstractAction> {
|
||||||
return listOf(createOkAction(), createCancelAction())
|
return listOf(createOkAction(), createCancelAction())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ internal class FramePlugin : InternalPlugin() {
|
|||||||
init {
|
init {
|
||||||
support.addExtension(DatabasePropertiesChangedExtension::class.java) { KeymapRefresher.getInstance() }
|
support.addExtension(DatabasePropertiesChangedExtension::class.java) { KeymapRefresher.getInstance() }
|
||||||
support.addExtension(DatabaseChangedExtension::class.java) { KeymapRefresher.getInstance() }
|
support.addExtension(DatabaseChangedExtension::class.java) { KeymapRefresher.getInstance() }
|
||||||
|
support.addExtension(ApplicationRunnerExtension::class.java) { ApplePressAndHoldEnabledApplicationRunnerExtension.instance }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
|
|||||||
@@ -3,14 +3,16 @@ package app.termora
|
|||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.ControlCharacters
|
||||||
|
import app.termora.terminal.DataKey
|
||||||
|
import app.termora.terminal.DataListener
|
||||||
|
import app.termora.terminal.Terminal
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.beans.PropertyChangeEvent
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
|
|
||||||
@@ -27,13 +29,8 @@ abstract class HostTerminalTab(
|
|||||||
protected val terminalTabbedManager
|
protected val terminalTabbedManager
|
||||||
get() = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent()))
|
get() = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent()))
|
||||||
.getData(DataProviders.TerminalTabbedManager)
|
.getData(DataProviders.TerminalTabbedManager)
|
||||||
protected val coroutineScope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.Swing) }
|
protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Swing)
|
||||||
protected val terminalModel get() = terminal.getTerminalModel()
|
protected val terminalModel get() = terminal.getTerminalModel()
|
||||||
protected var unread = false
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
firePropertyChange(PropertyChangeEvent(this, "icon", null, null))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* visualTerminal */
|
/* visualTerminal */
|
||||||
@@ -45,15 +42,6 @@ abstract class HostTerminalTab(
|
|||||||
terminal.getTerminalModel().setData(Host, host)
|
terminal.getTerminalModel().setData(Host, host)
|
||||||
terminal.getTerminalModel().addDataListener(object : DataListener {
|
terminal.getTerminalModel().addDataListener(object : DataListener {
|
||||||
override fun onChanged(key: DataKey<*>, data: Any) {
|
override fun onChanged(key: DataKey<*>, data: Any) {
|
||||||
if (key == VisualTerminal.Written) {
|
|
||||||
if (hasFocus || unread) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 如果当前选中的不是这个 Tab,那么设置成未读
|
|
||||||
if (terminalTabbedManager?.getSelectedTerminalTab() != this@HostTerminalTab) {
|
|
||||||
unread = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -75,8 +63,6 @@ abstract class HostTerminalTab(
|
|||||||
|
|
||||||
override fun onGrabFocus() {
|
override fun onGrabFocus() {
|
||||||
super.onGrabFocus()
|
super.onGrabFocus()
|
||||||
if (!unread) return
|
|
||||||
unread = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
|||||||
@@ -533,7 +533,7 @@ class AuraLaf : FlatPropertiesLaf("Aura", Properties().apply {
|
|||||||
TerminalColor.Bright.WHITE -> 0xffffff
|
TerminalColor.Bright.WHITE -> 0xffffff
|
||||||
|
|
||||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||||
TerminalColor.Cursor.BACKGROUND -> 0xedecee
|
TerminalColor.Cursor.BACKGROUND -> 0xacacac
|
||||||
|
|
||||||
else -> Int.MAX_VALUE
|
else -> Int.MAX_VALUE
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,19 +2,21 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.actions.DataProviders
|
import app.termora.actions.DataProviders
|
||||||
|
import app.termora.actions.SwitchTabAction
|
||||||
|
import app.termora.database.DatabaseManager
|
||||||
|
import app.termora.keymap.KeyShortcut
|
||||||
|
import app.termora.keymap.KeymapManager
|
||||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||||
|
import com.formdev.flatlaf.ui.FlatTabbedPaneUI
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
import java.awt.event.*
|
import java.awt.event.*
|
||||||
import java.awt.image.BufferedImage
|
import java.awt.image.BufferedImage
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.swing.ImageIcon
|
import javax.swing.*
|
||||||
import javax.swing.JDialog
|
|
||||||
import javax.swing.JLabel
|
|
||||||
import javax.swing.SwingUtilities
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
class MyTabbedPane : FlatTabbedPane() {
|
internal class MyTabbedPane : FlatTabbedPane(), Disposable {
|
||||||
|
|
||||||
private val dragMouseAdaptor = DragMouseAdaptor()
|
private val dragMouseAdaptor = DragMouseAdaptor()
|
||||||
private val terminalTabbedManager
|
private val terminalTabbedManager
|
||||||
@@ -23,6 +25,30 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
private val owner
|
private val owner
|
||||||
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
get() = AnActionEvent(this, StringUtils.EMPTY, EventObject(this))
|
||||||
.getData(DataProviders.TermoraFrame) as TermoraFrame
|
.getData(DataProviders.TermoraFrame) as TermoraFrame
|
||||||
|
private val keymap get() = KeymapManager.getInstance().getActiveKeymap()
|
||||||
|
private val tabOrder get() = DatabaseManager.getInstance().appearance.tabOrder
|
||||||
|
private val isScreen get() = TermoraLayout.Layout == TermoraLayout.Screen
|
||||||
|
private var isSwitchTabMode = false
|
||||||
|
set(value) {
|
||||||
|
if (tabOrder == TabOrder.Always.name) {
|
||||||
|
if (field.not()) {
|
||||||
|
field = true
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if (tabOrder == TabOrder.Hide.name) {
|
||||||
|
if (field) {
|
||||||
|
field = false
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if (tabOrder == TabOrder.AsNeed.name) {
|
||||||
|
if (field != value) {
|
||||||
|
field = value
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
isFocusable = false
|
isFocusable = false
|
||||||
@@ -38,6 +64,16 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
private fun initEvents() {
|
private fun initEvents() {
|
||||||
addMouseListener(dragMouseAdaptor)
|
addMouseListener(dragMouseAdaptor)
|
||||||
addMouseMotionListener(dragMouseAdaptor)
|
addMouseMotionListener(dragMouseAdaptor)
|
||||||
|
|
||||||
|
val awtEventListener = MyAWTEventListener()
|
||||||
|
toolkit.addAWTEventListener(awtEventListener, AWTEvent.KEY_EVENT_MASK or AWTEvent.WINDOW_EVENT_MASK)
|
||||||
|
|
||||||
|
Disposer.register(this, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
toolkit.removeAWTEventListener(awtEventListener)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun processMouseEvent(e: MouseEvent) {
|
override fun processMouseEvent(e: MouseEvent) {
|
||||||
@@ -70,6 +106,32 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
firePropertyChange("selectedIndex", oldIndex, index)
|
firePropertyChange("selectedIndex", oldIndex, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateUI() {
|
||||||
|
super.updateUI()
|
||||||
|
setUI(MyMyTabbedPaneUI())
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class MyAWTEventListener : AWTEventListener {
|
||||||
|
override fun eventDispatched(event: AWTEvent) {
|
||||||
|
if (event is KeyEvent) {
|
||||||
|
if (isSwitchTabMode) isSwitchTabMode = false
|
||||||
|
val shortcuts = keymap.getShortcut(SwitchTabAction.SWITCH_TAB)
|
||||||
|
if (shortcuts.isEmpty()) return
|
||||||
|
val shortcut = shortcuts.first() as KeyShortcut
|
||||||
|
val modifiers = KeyStroke.getKeyStroke(event.keyCode, event.modifiersEx).modifiers
|
||||||
|
if (shortcut.keyStroke.modifiers != modifiers) return
|
||||||
|
if (SwingUtilities.getWindowAncestor(event.component) != owner) return
|
||||||
|
if (isSwitchTabMode.not()) isSwitchTabMode = true
|
||||||
|
} else if (event is WindowEvent) {
|
||||||
|
if (event.id == WindowEvent.WINDOW_LOST_FOCUS || event.id == WindowEvent.WINDOW_DEACTIVATED) {
|
||||||
|
if (isSwitchTabMode) isSwitchTabMode = false
|
||||||
|
} else if (event.id == WindowEvent.WINDOW_GAINED_FOCUS || event.id == WindowEvent.WINDOW_ACTIVATED) {
|
||||||
|
// 触发一次刷新
|
||||||
|
isSwitchTabMode = isSwitchTabMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private inner class DragMouseAdaptor : MouseAdapter(), KeyEventDispatcher {
|
private inner class DragMouseAdaptor : MouseAdapter(), KeyEventDispatcher {
|
||||||
private var mousePressedPoint = Point()
|
private var mousePressedPoint = Point()
|
||||||
@@ -267,5 +329,81 @@ class MyTabbedPane : FlatTabbedPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class MyMyTabbedPaneUI : FlatTabbedPaneUI() {
|
||||||
|
override fun paintIcon(
|
||||||
|
g: Graphics,
|
||||||
|
tabPlacement: Int,
|
||||||
|
tabIndex: Int,
|
||||||
|
icon: Icon,
|
||||||
|
iconRect: Rectangle?,
|
||||||
|
isSelected: Boolean
|
||||||
|
) {
|
||||||
|
super.paintIcon(g, tabPlacement, tabIndex, MyIcon(icon, tabIndex, isSelected), iconRect, isSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun createMoreTabsButton(): JButton {
|
||||||
|
return MyMoreTabsButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class MyMoreTabsButton : FlatMoreTabsButton() {
|
||||||
|
override fun createTabMenuItem(tabIndex: Int): JMenuItem? {
|
||||||
|
val item = super.createTabMenuItem(tabIndex)
|
||||||
|
if (tabIndex == 0 && isScreen) {
|
||||||
|
item.text = Application.getName()
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getIconAt(index: Int): Icon? {
|
||||||
|
if (isSwitchTabMode) {
|
||||||
|
return MyIcon(super.getIconAt(index), index, selectedIndex == index)
|
||||||
|
}
|
||||||
|
return super.getIconAt(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class MyIcon(private val icon: Icon, private val tabIndex: Int, private val isSelected: Boolean) :
|
||||||
|
Icon {
|
||||||
|
override fun paintIcon(c: Component, g: Graphics, x: Int, y: Int) {
|
||||||
|
if (isScreen && tabIndex == 0) {
|
||||||
|
icon.paintIcon(c, g, x, y)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSwitchTabMode.not()) {
|
||||||
|
icon.paintIcon(c, g, x, y)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g !is Graphics2D) return
|
||||||
|
|
||||||
|
g.save()
|
||||||
|
setupAntialiasing(g)
|
||||||
|
|
||||||
|
val fm = g.getFontMetrics(g.font)
|
||||||
|
val text = "${tabIndex + 1}"
|
||||||
|
val textWidth = fm.stringWidth(text)
|
||||||
|
val textHeight = fm.ascent
|
||||||
|
|
||||||
|
val centerX = x + (icon.iconWidth - textWidth) / 2
|
||||||
|
val centerY = y + (icon.iconHeight + textHeight) / 2 - 1
|
||||||
|
|
||||||
|
g.color = c.getForeground()
|
||||||
|
g.drawString(text, centerX, centerY)
|
||||||
|
|
||||||
|
g.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIconWidth(): Int {
|
||||||
|
return icon.iconWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIconHeight(): Int {
|
||||||
|
return icon.iconHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@ package app.termora
|
|||||||
|
|
||||||
import app.termora.actions.StateAction
|
import app.termora.actions.StateAction
|
||||||
import app.termora.findeverywhere.FindEverywhereAction
|
import app.termora.findeverywhere.FindEverywhereAction
|
||||||
import app.termora.plugin.internal.update.AppUpdateAction
|
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
@@ -82,10 +81,11 @@ internal class MyTermoraToolbar(private val windowScope: WindowScope, private va
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
add(Box.createHorizontalGlue())
|
if (SystemInfo.isLinux || SystemInfo.isWindows) {
|
||||||
|
add(Box.createHorizontalStrut(24))
|
||||||
|
}
|
||||||
|
|
||||||
// update
|
add(Box.createHorizontalGlue())
|
||||||
add(redirectUpdateAction(disposable))
|
|
||||||
|
|
||||||
for (action in model.getActions()) {
|
for (action in model.getActions()) {
|
||||||
if (action.visible.not()) continue
|
if (action.visible.not()) continue
|
||||||
@@ -122,34 +122,6 @@ internal class MyTermoraToolbar(private val windowScope: WindowScope, private va
|
|||||||
toolbar.add(spacing)
|
toolbar.add(spacing)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun redirectUpdateAction(disposable: Disposable): AbstractButton {
|
|
||||||
val action = AppUpdateAction.getInstance()
|
|
||||||
val button = JButton(action.smallIcon)
|
|
||||||
button.toolTipText = (action.getValue(Action.SHORT_DESCRIPTION) as? String)
|
|
||||||
?: action.getValue(Action.NAME) as? String
|
|
||||||
button.isVisible = action.isEnabled
|
|
||||||
button.addActionListener(object : AbstractAction() {
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
|
||||||
action.actionPerformed(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
val listener = object : PropertyChangeListener, Disposable {
|
|
||||||
override fun propertyChange(evt: PropertyChangeEvent) {
|
|
||||||
button.isVisible = action.isEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispose() {
|
|
||||||
action.removePropertyChangeListener(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
action.addPropertyChangeListener(listener)
|
|
||||||
Disposer.register(disposable, listener)
|
|
||||||
|
|
||||||
return button
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun redirectAction(action: Action, disposable: Disposable): AbstractButton {
|
private fun redirectAction(action: Action, disposable: Disposable): AbstractButton {
|
||||||
val button = if (action is StateAction) JToggleButton() else JButton()
|
val button = if (action is StateAction) JToggleButton() else JButton()
|
||||||
button.toolTipText = (action.getValue(Action.SHORT_DESCRIPTION) as? String)
|
button.toolTipText = (action.getValue(Action.SHORT_DESCRIPTION) as? String)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -173,10 +173,21 @@ abstract class PtyHostTerminalTab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun reconnect() {
|
override fun reconnect() {
|
||||||
stop()
|
val manager = terminalTabbedManager ?: return
|
||||||
start()
|
val index = manager.indexOfTerminalTab(this)
|
||||||
|
if (index < 0) return
|
||||||
|
|
||||||
|
val tab = createReconnectTerminalTab()
|
||||||
|
manager.addTerminalTab(index, tab, true)
|
||||||
|
manager.closeTerminalTab(this, disposable = true, reconnect = true)
|
||||||
|
|
||||||
|
if (tab is HostTerminalTab) {
|
||||||
|
tab.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract fun createReconnectTerminalTab(): TerminalTab
|
||||||
|
|
||||||
override fun getJComponent(): JComponent {
|
override fun getJComponent(): JComponent {
|
||||||
return terminalPanel
|
return terminalPanel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import com.jgoodies.forms.layout.FormLayout
|
|||||||
import com.jthemedetecor.OsThemeDetector
|
import com.jthemedetecor.OsThemeDetector
|
||||||
import com.sun.jna.LastErrorException
|
import com.sun.jna.LastErrorException
|
||||||
import com.sun.jna.Native
|
import com.sun.jna.Native
|
||||||
|
import com.sun.jna.platform.WindowUtils
|
||||||
import com.sun.jna.platform.win32.Shell32
|
import com.sun.jna.platform.win32.Shell32
|
||||||
import com.sun.jna.platform.win32.ShlObj
|
import com.sun.jna.platform.win32.ShlObj
|
||||||
import com.sun.jna.platform.win32.WinDef
|
import com.sun.jna.platform.win32.WinDef
|
||||||
@@ -114,6 +115,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
val languageComboBox = FlatComboBox<String>()
|
val languageComboBox = FlatComboBox<String>()
|
||||||
val backgroundComBoBox = YesOrNoComboBox()
|
val backgroundComBoBox = YesOrNoComboBox()
|
||||||
val confirmTabCloseComBoBox = YesOrNoComboBox()
|
val confirmTabCloseComBoBox = YesOrNoComboBox()
|
||||||
|
val tabOrderComboBox = FlatComboBox<TabOrder>()
|
||||||
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
|
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
|
||||||
val preferredThemeBtn = JButton(Icons.settings)
|
val preferredThemeBtn = JButton(Icons.settings)
|
||||||
val opacitySpinner = NumberSpinner(100, 0, 100)
|
val opacitySpinner = NumberSpinner(100, 0, 100)
|
||||||
@@ -128,6 +130,12 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
|
|
||||||
|
tabOrderComboBox.addItem(TabOrder.Hide)
|
||||||
|
tabOrderComboBox.addItem(TabOrder.AsNeed)
|
||||||
|
tabOrderComboBox.addItem(TabOrder.Always)
|
||||||
|
tabOrderComboBox.selectedItem = runCatching { TabOrder.valueOf(appearance.tabOrder) }
|
||||||
|
.getOrNull() ?: TabOrder.Hide
|
||||||
|
|
||||||
layoutComboBox.addItem(TermoraLayout.Screen)
|
layoutComboBox.addItem(TermoraLayout.Screen)
|
||||||
layoutComboBox.addItem(TermoraLayout.Fence)
|
layoutComboBox.addItem(TermoraLayout.Fence)
|
||||||
layoutComboBox.renderer = object : DefaultListCellRenderer() {
|
layoutComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
@@ -162,7 +170,8 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
backgroundComBoBox.isEnabled = SystemInfo.isWindows || SystemInfo.isMacOS
|
backgroundComBoBox.isEnabled = SystemInfo.isWindows || SystemInfo.isMacOS
|
||||||
|
|
||||||
opacitySpinner.isEnabled = SystemInfo.isMacOS || SystemInfo.isWindows
|
opacitySpinner.isEnabled = (SystemInfo.isMacOS || SystemInfo.isWindows)
|
||||||
|
|| (SystemInfo.isLinux && WindowUtils.isWindowAlphaSupported())
|
||||||
opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) {
|
opacitySpinner.model = object : SpinnerNumberModel(appearance.opacity, 0.1, 1.0, 0.1) {
|
||||||
override fun getNextValue(): Any {
|
override fun getNextValue(): Any {
|
||||||
return super.getNextValue() ?: maximum
|
return super.getNextValue() ?: maximum
|
||||||
@@ -226,6 +235,14 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
tabOrderComboBox.addItemListener(object : ItemListener {
|
||||||
|
override fun itemStateChanged(e: ItemEvent) {
|
||||||
|
if (e.stateChange == ItemEvent.SELECTED) {
|
||||||
|
appearance.tabOrder = tabOrderComboBox.selectedItem?.toString() ?: return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
opacitySpinner.addChangeListener {
|
opacitySpinner.addChangeListener {
|
||||||
val opacity = opacitySpinner.value
|
val opacity = opacitySpinner.value
|
||||||
if (opacity is Double) {
|
if (opacity is Double) {
|
||||||
@@ -349,7 +366,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private fun getFormPanel(): JPanel {
|
private fun getFormPanel(): JPanel {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default, default:grow",
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default, default:grow",
|
||||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
)
|
)
|
||||||
val box = FlatToolBar()
|
val box = FlatToolBar()
|
||||||
box.add(followSystemCheckBox)
|
box.add(followSystemCheckBox)
|
||||||
@@ -380,6 +397,9 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
builder.add("${I18n.getString("termora.settings.appearance.background-running")}:").xy(1, rows)
|
builder.add("${I18n.getString("termora.settings.appearance.background-running")}:").xy(1, rows)
|
||||||
.add(backgroundComBoBox).xy(3, rows).apply { rows += step }
|
.add(backgroundComBoBox).xy(3, rows).apply { rows += step }
|
||||||
|
|
||||||
|
builder.add("${I18n.getString("termora.settings.appearance.tab-order")}:").xy(1, rows)
|
||||||
|
.add(tabOrderComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
|
||||||
val confirmTabCloseBox = Box.createHorizontalBox()
|
val confirmTabCloseBox = Box.createHorizontalBox()
|
||||||
confirmTabCloseBox.add(JLabel("${I18n.getString("termora.settings.appearance.confirm-tab-close")}:"))
|
confirmTabCloseBox.add(JLabel("${I18n.getString("termora.settings.appearance.confirm-tab-close")}:"))
|
||||||
confirmTabCloseBox.add(Box.createHorizontalStrut(8))
|
confirmTabCloseBox.add(Box.createHorizontalStrut(8))
|
||||||
@@ -404,6 +424,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private val fontSizeTextField = IntSpinner(0, 9, 99)
|
private val fontSizeTextField = IntSpinner(0, 9, 99)
|
||||||
private val terminalSetting get() = DatabaseManager.getInstance().terminal
|
private val terminalSetting get() = DatabaseManager.getInstance().terminal
|
||||||
private val selectCopyComboBox = YesOrNoComboBox()
|
private val selectCopyComboBox = YesOrNoComboBox()
|
||||||
|
private val rightClickComboBox = OutlineComboBox<String>()
|
||||||
private val autoCloseTabComboBox = YesOrNoComboBox()
|
private val autoCloseTabComboBox = YesOrNoComboBox()
|
||||||
private val floatingToolbarComboBox = YesOrNoComboBox()
|
private val floatingToolbarComboBox = YesOrNoComboBox()
|
||||||
private val hyperlinkComboBox = YesOrNoComboBox()
|
private val hyperlinkComboBox = YesOrNoComboBox()
|
||||||
@@ -417,6 +438,12 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rightClickComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
terminalSetting.rightClick = rightClickComboBox.selectedItem as String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fallbackFontComboBox.addItemListener {
|
fallbackFontComboBox.addItemListener {
|
||||||
if (it.stateChange == ItemEvent.SELECTED) {
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
terminalSetting.fallbackFont = fallbackFontComboBox.selectedItem as String
|
terminalSetting.fallbackFont = fallbackFontComboBox.selectedItem as String
|
||||||
@@ -518,6 +545,10 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
fontSizeTextField.value = terminalSetting.fontSize
|
fontSizeTextField.value = terminalSetting.fontSize
|
||||||
maxRowsTextField.value = terminalSetting.maxRows
|
maxRowsTextField.value = terminalSetting.maxRows
|
||||||
|
|
||||||
|
rightClickComboBox.addItem("Copy")
|
||||||
|
rightClickComboBox.addItem("CopyAndPaste")
|
||||||
|
|
||||||
|
rightClickComboBox.selectedItem = terminalSetting.rightClick
|
||||||
|
|
||||||
cursorStyleComboBox.renderer = object : DefaultListCellRenderer() {
|
cursorStyleComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
override fun getListCellRendererComponent(
|
override fun getListCellRendererComponent(
|
||||||
@@ -532,6 +563,24 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rightClickComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = value?.toString()
|
||||||
|
if (value == "Copy") {
|
||||||
|
text = I18n.getString("termora.settings.terminal.right-click.copy")
|
||||||
|
} else if (value == "CopyAndPaste") {
|
||||||
|
text = I18n.getString("termora.settings.terminal.right-click.copy-and-paste")
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cursorStyleComboBox.addItem(CursorStyle.Block)
|
cursorStyleComboBox.addItem(CursorStyle.Block)
|
||||||
cursorStyleComboBox.addItem(CursorStyle.Bar)
|
cursorStyleComboBox.addItem(CursorStyle.Bar)
|
||||||
cursorStyleComboBox.addItem(CursorStyle.Underline)
|
cursorStyleComboBox.addItem(CursorStyle.Underline)
|
||||||
@@ -595,7 +644,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private fun getCenterComponent(): JComponent {
|
private fun getCenterComponent(): JComponent {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, left:pref, $FORM_MARGIN, pref, default:grow",
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, left:pref, $FORM_MARGIN, pref, default:grow",
|
||||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
)
|
)
|
||||||
|
|
||||||
val beepBtn = JButton(Icons.run)
|
val beepBtn = JButton(Icons.run)
|
||||||
@@ -624,6 +673,8 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
.add(hyperlinkComboBox).xy(3, rows).apply { rows += step }
|
.add(hyperlinkComboBox).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.select-copy")}:").xy(1, rows)
|
||||||
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.settings.terminal.right-click")}:").xy(1, rows)
|
||||||
|
.add(rightClickComboBox).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
|
||||||
.add(cursorStyleComboBox).xy(3, rows).apply { rows += step }
|
.add(cursorStyleComboBox).xy(3, rows).apply { rows += step }
|
||||||
.add("${I18n.getString("termora.settings.terminal.cursor-blink")}:").xy(1, rows)
|
.add("${I18n.getString("termora.settings.terminal.cursor-blink")}:").xy(1, rows)
|
||||||
@@ -650,6 +701,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
private val browseEditCommandBtn = JButton(Icons.folder)
|
private val browseEditCommandBtn = JButton(Icons.folder)
|
||||||
private val pinTabComboBox = YesOrNoComboBox()
|
private val pinTabComboBox = YesOrNoComboBox()
|
||||||
private val preserveModificationTimeComboBox = YesOrNoComboBox()
|
private val preserveModificationTimeComboBox = YesOrNoComboBox()
|
||||||
|
private val doubleClickComboBox = OutlineComboBox<String>()
|
||||||
private val sftp get() = database.sftp
|
private val sftp get() = database.sftp
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -699,6 +751,13 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
doubleClickComboBox.addItemListener(object : ItemListener {
|
||||||
|
override fun itemStateChanged(e: ItemEvent) {
|
||||||
|
if (e.stateChange != ItemEvent.SELECTED) return
|
||||||
|
sftp.dbClickBehavior = doubleClickComboBox.selectedItem as String
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
preserveModificationTimeComboBox.addItemListener {
|
preserveModificationTimeComboBox.addItemListener {
|
||||||
if (it.stateChange == ItemEvent.SELECTED) {
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
sftp.preserveModificationTime = preserveModificationTimeComboBox.selectedItem as Boolean
|
sftp.preserveModificationTime = preserveModificationTimeComboBox.selectedItem as Boolean
|
||||||
@@ -780,6 +839,26 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
sftpCommandField.text = sftp.sftpCommand
|
sftpCommandField.text = sftp.sftpCommand
|
||||||
pinTabComboBox.selectedItem = sftp.pinTab
|
pinTabComboBox.selectedItem = sftp.pinTab
|
||||||
preserveModificationTimeComboBox.selectedItem = sftp.preserveModificationTime
|
preserveModificationTimeComboBox.selectedItem = sftp.preserveModificationTime
|
||||||
|
|
||||||
|
doubleClickComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component? {
|
||||||
|
var text = value?.toString()
|
||||||
|
if (value == "Edit") text = I18n.getString("termora.keymgr.edit")
|
||||||
|
if (value == "Transfer") text = getTitle()
|
||||||
|
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doubleClickComboBox.addItem("Transfer")
|
||||||
|
doubleClickComboBox.addItem("Edit")
|
||||||
|
|
||||||
|
doubleClickComboBox.selectedItem = sftp.dbClickBehavior
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(isSelected: Boolean): Icon {
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
@@ -813,6 +892,8 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
builder.add(editCommandField).xy(3, rows).apply { rows += 2 }
|
builder.add(editCommandField).xy(3, rows).apply { rows += 2 }
|
||||||
builder.add("${I18n.getString("termora.tabbed.contextmenu.sftp-command")}:").xy(1, rows)
|
builder.add("${I18n.getString("termora.tabbed.contextmenu.sftp-command")}:").xy(1, rows)
|
||||||
builder.add(sftpCommandField).xy(3, rows).apply { rows += 2 }
|
builder.add(sftpCommandField).xy(3, rows).apply { rows += 2 }
|
||||||
|
builder.add("${I18n.getString("termora.settings.sftp.db-click-behavior")}:").xy(1, rows)
|
||||||
|
builder.add(doubleClickComboBox).xy(3, rows).apply { rows += 2 }
|
||||||
builder.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
builder.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||||
builder.add(defaultDirectoryField).xy(3, rows).apply { rows += 2 }
|
builder.add(defaultDirectoryField).xy(3, rows).apply { rows += 2 }
|
||||||
builder.add(box).xyw(1, rows, 3).apply { rows += 2 }
|
builder.add(box).xyw(1, rows, 3).apply { rows += 2 }
|
||||||
|
|||||||
7
src/main/kotlin/app/termora/TabOrder.kt
Normal file
7
src/main/kotlin/app/termora/TabOrder.kt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package app.termora
|
||||||
|
|
||||||
|
internal enum class TabOrder {
|
||||||
|
Hide,
|
||||||
|
AsNeed,
|
||||||
|
Always,
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,13 +340,7 @@ class TerminalTabbed(
|
|||||||
val c = tab.getJComponent()
|
val c = tab.getJComponent()
|
||||||
val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
|
val title = (c.getClientProperty(titleProperty) ?: tab.getTitle()).toString()
|
||||||
|
|
||||||
tabbedPane.insertTab(
|
tabbedPane.insertTab(title, tab.getIcon(), c, StringUtils.EMPTY, index)
|
||||||
title,
|
|
||||||
tab.getIcon(),
|
|
||||||
c,
|
|
||||||
StringUtils.EMPTY,
|
|
||||||
index
|
|
||||||
)
|
|
||||||
|
|
||||||
// 设置标题
|
// 设置标题
|
||||||
c.putClientProperty(titleProperty, title)
|
c.putClientProperty(titleProperty, title)
|
||||||
@@ -367,6 +364,10 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun indexOfTerminalTab(tab: TerminalTab): Int {
|
||||||
|
return tabbedPane.indexOfComponent(tab.getJComponent())
|
||||||
|
}
|
||||||
|
|
||||||
private inner class SwitchFindEverywhereResult(
|
private inner class SwitchFindEverywhereResult(
|
||||||
private val title: String,
|
private val title: String,
|
||||||
private val icon: Icon?,
|
private val icon: Icon?,
|
||||||
@@ -453,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,6 +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
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
@@ -72,10 +72,12 @@ class TermoraFencePanel(
|
|||||||
leftTreePanel.addComponentListener(object : ComponentAdapter() {
|
leftTreePanel.addComponentListener(object : ComponentAdapter() {
|
||||||
override fun componentHidden(e: ComponentEvent) {
|
override fun componentHidden(e: ComponentEvent) {
|
||||||
toolbar.isVisible = true
|
toolbar.isVisible = true
|
||||||
|
enableManager.setFlag("Termora.Fence.colspan", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun componentShown(e: ComponentEvent) {
|
override fun componentShown(e: ComponentEvent) {
|
||||||
toolbar.isVisible = false
|
toolbar.isVisible = false
|
||||||
|
enableManager.setFlag("Termora.Fence.colspan", false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -86,6 +88,50 @@ class TermoraFencePanel(
|
|||||||
toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK
|
toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK
|
||||||
), "toggle"
|
), "toggle"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
splitPane.addPropertyChangeListener("dividerLocation") {
|
||||||
|
if (leftTreePanel.isVisible)
|
||||||
|
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableManager.getFlag("Termora.Fence.colspan", false)) {
|
||||||
|
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 {
|
||||||
@@ -144,19 +190,19 @@ class TermoraFencePanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
if (leftTreePanel.isVisible) dividerLocation = splitPane.dividerLocation
|
toggle()
|
||||||
leftTreePanel.isVisible = leftTreePanel.isVisible.not()
|
|
||||||
if (leftTreePanel.isVisible) splitPane.dividerLocation = dividerLocation
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun toggle() {
|
||||||
override fun dispose() {
|
if (leftTreePanel.isVisible) dividerLocation = splitPane.dividerLocation
|
||||||
if (leftTreePanel.isVisible)
|
leftTreePanel.isVisible = leftTreePanel.isVisible.not()
|
||||||
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
|
if (leftTreePanel.isVisible) splitPane.dividerLocation = dividerLocation
|
||||||
|
mySplitPane.doLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getHostTree(): NewHostTree {
|
fun getHostTree(): NewHostTree {
|
||||||
return leftTreePanel.hostTree
|
return leftTreePanel.hostTree
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ class TermoraFrame : JFrame(), DataProvider {
|
|||||||
|
|
||||||
}).let { Disposer.register(windowScope, it) }
|
}).let { Disposer.register(windowScope, it) }
|
||||||
|
|
||||||
|
Disposer.register(windowScope, tabbedPane)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
@@ -210,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)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import app.termora.plugin.ExtensionManager
|
|||||||
import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary
|
import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import com.sun.jna.Pointer
|
import com.sun.jna.Pointer
|
||||||
|
import com.sun.jna.platform.WindowUtils
|
||||||
import com.sun.jna.platform.win32.User32
|
import com.sun.jna.platform.win32.User32
|
||||||
import com.sun.jna.platform.win32.WinDef
|
import com.sun.jna.platform.win32.WinDef
|
||||||
import com.sun.jna.platform.win32.WinUser.*
|
import com.sun.jna.platform.win32.WinUser.*
|
||||||
@@ -206,7 +207,7 @@ class TermoraFrameManager : Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setOpacity(opacity: Double) {
|
fun setOpacity(opacity: Double) {
|
||||||
if (opacity < 0 || opacity > 1 || SystemInfo.isLinux) return
|
if (opacity < 0 || opacity > 1) return
|
||||||
for (window in getWindows()) {
|
for (window in getWindows()) {
|
||||||
setOpacity(window, opacity)
|
setOpacity(window, opacity)
|
||||||
}
|
}
|
||||||
@@ -227,6 +228,8 @@ class TermoraFrameManager : Disposable {
|
|||||||
User32.INSTANCE.SetWindowLong(hwnd, GWL_EXSTYLE, exStyle or WS_EX_LAYERED)
|
User32.INSTANCE.SetWindowLong(hwnd, GWL_EXSTYLE, exStyle or WS_EX_LAYERED)
|
||||||
}
|
}
|
||||||
User32.INSTANCE.SetLayeredWindowAttributes(hwnd, 0, alpha, LWA_ALPHA)
|
User32.INSTANCE.SetLayeredWindowAttributes(hwnd, 0, alpha, LWA_ALPHA)
|
||||||
|
} else if (SystemInfo.isLinux && WindowUtils.isWindowAlphaSupported()) {
|
||||||
|
WindowUtils.setWindowAlpha(window, opacity.toFloat())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
@@ -224,7 +226,7 @@ class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProv
|
|||||||
|
|
||||||
|
|
||||||
override fun getTitle(): String {
|
override fun getTitle(): String {
|
||||||
return I18n.getString("termora.title")
|
return StringUtils.EMPTY
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(): Icon {
|
override fun getIcon(): Icon {
|
||||||
@@ -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,6 @@ 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
|
|
||||||
val hash: String
|
val hash: String
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -126,7 +125,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
|||||||
while (true) {
|
while (true) {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.get()
|
.get()
|
||||||
.url("${accountManager.getServer()}/v1/data/changes?since=${since}&after=${after}&limit=${limit}")
|
.url("${accountManager.getServer()}/v1/data/changes?since=${nextSince}&after=${after}&limit=${limit}")
|
||||||
.build()
|
.build()
|
||||||
val text = AccountHttp.execute(request = request)
|
val text = AccountHttp.execute(request = request)
|
||||||
val response = ohMyJson.decodeFromString<DataChangesResponse>(text)
|
val response = ohMyJson.decodeFromString<DataChangesResponse>(text)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ object DataProviders {
|
|||||||
val Terminal = DataKey(app.termora.terminal.Terminal::class)
|
val Terminal = DataKey(app.termora.terminal.Terminal::class)
|
||||||
val TerminalWriter get() = DataKey.TerminalWriter
|
val TerminalWriter get() = DataKey.TerminalWriter
|
||||||
|
|
||||||
val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
|
internal val TabbedPane = DataKey(app.termora.MyTabbedPane::class)
|
||||||
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
|
val TerminalTabbed = DataKey(app.termora.TerminalTabbed::class)
|
||||||
val TerminalTab = DataKey(app.termora.TerminalTab::class)
|
val TerminalTab = DataKey(app.termora.TerminalTab::class)
|
||||||
val TerminalTabbedManager = DataKey(app.termora.TerminalTabbedManager::class)
|
val TerminalTabbedManager = DataKey(app.termora.TerminalTabbedManager::class)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.ApplicationScope
|
import app.termora.*
|
||||||
import app.termora.I18n
|
|
||||||
import app.termora.Icons
|
|
||||||
import app.termora.SettingsDialog
|
|
||||||
import com.formdev.flatlaf.extras.FlatDesktop
|
import com.formdev.flatlaf.extras.FlatDesktop
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.awt.KeyboardFocusManager
|
import java.awt.KeyboardFocusManager
|
||||||
@@ -32,13 +29,13 @@ class SettingsAction private constructor() : AnAction(
|
|||||||
private val action get() = this
|
private val action get() = this
|
||||||
|
|
||||||
init {
|
init {
|
||||||
FlatDesktop.setPreferencesHandler {
|
FlatDesktop.setPreferencesHandler(object : Runnable {
|
||||||
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
override fun run() {
|
||||||
// Doorman 的情况下不允许打开
|
val focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow ?: return
|
||||||
if (owner != null && ApplicationScope.windowScopes().isNotEmpty()) {
|
if (focusedWindow !is TermoraFrame) return
|
||||||
actionPerformed(ActionEvent(owner, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
actionPerformed(ActionEvent(focusedWindow, ActionEvent.ACTION_PERFORMED, StringUtils.EMPTY))
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -666,6 +666,11 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
*/
|
*/
|
||||||
var selectCopy by BooleanPropertyDelegate(false)
|
var selectCopy by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 右键点击:Copy、CopyAndPaste
|
||||||
|
*/
|
||||||
|
var rightClick by StringPropertyDelegate("Copy")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 光标样式
|
* 光标样式
|
||||||
*/
|
*/
|
||||||
@@ -716,6 +721,11 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
*/
|
*/
|
||||||
var layout by StringPropertyDelegate(TermoraLayout.Screen.name)
|
var layout by StringPropertyDelegate(TermoraLayout.Screen.name)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签序号
|
||||||
|
*/
|
||||||
|
var tabOrder by StringPropertyDelegate(TabOrder.Hide.name)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 跟随系统
|
* 跟随系统
|
||||||
*/
|
*/
|
||||||
@@ -763,6 +773,12 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
*/
|
*/
|
||||||
var editCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
var editCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 双击行为
|
||||||
|
*
|
||||||
|
* Transfer、Edit
|
||||||
|
*/
|
||||||
|
var dbClickBehavior by StringPropertyDelegate("Transfer")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* sftp command
|
* sftp command
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package app.termora.highlight
|
package app.termora.highlight
|
||||||
|
|
||||||
import app.termora.DialogWrapper
|
import app.termora.DialogWrapper
|
||||||
|
import app.termora.Disposable
|
||||||
|
import app.termora.Disposer
|
||||||
import app.termora.TerminalFactory
|
import app.termora.TerminalFactory
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
@@ -15,8 +17,9 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
|
|||||||
var colorIndex = -1
|
var colorIndex = -1
|
||||||
var defaultColor: Color = Color.white
|
var defaultColor: Color = Color.white
|
||||||
|
|
||||||
|
var ok = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
|
||||||
isModal = true
|
isModal = true
|
||||||
super.setTitle(title)
|
super.setTitle(title)
|
||||||
controlsVisible = false
|
controlsVisible = false
|
||||||
@@ -30,11 +33,12 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
|
|||||||
|
|
||||||
override fun createCenterPanel(): JComponent {
|
override fun createCenterPanel(): JComponent {
|
||||||
val panel = JPanel(GridLayout(2, 8, 4, 4))
|
val panel = JPanel(GridLayout(2, 8, 4, 4))
|
||||||
val colorPalette = TerminalFactory.getInstance()
|
val terminal = TerminalFactory.getInstance().createTerminal()
|
||||||
.createTerminal().getTerminalModel().getColorPalette()
|
val colorPalette = terminal.getTerminalModel().getColorPalette()
|
||||||
for (i in 1..16) {
|
for (i in 1..16) {
|
||||||
val c = JPanel()
|
val c = JPanel()
|
||||||
c.preferredSize = Dimension(24, 24)
|
c.preferredSize = Dimension(24, 24)
|
||||||
|
c.minimumSize = c.preferredSize
|
||||||
c.background = Color(colorPalette.getXTerm256Color(i))
|
c.background = Color(colorPalette.getXTerm256Color(i))
|
||||||
c.addMouseListener(object : MouseAdapter() {
|
c.addMouseListener(object : MouseAdapter() {
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
@@ -67,6 +71,12 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
|
|||||||
cPanel.add(panel, BorderLayout.CENTER)
|
cPanel.add(panel, BorderLayout.CENTER)
|
||||||
cPanel.add(customBtn, BorderLayout.SOUTH)
|
cPanel.add(customBtn, BorderLayout.SOUTH)
|
||||||
cPanel.border = BorderFactory.createEmptyBorder(if (SystemInfo.isLinux) 6 else 0, 12, 12, 12)
|
cPanel.border = BorderFactory.createEmptyBorder(if (SystemInfo.isLinux) 6 else 0, 12, 12, 12)
|
||||||
|
|
||||||
|
Disposer.register(disposable, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
terminal.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
return cPanel
|
return cPanel
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,4 +84,9 @@ class ChooseColorTemplateDialog(owner: Window, title: String) : DialogWrapper(ow
|
|||||||
override fun createSouthPanel(): JComponent? {
|
override fun createSouthPanel(): JComponent? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun doOKAction() {
|
||||||
|
ok = true
|
||||||
|
super.doOKAction()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,8 @@ package app.termora.highlight
|
|||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
import javax.swing.JPanel
|
import javax.swing.JPanel
|
||||||
|
|
||||||
class ColorPanel : JPanel {
|
class ColorPanel : JPanel() {
|
||||||
var color: Color = Color.WHITE
|
var color: Color? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
background = value
|
background = value
|
||||||
val old = field
|
val old = field
|
||||||
@@ -13,7 +13,4 @@ class ColorPanel : JPanel {
|
|||||||
}
|
}
|
||||||
var colorIndex = -1
|
var colorIndex = -1
|
||||||
|
|
||||||
constructor(color: Color) : super() {
|
|
||||||
this.color = color
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -101,6 +101,16 @@ internal class KeywordHighlightPaintListener private constructor() : TerminalPai
|
|||||||
// -1 表示不使用高亮集
|
// -1 表示不使用高亮集
|
||||||
if (keywordHighlightSetId == "-1") return
|
if (keywordHighlightSetId == "-1") return
|
||||||
|
|
||||||
|
try {
|
||||||
|
doFind(offset, count, terminal, keywordHighlightSetId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doFind(offset: Int, count: Int, terminal: Terminal, keywordHighlightSetId: String) {
|
||||||
for (highlight in keywordHighlights) {
|
for (highlight in keywordHighlights) {
|
||||||
if (highlight.enabled.not()) continue
|
if (highlight.enabled.not()) continue
|
||||||
if (highlight.type != KeywordHighlightType.Highlight) continue
|
if (highlight.type != KeywordHighlightType.Highlight) continue
|
||||||
@@ -151,7 +161,6 @@ internal class KeywordHighlightPaintListener private constructor() : TerminalPai
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun after(
|
override fun after(
|
||||||
|
|||||||
@@ -285,29 +285,27 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
|
|||||||
dialog.keywordTextField.text = keywordHighlight.keyword
|
dialog.keywordTextField.text = keywordHighlight.keyword
|
||||||
dialog.descriptionTextField.text = keywordHighlight.description
|
dialog.descriptionTextField.text = keywordHighlight.description
|
||||||
|
|
||||||
if (keywordHighlight.textColor <= 16) {
|
if (keywordHighlight.textColor in 0..16) {
|
||||||
if (keywordHighlight.textColor == 0) {
|
if (keywordHighlight.textColor == 0) {
|
||||||
dialog.textColor.color = Color(colorPalette.getColor(TerminalColor.Basic.FOREGROUND))
|
dialog.textColor.background = Color(colorPalette.getColor(TerminalColor.Basic.FOREGROUND))
|
||||||
|
dialog.textColor.colorIndex = -1
|
||||||
} else {
|
} else {
|
||||||
dialog.textColor.color = Color(colorPalette.getXTerm256Color(keywordHighlight.textColor))
|
dialog.textColor.color = Color(colorPalette.getXTerm256Color(keywordHighlight.textColor))
|
||||||
|
dialog.textColor.colorIndex = keywordHighlight.textColor
|
||||||
}
|
}
|
||||||
dialog.textColor.colorIndex = keywordHighlight.textColor
|
|
||||||
} else {
|
} else {
|
||||||
dialog.textColor.color = Color(keywordHighlight.textColor)
|
dialog.textColor.color = Color(keywordHighlight.textColor)
|
||||||
dialog.textColor.colorIndex = -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keywordHighlight.backgroundColor <= 16) {
|
if (keywordHighlight.backgroundColor in 0..16) {
|
||||||
if (keywordHighlight.backgroundColor == 0) {
|
if (keywordHighlight.backgroundColor == 0) {
|
||||||
dialog.backgroundColor.color = Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND))
|
dialog.backgroundColor.background = Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND))
|
||||||
|
dialog.backgroundColor.colorIndex = -1
|
||||||
} else {
|
} else {
|
||||||
dialog.backgroundColor.color =
|
dialog.backgroundColor.color =
|
||||||
Color(colorPalette.getXTerm256Color(keywordHighlight.backgroundColor))
|
Color(colorPalette.getXTerm256Color(keywordHighlight.backgroundColor))
|
||||||
|
dialog.backgroundColor.colorIndex = keywordHighlight.backgroundColor
|
||||||
}
|
}
|
||||||
dialog.backgroundColor.colorIndex = keywordHighlight.backgroundColor
|
|
||||||
} else {
|
|
||||||
dialog.backgroundColor.color = Color(keywordHighlight.backgroundColor)
|
|
||||||
dialog.backgroundColor.colorIndex = -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.boldCheckBox.isSelected = keywordHighlight.bold
|
dialog.boldCheckBox.isSelected = keywordHighlight.bold
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import java.awt.event.MouseEvent
|
|||||||
import java.awt.event.WindowAdapter
|
import java.awt.event.WindowAdapter
|
||||||
import java.awt.event.WindowEvent
|
import java.awt.event.WindowEvent
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class NewKeywordHighlightDialog(
|
class NewKeywordHighlightDialog(
|
||||||
owner: Window,
|
owner: Window,
|
||||||
@@ -81,7 +82,7 @@ class NewKeywordHighlightDialog(
|
|||||||
FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON
|
FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON
|
||||||
)
|
)
|
||||||
|
|
||||||
matchCaseBtn.toolTipText = "Match case"
|
matchCaseBtn.toolTipText = I18n.getString("termora.match-case")
|
||||||
|
|
||||||
|
|
||||||
val box = FlatToolBar()
|
val box = FlatToolBar()
|
||||||
@@ -95,7 +96,7 @@ class NewKeywordHighlightDialog(
|
|||||||
|
|
||||||
init()
|
init()
|
||||||
pack()
|
pack()
|
||||||
size = Dimension(UIManager.getInt("Dialog.width") - 200, height)
|
size = Dimension(UIManager.getInt("Dialog.width") - 200, max(height, preferredSize.height))
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -121,13 +122,15 @@ class NewKeywordHighlightDialog(
|
|||||||
lineThroughCheckBox.addActionListener { repaintKeywordHighlightView() }
|
lineThroughCheckBox.addActionListener { repaintKeywordHighlightView() }
|
||||||
|
|
||||||
textColorRevert.addActionListener {
|
textColorRevert.addActionListener {
|
||||||
textColor.color = Color(colorPalette.getColor(TerminalColor.Basic.FOREGROUND))
|
textColor.color = null
|
||||||
textColor.colorIndex = 0
|
textColor.background = Color(colorPalette.getColor(TerminalColor.Basic.FOREGROUND))
|
||||||
|
textColor.colorIndex = -1
|
||||||
repaintKeywordHighlightView()
|
repaintKeywordHighlightView()
|
||||||
}
|
}
|
||||||
backgroundColorRevert.addActionListener {
|
backgroundColorRevert.addActionListener {
|
||||||
backgroundColor.color = Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND))
|
backgroundColor.color = null
|
||||||
backgroundColor.colorIndex = 0
|
backgroundColor.background = Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND))
|
||||||
|
backgroundColor.colorIndex = -1
|
||||||
repaintKeywordHighlightView()
|
repaintKeywordHighlightView()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,8 +148,22 @@ class NewKeywordHighlightDialog(
|
|||||||
keywordHighlightView.italic = italicCheckBox.isSelected
|
keywordHighlightView.italic = italicCheckBox.isSelected
|
||||||
keywordHighlightView.underline = underlineCheckBox.isSelected
|
keywordHighlightView.underline = underlineCheckBox.isSelected
|
||||||
keywordHighlightView.lineThrough = lineThroughCheckBox.isSelected
|
keywordHighlightView.lineThrough = lineThroughCheckBox.isSelected
|
||||||
keywordHighlightView.textColor = textColor.color
|
|
||||||
keywordHighlightView.backgroundColor = backgroundColor.color
|
if (textColor.color == null && textColor.colorIndex == -1) {
|
||||||
|
keywordHighlightView.textColor = Color(colorPalette.getColor(TerminalColor.Basic.FOREGROUND))
|
||||||
|
} else if (textColor.color != null) {
|
||||||
|
keywordHighlightView.textColor = textColor.color
|
||||||
|
} else {
|
||||||
|
keywordHighlightView.textColor = Color(colorPalette.getXTerm256Color(textColor.colorIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backgroundColor.color == null && backgroundColor.colorIndex == -1) {
|
||||||
|
keywordHighlightView.backgroundColor = Color(colorPalette.getColor(TerminalColor.Basic.BACKGROUND))
|
||||||
|
} else if (backgroundColor.color != null) {
|
||||||
|
keywordHighlightView.backgroundColor = backgroundColor.color
|
||||||
|
} else {
|
||||||
|
keywordHighlightView.backgroundColor = Color(colorPalette.getXTerm256Color(backgroundColor.colorIndex))
|
||||||
|
}
|
||||||
keywordHighlightView.repaint()
|
keywordHighlightView.repaint()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +209,8 @@ class NewKeywordHighlightDialog(
|
|||||||
val owner = this
|
val owner = this
|
||||||
val arc = UIManager.getInt("Component.arc")
|
val arc = UIManager.getInt("Component.arc")
|
||||||
val lineBorder = FlatLineBorder(Insets(1, 1, 1, 1), DynamicColor.BorderColor, 1f, arc)
|
val lineBorder = FlatLineBorder(Insets(1, 1, 1, 1), DynamicColor.BorderColor, 1f, arc)
|
||||||
val colorPanel = ColorPanel(color)
|
val colorPanel = ColorPanel()
|
||||||
|
colorPanel.background = color
|
||||||
colorPanel.preferredSize = keywordTextField.preferredSize
|
colorPanel.preferredSize = keywordTextField.preferredSize
|
||||||
colorPanel.border = lineBorder
|
colorPanel.border = lineBorder
|
||||||
colorPanel.addMouseListener(object : MouseAdapter() {
|
colorPanel.addMouseListener(object : MouseAdapter() {
|
||||||
@@ -200,10 +218,19 @@ class NewKeywordHighlightDialog(
|
|||||||
if (SwingUtilities.isLeftMouseButton(e)) {
|
if (SwingUtilities.isLeftMouseButton(e)) {
|
||||||
val dialog = ChooseColorTemplateDialog(owner, title)
|
val dialog = ChooseColorTemplateDialog(owner, title)
|
||||||
dialog.setLocationRelativeTo(owner)
|
dialog.setLocationRelativeTo(owner)
|
||||||
dialog.defaultColor = colorPanel.color
|
dialog.defaultColor = colorPanel.color ?: Color.orange
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
colorPanel.color = dialog.color ?: return
|
if (dialog.ok.not()) return
|
||||||
colorPanel.colorIndex = dialog.colorIndex
|
|
||||||
|
colorPanel.colorIndex = -1
|
||||||
|
colorPanel.color = null
|
||||||
|
if (dialog.colorIndex in 1..16) {
|
||||||
|
colorPanel.colorIndex = dialog.colorIndex
|
||||||
|
colorPanel.background = Color(colorPalette.getXTerm256Color(dialog.colorIndex))
|
||||||
|
} else {
|
||||||
|
colorPanel.color = dialog.color
|
||||||
|
}
|
||||||
|
repaintKeywordHighlightView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -218,13 +245,22 @@ class NewKeywordHighlightDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val newTextColor = if (textColor.color != null) textColor.color?.toRGB() ?: 0
|
||||||
|
else if (textColor.colorIndex == -1) 0
|
||||||
|
else textColor.colorIndex
|
||||||
|
|
||||||
|
|
||||||
|
val newBackgroundColor = if (backgroundColor.color != null) backgroundColor.color?.toRGB() ?: 0
|
||||||
|
else if (backgroundColor.colorIndex == -1) 0
|
||||||
|
else backgroundColor.colorIndex
|
||||||
|
|
||||||
keywordHighlight = KeywordHighlight(
|
keywordHighlight = KeywordHighlight(
|
||||||
keyword = keywordTextField.text,
|
keyword = keywordTextField.text,
|
||||||
description = descriptionTextField.text,
|
description = descriptionTextField.text,
|
||||||
matchCase = matchCaseBtn.isSelected,
|
matchCase = matchCaseBtn.isSelected,
|
||||||
regex = regexBtn.isSelected,
|
regex = regexBtn.isSelected,
|
||||||
textColor = if (textColor.colorIndex != -1) textColor.colorIndex else textColor.color.toRGB(),
|
textColor = newTextColor,
|
||||||
backgroundColor = if (backgroundColor.colorIndex != -1) backgroundColor.colorIndex else backgroundColor.color.toRGB(),
|
backgroundColor = newBackgroundColor,
|
||||||
bold = boldCheckBox.isSelected,
|
bold = boldCheckBox.isSelected,
|
||||||
italic = italicCheckBox.isSelected,
|
italic = italicCheckBox.isSelected,
|
||||||
lineThrough = lineThroughCheckBox.isSelected,
|
lineThrough = lineThroughCheckBox.isSelected,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import app.termora.plugin.internal.rdp.RDPInternalPlugin
|
|||||||
import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin
|
import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin
|
||||||
import app.termora.plugin.internal.ssh.SSHInternalPlugin
|
import app.termora.plugin.internal.ssh.SSHInternalPlugin
|
||||||
import app.termora.plugin.internal.telnet.TelnetInternalPlugin
|
import app.termora.plugin.internal.telnet.TelnetInternalPlugin
|
||||||
import app.termora.plugin.internal.update.UpdatePlugin
|
import app.termora.plugin.internal.updater.UpdaterPlugin
|
||||||
import app.termora.plugin.internal.wsl.WSLInternalPlugin
|
import app.termora.plugin.internal.wsl.WSLInternalPlugin
|
||||||
import app.termora.swingCoroutineScope
|
import app.termora.swingCoroutineScope
|
||||||
import app.termora.terminal.panel.vw.FloatingToolbarPlugin
|
import app.termora.terminal.panel.vw.FloatingToolbarPlugin
|
||||||
@@ -111,7 +111,7 @@ internal class PluginManager private constructor() {
|
|||||||
// badge plugin
|
// badge plugin
|
||||||
plugins.add(PluginDescriptor(BadgePlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(BadgePlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
// update plugin
|
// update plugin
|
||||||
plugins.add(PluginDescriptor(UpdatePlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(UpdaterPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
// frame plugin
|
// frame plugin
|
||||||
plugins.add(PluginDescriptor(FramePlugin(), origin = PluginOrigin.Internal, version = version))
|
plugins.add(PluginDescriptor(FramePlugin(), origin = PluginOrigin.Internal, version = version))
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ open class BasicTerminalOption() : JPanel(BorderLayout()), Option {
|
|||||||
var showCharsetComboBox: Boolean = false
|
var showCharsetComboBox: Boolean = false
|
||||||
var showStartupCommandTextField: Boolean = false
|
var showStartupCommandTextField: Boolean = false
|
||||||
var showHeartbeatIntervalTextField: Boolean = false
|
var showHeartbeatIntervalTextField: Boolean = false
|
||||||
|
var showTimeoutTextField: Boolean = false
|
||||||
var showEnvironmentTextArea: Boolean = false
|
var showEnvironmentTextArea: Boolean = false
|
||||||
var showLoginScripts: Boolean = false
|
var showLoginScripts: Boolean = false
|
||||||
var showBackspaceComboBox: Boolean = false
|
var showBackspaceComboBox: Boolean = false
|
||||||
@@ -33,7 +34,8 @@ open class BasicTerminalOption() : JPanel(BorderLayout()), Option {
|
|||||||
|
|
||||||
val charsetComboBox = JComboBox<String>()
|
val charsetComboBox = JComboBox<String>()
|
||||||
val startupCommandTextField = OutlineTextField()
|
val startupCommandTextField = OutlineTextField()
|
||||||
val heartbeatIntervalTextField = IntSpinner(30, minimum = 3, maximum = Int.MAX_VALUE)
|
val heartbeatIntervalTextField = IntSpinner(60, minimum = 3, maximum = Int.MAX_VALUE)
|
||||||
|
val timeoutTextField = IntSpinner(60, minimum = 10, maximum = Int.MAX_VALUE)
|
||||||
val environmentTextArea = FixedLengthTextArea(2048)
|
val environmentTextArea = FixedLengthTextArea(2048)
|
||||||
val loginScripts = mutableListOf<LoginScript>()
|
val loginScripts = mutableListOf<LoginScript>()
|
||||||
val backspaceComboBox = JComboBox<Backspace>()
|
val backspaceComboBox = JComboBox<Backspace>()
|
||||||
@@ -173,7 +175,7 @@ open class BasicTerminalOption() : JPanel(BorderLayout()), Option {
|
|||||||
private fun getCenterComponent(): JComponent {
|
private fun getCenterComponent(): JComponent {
|
||||||
val layout = FormLayout(
|
val layout = FormLayout(
|
||||||
"left:pref, $FORM_MARGIN, default:grow",
|
"left:pref, $FORM_MARGIN, default:grow",
|
||||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
)
|
)
|
||||||
|
|
||||||
val accountOwner = this.accountOwner
|
val accountOwner = this.accountOwner
|
||||||
@@ -210,6 +212,11 @@ open class BasicTerminalOption() : JPanel(BorderLayout()), Option {
|
|||||||
.add(characterAtATimeTextField).xy(3, rows).apply { rows += step }
|
.add(characterAtATimeTextField).xy(3, rows).apply { rows += step }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showTimeoutTextField) {
|
||||||
|
builder.add("${I18n.getString("termora.new-host.terminal.timeout")}:").xy(1, rows)
|
||||||
|
.add(timeoutTextField).xy(3, rows).apply { rows += step }
|
||||||
|
}
|
||||||
|
|
||||||
if (showHeartbeatIntervalTextField) {
|
if (showHeartbeatIntervalTextField) {
|
||||||
builder.add("${I18n.getString("termora.new-host.terminal.heartbeat-interval")}:").xy(1, rows)
|
builder.add("${I18n.getString("termora.new-host.terminal.heartbeat-interval")}:").xy(1, rows)
|
||||||
.add(heartbeatIntervalTextField).xy(3, rows).apply { rows += step }
|
.add(heartbeatIntervalTextField).xy(3, rows).apply { rows += step }
|
||||||
@@ -220,14 +227,12 @@ open class BasicTerminalOption() : JPanel(BorderLayout()), Option {
|
|||||||
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
|
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (showEnvironmentTextArea) {
|
if (showEnvironmentTextArea) {
|
||||||
builder.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
|
builder.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
|
||||||
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
|
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
|
||||||
.apply { rows += step }
|
.apply { rows += step }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(): Icon {
|
override fun getIcon(): Icon {
|
||||||
return if (unread) Icons.terminalUnread else Icons.terminal
|
return Icons.terminal
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun willBeClose(): Boolean {
|
override fun willBeClose(): Boolean {
|
||||||
@@ -62,6 +62,9 @@ class LocalTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
) == JOptionPane.OK_OPTION
|
) == JOptionPane.OK_OPTION
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createReconnectTerminalTab(): TerminalTab {
|
||||||
|
return LocalTerminalTab(windowScope, host)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getPtyProcessConnector(): PtyProcessConnector? {
|
private fun getPtyProcessConnector(): PtyProcessConnector? {
|
||||||
var p = getPtyConnector() as PtyConnector?
|
var p = getPtyConnector() as PtyConnector?
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -82,24 +82,42 @@ internal class RDPProtocolProvider private constructor() : GenericProtocolProvid
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val file = FileUtils.getFile(Application.getTemporaryDir(), randomUUID() + ".rdp")
|
|
||||||
file.outputStream().use { IOUtils.write(sb.toString(), it, Charsets.UTF_8) }
|
|
||||||
|
|
||||||
if (host.authentication.type == AuthenticationType.Password) {
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
val systemClipboard = windowScope.window.toolkit.systemClipboard
|
|
||||||
val password = host.authentication.password
|
val password = host.authentication.password
|
||||||
systemClipboard.setContents(StringSelection(password), null)
|
var ep = StringUtils.EMPTY
|
||||||
// clear password
|
|
||||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
if (SystemInfo.isWindows) {
|
||||||
delay(30.seconds)
|
val cmd = "ConvertTo-SecureString '${password}' -AsPlainText -Force | ConvertFrom-SecureString"
|
||||||
if (systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
val process = ProcessBuilder("powershell.exe", "-Command", cmd).start()
|
||||||
if (systemClipboard.getData(DataFlavor.stringFlavor) == password) {
|
if (process.waitFor() == 0) {
|
||||||
systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
|
ep = String(process.inputStream.readAllBytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ep.isNotBlank()) {
|
||||||
|
sb.append("password 51:b:").append(ep).appendLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果获取加密密码失败,那么依然要走剪切板
|
||||||
|
if (ep.isBlank() || SystemInfo.isMacOS) {
|
||||||
|
val systemClipboard = windowScope.window.toolkit.systemClipboard
|
||||||
|
systemClipboard.setContents(StringSelection(password), null)
|
||||||
|
// clear password
|
||||||
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
|
delay(30.seconds)
|
||||||
|
if (systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||||
|
if (systemClipboard.getData(DataFlavor.stringFlavor) == password) {
|
||||||
|
systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val file = FileUtils.getFile(Application.getTemporaryDir(), randomUUID() + ".rdp")
|
||||||
|
file.outputStream().use { IOUtils.write(sb.toString(), it, Charsets.UTF_8) }
|
||||||
|
|
||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isMacOS) {
|
||||||
ProcessBuilder("open", file.absolutePath).start()
|
ProcessBuilder("open", file.absolutePath).start()
|
||||||
} else if (SystemInfo.isWindows) {
|
} else if (SystemInfo.isWindows) {
|
||||||
|
|||||||
@@ -103,9 +103,20 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
|||||||
commands.add("Compression=yes")
|
commands.add("Compression=yes")
|
||||||
|
|
||||||
// HostKeyAlgorithms 让 SFTP 命令的顺序和 sshd 的一致 这样可以避免 known_hosts 文件不一致问题
|
// HostKeyAlgorithms 让 SFTP 命令的顺序和 sshd 的一致 这样可以避免 known_hosts 文件不一致问题
|
||||||
val hostKeyAlgorithms = ClientBuilder.setUpDefaultSignatureFactories(true).joinToString(",") { it.name }
|
val hostKeyAlgorithms = ClientBuilder.setUpDefaultSignatureFactories(true).map { it.name }.toMutableList()
|
||||||
|
val localHostKeyAlgorithms = getLocalSSHHostKeyAlgorithms()
|
||||||
|
// 删除本地 ssh 不存在的算法
|
||||||
|
hostKeyAlgorithms.removeIf { localHostKeyAlgorithms.contains(it).not() }
|
||||||
|
|
||||||
|
// 把本地支持的再添加进去
|
||||||
|
for (algorithm in localHostKeyAlgorithms) {
|
||||||
|
if (hostKeyAlgorithms.contains(algorithm).not()) {
|
||||||
|
hostKeyAlgorithms.add(algorithm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
commands.add("-o")
|
commands.add("-o")
|
||||||
commands.add("HostKeyAlgorithms=${hostKeyAlgorithms}")
|
commands.add("HostKeyAlgorithms=${hostKeyAlgorithms.joinToString(",")}")
|
||||||
|
|
||||||
// 不使用配置文件
|
// 不使用配置文件
|
||||||
commands.add("-F")
|
commands.add("-F")
|
||||||
@@ -143,6 +154,15 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
|||||||
return ptyConnector
|
return ptyConnector
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getLocalSSHHostKeyAlgorithms(): Set<String> {
|
||||||
|
val pb = ProcessBuilder("ssh", "-Q", "key")
|
||||||
|
val process = pb.start()
|
||||||
|
if (process.waitFor() != 0) {
|
||||||
|
return emptySet()
|
||||||
|
}
|
||||||
|
return String(process.inputStream.readAllBytes()).lines().filter { it.isNotBlank() }.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setAuthentication(commands: MutableList<String>, host: Host) {
|
private fun setAuthentication(commands: MutableList<String>, host: Host) {
|
||||||
// 如果通过公钥连接
|
// 如果通过公钥连接
|
||||||
if (host.authentication.type == AuthenticationType.PublicKey) {
|
if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||||
@@ -185,6 +205,10 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal
|
|||||||
return Icons.fileFormat
|
return Icons.fileFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createReconnectTerminalTab(): TerminalTab {
|
||||||
|
return SFTPPtyTerminalTab(windowScope, host)
|
||||||
|
}
|
||||||
|
|
||||||
override fun sendStartupCommand(ptyConnector: PtyConnector, bytes: ByteArray) {
|
override fun sendStartupCommand(ptyConnector: PtyConnector, bytes: ByteArray) {
|
||||||
// Nothing
|
// Nothing
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -24,6 +25,7 @@ import org.eclipse.jgit.internal.transport.sshd.agent.connector.WinPipeConnector
|
|||||||
import java.awt.*
|
import java.awt.*
|
||||||
import java.awt.event.*
|
import java.awt.event.*
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
import javax.swing.event.DocumentEvent
|
||||||
import javax.swing.table.DefaultTableCellRenderer
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
import javax.swing.table.DefaultTableModel
|
import javax.swing.table.DefaultTableModel
|
||||||
|
|
||||||
@@ -35,9 +37,11 @@ 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
|
||||||
|
showTimeoutTextField = true
|
||||||
showHighlightSet = true
|
showHighlightSet = true
|
||||||
accountOwner = this@SSHHostOptionsPane.accountOwner
|
accountOwner = this@SSHHostOptionsPane.accountOwner
|
||||||
init()
|
init()
|
||||||
@@ -45,6 +49,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
|||||||
private val jumpHostsOption = JumpHostsOption()
|
private val jumpHostsOption = JumpHostsOption()
|
||||||
private val sftpOption = SFTPOption()
|
private val sftpOption = SFTPOption()
|
||||||
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
private val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
private var setHostMode = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
addOption(generalOption)
|
addOption(generalOption)
|
||||||
@@ -109,10 +114,13 @@ 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
|
||||||
?: "-1"),
|
?: "-1"),
|
||||||
|
"timeout" to (terminalOption.timeoutTextField.value ?: 60).toString(),
|
||||||
|
"forwardAgent" to tunnelingOption.forwardAgentCheckBox.isSelected.toString(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -132,6 +140,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setHost(host: Host) {
|
fun setHost(host: Host) {
|
||||||
|
setHostMode = true
|
||||||
generalOption.portTextField.value = host.port
|
generalOption.portTextField.value = host.port
|
||||||
generalOption.nameTextField.text = host.name
|
generalOption.nameTextField.text = host.name
|
||||||
generalOption.usernameTextField.text = host.username
|
generalOption.usernameTextField.text = host.username
|
||||||
@@ -163,6 +172,13 @@ 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"
|
||||||
|
terminalOption.timeoutTextField.value = timeout.toIntOrNull() ?: 60
|
||||||
|
|
||||||
|
|
||||||
val keywordHighlightSetId = host.options.extras["keywordHighlightSetId"]
|
val keywordHighlightSetId = host.options.extras["keywordHighlightSetId"]
|
||||||
for (i in 0 until terminalOption.highlightSetComboBox.itemCount) {
|
for (i in 0 until terminalOption.highlightSetComboBox.itemCount) {
|
||||||
val item = terminalOption.highlightSetComboBox.getItemAt(i)
|
val item = terminalOption.highlightSetComboBox.getItemAt(i)
|
||||||
@@ -176,6 +192,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
|||||||
tunnelingOption.tunnelings.addAll(host.tunnelings)
|
tunnelingOption.tunnelings.addAll(host.tunnelings)
|
||||||
tunnelingOption.x11ForwardingCheckBox.isSelected = host.options.enableX11Forwarding
|
tunnelingOption.x11ForwardingCheckBox.isSelected = host.options.enableX11Forwarding
|
||||||
tunnelingOption.x11ServerTextField.text = StringUtils.defaultIfBlank(host.options.x11Forwarding, "localhost:0")
|
tunnelingOption.x11ServerTextField.text = StringUtils.defaultIfBlank(host.options.x11Forwarding, "localhost:0")
|
||||||
|
tunnelingOption.forwardAgentCheckBox.isSelected = host.options.extras["forwardAgent"]?.toBoolean() ?: false
|
||||||
|
|
||||||
if (host.options.jumpHosts.isNotEmpty()) {
|
if (host.options.jumpHosts.isNotEmpty()) {
|
||||||
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
|
||||||
@@ -290,6 +307,8 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
|||||||
val remarkTextArea = FixedLengthTextArea(512)
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||||
|
|
||||||
|
private var hostFocused = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
initEvents()
|
initEvents()
|
||||||
@@ -397,6 +416,26 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
|||||||
removeComponentListener(this)
|
removeComponentListener(this)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
hostTextField.addFocusListener(object : FocusAdapter() {
|
||||||
|
override fun focusGained(e: FocusEvent) {
|
||||||
|
hostTextField.removeFocusListener(this)
|
||||||
|
hostFocused = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
nameTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
|
if (nameTextField.hasFocus().not()) return
|
||||||
|
|
||||||
|
if (hostFocused || setHostMode) {
|
||||||
|
nameTextField.document.removeDocumentListener(this)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hostTextField.text = nameTextField.text
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun chooseKeyPair() {
|
private fun chooseKeyPair() {
|
||||||
@@ -564,9 +603,10 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected inner class TunnelingOption : JPanel(BorderLayout()), Option {
|
private inner class TunnelingOption : JPanel(BorderLayout()), Option {
|
||||||
val tunnelings = mutableListOf<Tunneling>()
|
val tunnelings = mutableListOf<Tunneling>()
|
||||||
val x11ForwardingCheckBox = JCheckBox("X DISPLAY:")
|
val x11ForwardingCheckBox = JCheckBox("X DISPLAY:")
|
||||||
|
val forwardAgentCheckBox = JCheckBox("Enable ForwardAgent")
|
||||||
val x11ServerTextField = OutlineTextField(255)
|
val x11ServerTextField = OutlineTextField(255)
|
||||||
|
|
||||||
private val model = object : DefaultTableModel() {
|
private val model = object : DefaultTableModel() {
|
||||||
@@ -643,6 +683,7 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
|||||||
box.add(deleteBtn)
|
box.add(deleteBtn)
|
||||||
|
|
||||||
x11ForwardingCheckBox.isFocusable = false
|
x11ForwardingCheckBox.isFocusable = false
|
||||||
|
forwardAgentCheckBox.isFocusable = false
|
||||||
|
|
||||||
if (x11ServerTextField.text.isBlank()) {
|
if (x11ServerTextField.text.isBlank()) {
|
||||||
x11ServerTextField.text = "localhost:0"
|
x11ServerTextField.text = "localhost:0"
|
||||||
@@ -656,6 +697,13 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
|||||||
x11Forwarding.add(x11ForwardingCheckBox)
|
x11Forwarding.add(x11ForwardingCheckBox)
|
||||||
x11Forwarding.add(x11ServerTextField)
|
x11Forwarding.add(x11ServerTextField)
|
||||||
|
|
||||||
|
val forwardAgent = Box.createHorizontalBox()
|
||||||
|
forwardAgent.border = BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createTitledBorder("Agent Forwarding"),
|
||||||
|
BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
|
)
|
||||||
|
forwardAgent.add(forwardAgentCheckBox)
|
||||||
|
|
||||||
x11ServerTextField.isEnabled = x11ForwardingCheckBox.isSelected
|
x11ServerTextField.isEnabled = x11ForwardingCheckBox.isSelected
|
||||||
|
|
||||||
val panel = JPanel(BorderLayout())
|
val panel = JPanel(BorderLayout())
|
||||||
@@ -664,8 +712,13 @@ internal class SSHHostOptionsPane(private val accountOwner: AccountOwner) : Opti
|
|||||||
panel.add(box, BorderLayout.SOUTH)
|
panel.add(box, BorderLayout.SOUTH)
|
||||||
panel.border = BorderFactory.createEmptyBorder(0, 0, 8, 0)
|
panel.border = BorderFactory.createEmptyBorder(0, 0, 8, 0)
|
||||||
|
|
||||||
|
val forwardingBox = Box.createHorizontalBox()
|
||||||
|
forwardingBox.add(x11Forwarding)
|
||||||
|
forwardingBox.add(Box.createHorizontalStrut(4))
|
||||||
|
forwardingBox.add(forwardAgent)
|
||||||
|
|
||||||
add(panel, BorderLayout.CENTER)
|
add(panel, BorderLayout.CENTER)
|
||||||
add(x11Forwarding, BorderLayout.SOUTH)
|
add(forwardingBox, BorderLayout.SOUTH)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -54,6 +54,10 @@ class SSHTerminalTab(
|
|||||||
return mutex.isLocked.not()
|
return mutex.isLocked.not()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createReconnectTerminalTab(): TerminalTab {
|
||||||
|
return SSHTerminalTab(windowScope, host)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun openPtyConnector(): PtyConnector {
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
if (mutex.tryLock()) {
|
if (mutex.tryLock()) {
|
||||||
try {
|
try {
|
||||||
@@ -79,15 +83,14 @@ class SSHTerminalTab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val loading = coroutineScope.launch(Dispatchers.Swing) {
|
val loading = coroutineScope.launch(Dispatchers.Swing) {
|
||||||
|
val braille = "⡿⣟⣯⣷⣾⣽⣻⢿".reversed().toCharArray()
|
||||||
|
// val braille = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏".toCharArray()
|
||||||
var c = 0
|
var c = 0
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
if (++c > 6) c = 1
|
if (++c >= braille.size) c = 0
|
||||||
terminal.write("${ControlCharacters.ESC}[1;32m")
|
terminal.write("${braille[c]}")
|
||||||
terminal.write(".".repeat(c))
|
delay(100.milliseconds)
|
||||||
terminal.write(" ".repeat(6 - c))
|
terminal.write("${ControlCharacters.BS}")
|
||||||
terminal.write("${ControlCharacters.ESC}[0m")
|
|
||||||
delay(350.milliseconds)
|
|
||||||
terminal.write("${ControlCharacters.BS}".repeat(6))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,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(
|
||||||
@@ -211,17 +225,6 @@ class SSHTerminalTab(
|
|||||||
return super.getData(dataKey)
|
return super.getData(dataKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reconnect() {
|
|
||||||
stop()
|
|
||||||
|
|
||||||
// 重新连接时就等于重新打开了一个标签,handler 重置
|
|
||||||
handler.client = null
|
|
||||||
handler.session = null
|
|
||||||
handler.client = null
|
|
||||||
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
if (mutex.tryLock()) {
|
if (mutex.tryLock()) {
|
||||||
try {
|
try {
|
||||||
@@ -234,7 +237,7 @@ class SSHTerminalTab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(): Icon {
|
override fun getIcon(): Icon {
|
||||||
return if (unread) Icons.terminalUnread else Icons.terminal
|
return Icons.terminal
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun beforeClose() {
|
override fun beforeClose() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import org.apache.sshd.agent.local.ChannelAgentForwardingFactory
|
||||||
|
import org.apache.sshd.common.FactoryManager
|
||||||
|
import org.apache.sshd.common.channel.ChannelFactory
|
||||||
|
import org.eclipse.jgit.internal.transport.sshd.agent.JGitSshAgentFactory
|
||||||
|
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
internal class SshAgentFactory(factory: ConnectorFactory, homeDir: File?) : JGitSshAgentFactory(factory, homeDir) {
|
||||||
|
override fun getChannelForwardingFactories(manager: FactoryManager?): List<ChannelFactory> {
|
||||||
|
return listOf(ChannelAgentForwardingFactory.OPENSSH, ChannelAgentForwardingFactory.IETF)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,7 +56,6 @@ import org.apache.sshd.server.forward.AcceptAllForwardingFilter
|
|||||||
import org.apache.sshd.server.forward.RejectAllForwardingFilter
|
import org.apache.sshd.server.forward.RejectAllForwardingFilter
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession
|
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient
|
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient
|
||||||
import org.eclipse.jgit.internal.transport.sshd.agent.JGitSshAgentFactory
|
|
||||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.PageantConnector
|
import org.eclipse.jgit.internal.transport.sshd.agent.connector.PageantConnector
|
||||||
import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocketConnector
|
import org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixDomainSocketConnector
|
||||||
import org.eclipse.jgit.internal.transport.sshd.proxy.AbstractClientProxyConnector
|
import org.eclipse.jgit.internal.transport.sshd.proxy.AbstractClientProxyConnector
|
||||||
@@ -88,7 +87,6 @@ object SshClients {
|
|||||||
|
|
||||||
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
|
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
|
||||||
|
|
||||||
private val timeout = Duration.ofSeconds(30)
|
|
||||||
private val hostManager get() = HostManager.Companion.getInstance()
|
private val hostManager get() = HostManager.Companion.getInstance()
|
||||||
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
|
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
|
||||||
|
|
||||||
@@ -101,6 +99,7 @@ object SshClients {
|
|||||||
session: ClientSession,
|
session: ClientSession,
|
||||||
): ChannelShell {
|
): ChannelShell {
|
||||||
|
|
||||||
|
val timeout = Duration.ofSeconds(host.options.extras["timeout"]?.toLongOrNull() ?: 60)
|
||||||
|
|
||||||
val configuration = PtyChannelConfiguration()
|
val configuration = PtyChannelConfiguration()
|
||||||
configuration.ptyColumns = size.cols
|
configuration.ptyColumns = size.cols
|
||||||
@@ -112,6 +111,8 @@ object SshClients {
|
|||||||
env.putAll(host.options.envs())
|
env.putAll(host.options.envs())
|
||||||
|
|
||||||
val channel = session.createShellChannel(configuration, env)
|
val channel = session.createShellChannel(configuration, env)
|
||||||
|
channel.isAgentForwarding = host.options.extras["forwardAgent"]?.toBoolean() == true
|
||||||
|
|
||||||
if (host.options.enableX11Forwarding) {
|
if (host.options.enableX11Forwarding) {
|
||||||
if (channel is app.termora.x11.ChannelShell) {
|
if (channel is app.termora.x11.ChannelShell) {
|
||||||
channel.xForwarding = true
|
channel.xForwarding = true
|
||||||
@@ -136,6 +137,7 @@ object SshClients {
|
|||||||
command: String
|
command: String
|
||||||
): Pair<Int, String> {
|
): Pair<Int, String> {
|
||||||
|
|
||||||
|
val timeout = Duration.ofSeconds(60)
|
||||||
val baos = ByteArrayOutputStream()
|
val baos = ByteArrayOutputStream()
|
||||||
val channel = session.createExecChannel(command)
|
val channel = session.createExecChannel(command)
|
||||||
channel.out = baos
|
channel.out = baos
|
||||||
@@ -248,6 +250,7 @@ object SshClients {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val timeout = Duration.ofSeconds(host.options.extras["timeout"]?.toLongOrNull() ?: 60)
|
||||||
val session = client.connect(entry).verify(timeout).session
|
val session = client.connect(entry).verify(timeout).session
|
||||||
if (host.authentication.type == AuthenticationType.Password) {
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
if (StringUtils.isNotBlank(host.authentication.password))
|
if (StringUtils.isNotBlank(host.authentication.password))
|
||||||
@@ -384,7 +387,7 @@ object SshClients {
|
|||||||
|
|
||||||
val channelFactories = mutableListOf<ChannelFactory>()
|
val channelFactories = mutableListOf<ChannelFactory>()
|
||||||
channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES)
|
channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES)
|
||||||
channelFactories.add(X11ChannelFactory.Companion.INSTANCE)
|
channelFactories.add(X11ChannelFactory.INSTANCE)
|
||||||
builder.channelFactories(channelFactories)
|
builder.channelFactories(channelFactories)
|
||||||
|
|
||||||
val sshClient = builder.build() as JGitSshClient
|
val sshClient = builder.build() as JGitSshClient
|
||||||
@@ -393,12 +396,14 @@ object SshClients {
|
|||||||
// JGit 会尝试读取本地的私钥或缓存的私钥
|
// JGit 会尝试读取本地的私钥或缓存的私钥
|
||||||
sshClient.keyIdentityProvider = KeyIdentityProvider { mutableListOf() }
|
sshClient.keyIdentityProvider = KeyIdentityProvider { mutableListOf() }
|
||||||
|
|
||||||
|
// https://github.com/TermoraDev/termora/issues/1001
|
||||||
|
if (host.authentication.type == AuthenticationType.SSHAgent || host.options.extras["forwardAgent"]?.toBoolean() == true) {
|
||||||
|
// ssh-agent
|
||||||
|
sshClient.agentFactory = SshAgentFactory(ConnectorFactory.getDefault(), null)
|
||||||
|
}
|
||||||
|
|
||||||
// 设置优先级
|
// 设置优先级
|
||||||
if (host.authentication.type == AuthenticationType.PublicKey || host.authentication.type == AuthenticationType.SSHAgent) {
|
if (host.authentication.type == AuthenticationType.PublicKey || host.authentication.type == AuthenticationType.SSHAgent) {
|
||||||
if (host.authentication.type == AuthenticationType.SSHAgent) {
|
|
||||||
// ssh-agent
|
|
||||||
sshClient.agentFactory = JGitSshAgentFactory(ConnectorFactory.getDefault(), null)
|
|
||||||
}
|
|
||||||
CoreModuleProperties.PREFERRED_AUTHS.set(
|
CoreModuleProperties.PREFERRED_AUTHS.set(
|
||||||
sshClient,
|
sshClient,
|
||||||
listOf(
|
listOf(
|
||||||
@@ -420,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()) }
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package app.termora.plugin.internal.telnet
|
package app.termora.plugin.internal.telnet
|
||||||
|
|
||||||
import app.termora.Host
|
import app.termora.*
|
||||||
import app.termora.ProxyType
|
|
||||||
import app.termora.PtyHostTerminalTab
|
|
||||||
import app.termora.WindowScope
|
|
||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.terminal.ControlCharacters
|
||||||
import app.termora.terminal.KeyEncoderImpl
|
import app.termora.terminal.KeyEncoderImpl
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
@@ -71,5 +68,9 @@ class TelnetTerminalTab(
|
|||||||
return ptyConnectorFactory.decorate(TelnetStreamPtyConnector(telnet, telnet.charset, characterMode))
|
return ptyConnectorFactory.decorate(TelnetStreamPtyConnector(telnet, telnet.charset, characterMode))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createReconnectTerminalTab(): TerminalTab {
|
||||||
|
return TelnetTerminalTab(windowScope, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
package app.termora.plugin.internal.update
|
|
||||||
|
|
||||||
import app.termora.*
|
|
||||||
import app.termora.actions.AnAction
|
|
||||||
import app.termora.actions.AnActionEvent
|
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
|
||||||
import com.sun.jna.platform.win32.Advapi32
|
|
||||||
import com.sun.jna.platform.win32.WinError
|
|
||||||
import com.sun.jna.platform.win32.WinNT
|
|
||||||
import com.sun.jna.platform.win32.WinReg
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.jdesktop.swingx.JXEditorPane
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.awt.Dimension
|
|
||||||
import java.awt.KeyboardFocusManager
|
|
||||||
import java.net.URI
|
|
||||||
import javax.swing.BorderFactory
|
|
||||||
import javax.swing.JOptionPane
|
|
||||||
import javax.swing.JScrollPane
|
|
||||||
import javax.swing.UIManager
|
|
||||||
import javax.swing.event.HyperlinkEvent
|
|
||||||
|
|
||||||
internal class AppUpdateAction private constructor() : AnAction(StringUtils.EMPTY, Icons.ideUpdate) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val log = LoggerFactory.getLogger(AppUpdateAction::class.java)
|
|
||||||
|
|
||||||
fun getInstance(): AppUpdateAction {
|
|
||||||
return ApplicationScope.forApplicationScope().getOrCreate(AppUpdateAction::class) { AppUpdateAction() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val updaterManager get() = UpdaterManager.getInstance()
|
|
||||||
|
|
||||||
init {
|
|
||||||
isEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
|
||||||
showUpdateDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun showUpdateDialog() {
|
|
||||||
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
|
|
||||||
val lastVersion = updaterManager.lastVersion
|
|
||||||
val editorPane = JXEditorPane()
|
|
||||||
editorPane.contentType = "text/html"
|
|
||||||
editorPane.text = lastVersion.htmlBody
|
|
||||||
editorPane.isEditable = false
|
|
||||||
editorPane.addHyperlinkListener {
|
|
||||||
if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) {
|
|
||||||
Application.browse(it.url.toURI())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
editorPane.background = DynamicColor("window")
|
|
||||||
val scrollPane = JScrollPane(editorPane)
|
|
||||||
scrollPane.border = BorderFactory.createEmptyBorder()
|
|
||||||
scrollPane.preferredSize = Dimension(
|
|
||||||
UIManager.getInt("Dialog.width") - 100,
|
|
||||||
UIManager.getInt("Dialog.height") - 100
|
|
||||||
)
|
|
||||||
|
|
||||||
val option = OptionPane.showConfirmDialog(
|
|
||||||
owner,
|
|
||||||
scrollPane,
|
|
||||||
title = I18n.getString("termora.update.title"),
|
|
||||||
messageType = JOptionPane.PLAIN_MESSAGE,
|
|
||||||
optionType = JOptionPane.OK_CANCEL_OPTION,
|
|
||||||
options = arrayOf(
|
|
||||||
I18n.getString("termora.update.update"),
|
|
||||||
I18n.getString("termora.cancel")
|
|
||||||
),
|
|
||||||
initialValue = I18n.getString("termora.update.update")
|
|
||||||
)
|
|
||||||
if (option == JOptionPane.CANCEL_OPTION) {
|
|
||||||
return
|
|
||||||
} else if (option == JOptionPane.OK_OPTION) {
|
|
||||||
updateSelf(lastVersion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateSelf(latestVersion: UpdaterManager.LatestVersion) {
|
|
||||||
val pkg = Updater.getInstance().getLatestPkg()
|
|
||||||
if (SystemInfo.isLinux || pkg == null) {
|
|
||||||
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${latestVersion.version}"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val file = pkg.file
|
|
||||||
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
|
||||||
val commands = if (SystemInfo.isMacOS) listOf("open", "-n", file.absolutePath)
|
|
||||||
// 如果安装过,那么直接静默安装和自动启动
|
|
||||||
else if (isAppInstalled()) listOf(
|
|
||||||
file.absolutePath,
|
|
||||||
"/SILENT",
|
|
||||||
"/AUTOSTART",
|
|
||||||
"/NORESTART",
|
|
||||||
"/FORCECLOSEAPPLICATIONS"
|
|
||||||
)
|
|
||||||
// 没有安装过 则打开安装向导
|
|
||||||
else listOf(file.absolutePath)
|
|
||||||
|
|
||||||
if (log.isInfoEnabled) {
|
|
||||||
log.info("restart {}", commands.joinToString(StringUtils.SPACE))
|
|
||||||
}
|
|
||||||
|
|
||||||
TermoraRestarter.getInstance().scheduleRestart(owner, true, commands)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isAppInstalled(): Boolean {
|
|
||||||
val keyPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${Application.getName()}_is1"
|
|
||||||
val phkKey = WinReg.HKEYByReference()
|
|
||||||
|
|
||||||
// 尝试打开注册表键
|
|
||||||
val result = Advapi32.INSTANCE.RegOpenKeyEx(
|
|
||||||
WinReg.HKEY_LOCAL_MACHINE,
|
|
||||||
keyPath,
|
|
||||||
0,
|
|
||||||
WinNT.KEY_READ,
|
|
||||||
phkKey
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result == WinError.ERROR_SUCCESS) {
|
|
||||||
// 键存在,关闭句柄
|
|
||||||
Advapi32.INSTANCE.RegCloseKey(phkKey.getValue())
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
// 键不存在或无权限
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package app.termora.plugin.internal.update
|
|
||||||
|
|
||||||
import app.termora.ApplicationRunnerExtension
|
|
||||||
|
|
||||||
internal class MyApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
|
|
||||||
companion object {
|
|
||||||
val instance = MyApplicationRunnerExtension()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun ready() {
|
|
||||||
Updater.getInstance().scheduleUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user