chore!: migrate to version 2.x

This commit is contained in:
hstyi
2025-06-13 15:16:56 +08:00
committed by GitHub
parent ca484618c7
commit 6177bbdc68
444 changed files with 18594 additions and 3832 deletions

View File

@@ -2,10 +2,6 @@ annotations
Apache License 2.0 Apache License 2.0
https://github.com/JetBrains/java-annotations/blob/master/LICENSE.txt https://github.com/JetBrains/java-annotations/blob/master/LICENSE.txt
kotlin-bip39
MIT License
https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/LICENSE
colorpicker colorpicker
BSD 3-Clause "New" or "Revised" License BSD 3-Clause "New" or "Revised" License
https://github.com/dheid/colorpicker/blob/main/LICENSE https://github.com/dheid/colorpicker/blob/main/LICENSE
@@ -18,10 +14,6 @@ commons-codec
Apache License 2.0 Apache License 2.0
https://github.com/apache/commons-codec/blob/master/LICENSE.txt https://github.com/apache/commons-codec/blob/master/LICENSE.txt
commons-compress
Apache License 2.0
https://github.com/apache/commons-compress/blob/master/LICENSE.txt
commons-vfs2 commons-vfs2
Apache License 2.0 Apache License 2.0
https://github.com/apache/commons-vfs/blob/master/LICENSE.txt https://github.com/apache/commons-vfs/blob/master/LICENSE.txt
@@ -226,26 +218,6 @@ versioncompare
Apache License 2.0 Apache License 2.0
https://github.com/G00fY2/version-compare/blob/main/LICENSE https://github.com/G00fY2/version-compare/blob/main/LICENSE
xodus-compress
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
xodus-environment
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
xodus-openAPI
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
xodus-utils
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
xodus-vfs
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
jediterm jediterm
Apache License 2.0 Apache License 2.0
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
@@ -261,3 +233,31 @@ https://github.com/stleary/JSON-java/blob/master/LICENSE
jSerialComm jSerialComm
Apache License 2.0 Apache License 2.0
https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0 https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0
exposed-core
Apache License 2.0
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
exposed-crypt
Apache License 2.0
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
exposed-jdbc
Apache License 2.0
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
sqlite-jdbc
Apache License 2.0
https://www.apache.org/licenses/LICENSE-2.0.txt
java-uuid-generator
Apache License 2.0
https://github.com/cowtowncoder/java-uuid-generator/blob/master/LICENSE
semver4j
MIT
https://github.com/semver4j/semver4j/blob/main/LICENSE
dom4j
Plexus (https://dom4j.github.io)
https://github.com/dom4j/dom4j/blob/master/LICENSE

1
VERSION Normal file
View File

@@ -0,0 +1 @@
2.0.0-beta.1

View File

@@ -5,8 +5,10 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
import org.jetbrains.kotlin.org.apache.commons.io.filefilter.FileFilterUtils import org.jetbrains.kotlin.org.apache.commons.io.filefilter.FileFilterUtils
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
import org.jetbrains.kotlin.org.apache.commons.lang3.time.DateFormatUtils
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.nio.file.Files import java.nio.file.Files
import java.util.*
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.Future import java.util.concurrent.Future
@@ -21,10 +23,11 @@ plugins {
group = "app.termora" group = "app.termora"
version = "1.0.16" version = rootProject.projectDir.resolve("VERSION").readText().trim()
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem() val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture() val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
val appVersion = project.version.toString().split("-")[0]
// macOS 签名信息 // macOS 签名信息
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
@@ -36,15 +39,16 @@ val macOSNotaryKeychainProfile = System.getenv("TERMORA_MAC_NOTARY_KEYCHAIN_PROF
val macOSNotary = macOSSign && macOSNotaryKeychainProfile.isNotBlank() val macOSNotary = macOSSign && macOSNotaryKeychainProfile.isNotBlank()
&& System.getenv("TERMORA_MAC_NOTARY").toBoolean() && System.getenv("TERMORA_MAC_NOTARY").toBoolean()
allprojects {
repositories { repositories {
mavenCentral() mavenCentral()
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies")
maven("https://www.jitpack.io") maven("https://www.jitpack.io")
maven("https://central.sonatype.com/repository/maven-snapshots")
}
} }
dependencies { dependencies {
// 由于签名和公证macOS 不携带 natives
val useNoNativesFlatLaf = os.isMacOsX && System.getenv("ENABLE_BUILD").toBoolean()
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
testImplementation(libs.hutool) testImplementation(libs.hutool)
@@ -54,6 +58,8 @@ dependencies {
testImplementation(libs.delight.rhino.sandbox) testImplementation(libs.delight.rhino.sandbox)
testImplementation(platform(libs.testcontainers.bom)) testImplementation(platform(libs.testcontainers.bom))
testImplementation(libs.testcontainers) testImplementation(libs.testcontainers)
testImplementation(libs.h2)
testImplementation(libs.exposed.migration)
// implementation(platform(libs.koin.bom)) // implementation(platform(libs.koin.bom))
// implementation(libs.koin.core) // implementation(libs.koin.core)
@@ -67,28 +73,13 @@ dependencies {
api(libs.commons.csv) api(libs.commons.csv)
api(libs.commons.net) api(libs.commons.net)
api(libs.commons.text) api(libs.commons.text)
api(libs.commons.compress)
api(libs.commons.vfs2) { exclude(group = "*", module = "*") } api(libs.commons.vfs2) { exclude(group = "*", module = "*") }
api(libs.kotlinx.coroutines.swing) api(libs.kotlinx.coroutines.swing)
api(libs.kotlinx.coroutines.core) api(libs.kotlinx.coroutines.core)
api(libs.flatlaf) { api(libs.flatlaf)
artifact { api(libs.flatlafextras)
if (useNoNativesFlatLaf) { api(libs.flatlafswingx)
classifier = "no-natives"
}
}
}
api(libs.flatlaf.extras) {
if (useNoNativesFlatLaf) {
exclude(group = "com.formdev", module = "flatlaf")
}
}
api(libs.flatlaf.swingx) {
if (useNoNativesFlatLaf) {
exclude(group = "com.formdev", module = "flatlaf")
}
}
api(libs.kotlinx.serialization.json) api(libs.kotlinx.serialization.json)
api(libs.swingx) api(libs.swingx)
@@ -109,24 +100,26 @@ dependencies {
api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") } api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
api(libs.eddsa) api(libs.eddsa)
api(libs.jnafilechooser) api(libs.jnafilechooser)
api(libs.xodus.vfs)
api(libs.xodus.openAPI)
api(libs.xodus.environment)
api(libs.bip39)
api(libs.colorpicker) api(libs.colorpicker)
api(libs.mixpanel) api(libs.mixpanel)
api(libs.jSerialComm) api(libs.jSerialComm)
api(libs.ini4j) api(libs.ini4j)
api(libs.restart4j) api(libs.restart4j)
api(libs.exposed.core)
api(libs.exposed.crypt)
api(libs.exposed.jdbc)
api(libs.sqlite)
api(libs.jug)
api(libs.semver4j)
api(libs.jsvg)
api(libs.dom4j) { exclude(group = "*", module = "*") }
} }
application { application {
val args = mutableListOf( val args = mutableListOf(
"-Xmx2g", "-Xmx2048m",
"-XX:+UseZGC", "-Drelease-date=${DateFormatUtils.format(Date(), "yyyy-MM-dd")}"
"-XX:+ZUncommit",
"-XX:+ZGenerational",
"-XX:ZUncommitDelay=60",
) )
if (os.isMacOsX) { if (os.isMacOsX) {
@@ -139,7 +132,7 @@ application {
args.add("-Dapple.awt.application.appearance=system") args.add("-Dapple.awt.application.appearance=system")
} }
args.add("-Dapp-version=${project.version}") args.add("-DTERMORA_PLUGIN_DIRECTORY=${layout.buildDirectory.get().asFile.absolutePath}${File.separator}plugins")
if (os.isLinux) { if (os.isLinux) {
args.add("-Dsun.java2d.opengl=true") args.add("-Dsun.java2d.opengl=true")
@@ -153,6 +146,7 @@ publishing {
publications { publications {
create<MavenPublication>("mavenJava") { create<MavenPublication>("mavenJava") {
from(components["java"]) from(components["java"])
pom { pom {
name = project.name name = project.name
description = "Termora is a terminal emulator and SSH client for Windows, macOS and Linux" description = "Termora is a terminal emulator and SSH client for Windows, macOS and Linux"
@@ -189,8 +183,10 @@ tasks.register<Copy>("copy-dependencies") {
from(configurations.runtimeClasspath).into(dir) from(configurations.runtimeClasspath).into(dir)
val jna = libs.jna.asProvider().get() val jna = libs.jna.asProvider().get()
val pty4j = libs.pty4j.get() val pty4j = libs.pty4j.get()
val flatlaf = libs.flatlaf.get()
val jSerialComm = libs.jSerialComm.get() val jSerialComm = libs.jSerialComm.get()
val restart4j = libs.restart4j.get() val restart4j = libs.restart4j.get()
val sqlite = libs.sqlite.get()
// 对 JNA 和 PTY4J 的本地库提取 // 对 JNA 和 PTY4J 的本地库提取
// 提取出来是为了单独签名,不然无法通过公证 // 提取出来是为了单独签名,不然无法通过公证
@@ -254,6 +250,22 @@ tasks.register<Copy>("copy-dependencies") {
)) { )) {
e.setExecutable(true) e.setExecutable(true)
} }
} else if ("${sqlite.name}-${sqlite.version}" == file.nameWithoutExtension) {
val targetDir = FileUtils.getFile(dylib, sqlite.name)
FileUtils.forceMkdir(targetDir)
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "org/sqlite/native/Mac/${archName}/*", "-d", targetDir.absolutePath) }
// @formatter:on
// 删除所有二进制类库
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/*") }
} else if ("${flatlaf.name}-${flatlaf.version}" == file.nameWithoutExtension) {
val targetDir = FileUtils.getFile(dylib, flatlaf.name)
FileUtils.forceMkdir(targetDir)
val isArm = arch.isArm
// @formatter:off
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "com/formdev/flatlaf/natives/*macos*${if (isArm) "arm" else "x86"}*", "-d", targetDir.absolutePath) }
// @formatter:on
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*") }
} }
} }
@@ -330,6 +342,48 @@ tasks.register<Copy>("copy-dependencies") {
exec { commandLine("zip", "-d", file.absolutePath, "linux/aarch64/*") } exec { commandLine("zip", "-d", file.absolutePath, "linux/aarch64/*") }
} }
} }
} else if ("${sqlite.name}-${sqlite.version}" == file.nameWithoutExtension) {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux-*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/FreeBSD/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Mac/*") }
if (os.isWindows) {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/armv7/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/x86/*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/x86_64/*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/aarch64/*") }
}
} else if (os.isLinux) {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/arm*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/ppc64/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/riscv64/*") }
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/x86/*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/x86_64/*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/aarch64/*") }
}
}
} else if ("${flatlaf.name}-${flatlaf.version}" == file.nameWithoutExtension) {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*macos*") }
if (os.isWindows) {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*linux*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*x86*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*x86.dll") }
}
} else if (os.isLinux) {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*windows*") }
if (arch.isArm) {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*x86*") }
} else {
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*arm*") }
}
}
} }
} }
} }
@@ -343,6 +397,7 @@ tasks.register<Exec>("jlink") {
"java.logging", "java.logging",
"java.management", "java.management",
"java.rmi", "java.rmi",
"java.sql",
"java.security.jgss", "java.security.jgss",
"jdk.crypto.ec", "jdk.crypto.ec",
"jdk.unsupported", "jdk.unsupported",
@@ -368,26 +423,23 @@ tasks.register<Exec>("jpackage") {
val buildDir = layout.buildDirectory.get() val buildDir = layout.buildDirectory.get()
val options = mutableListOf( val options = mutableListOf(
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED", "-Xmx2048m",
"-Xmx2g",
"-XX:+UseZGC",
"-XX:+ZUncommit",
"-XX:+ZGenerational",
"-XX:ZUncommitDelay=60",
"-XX:+HeapDumpOnOutOfMemoryError", "-XX:+HeapDumpOnOutOfMemoryError",
"-Dlogger.console.level=off", "-Dlogger.console.level=off",
"-Dkotlinx.coroutines.debug=off", "-Dkotlinx.coroutines.debug=off",
"-Dapp-version=${project.version}", "-Dapp-version=${project.version}",
"-Drelease-date=${DateFormatUtils.format(Date(), "yyyy-MM-dd")}",
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
) )
options.add("-Dsun.java2d.metal=true") options.add("-Dsun.java2d.metal=true")
if (os.isMacOsX) { if (os.isMacOsX) {
// NSWindow // NSWindow
options.add("-Dapple.awt.application.appearance=system")
options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED") options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
options.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED") options.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED") options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
options.add("-Dapple.awt.application.appearance=system")
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED") options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
options.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED") options.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
} }
@@ -399,7 +451,7 @@ tasks.register<Exec>("jpackage") {
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage") val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage")
arguments.addAll(listOf("--runtime-image", "${buildDir}/jlink")) arguments.addAll(listOf("--runtime-image", "${buildDir}/jlink"))
arguments.addAll(listOf("--name", project.name.uppercaseFirstChar())) arguments.addAll(listOf("--name", project.name.uppercaseFirstChar()))
arguments.addAll(listOf("--app-version", "${project.version}")) arguments.addAll(listOf("--app-version", appVersion.toString()))
arguments.addAll(listOf("--main-jar", tasks.jar.get().archiveFileName.get())) arguments.addAll(listOf("--main-jar", tasks.jar.get().archiveFileName.get()))
arguments.addAll(listOf("--main-class", application.mainClass.get())) arguments.addAll(listOf("--main-class", application.mainClass.get()))
arguments.addAll(listOf("--input", "$buildDir/libs")) arguments.addAll(listOf("--input", "$buildDir/libs"))
@@ -408,6 +460,7 @@ tasks.register<Exec>("jpackage") {
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE))) arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
arguments.addAll(listOf("--vendor", "TermoraDev")) arguments.addAll(listOf("--vendor", "TermoraDev"))
arguments.addAll(listOf("--copyright", "TermoraDev")) arguments.addAll(listOf("--copyright", "TermoraDev"))
arguments.addAll(listOf("--app-content", "$buildDir/plugins"))
if (os.isWindows) { if (os.isWindows) {
arguments.addAll( arguments.addAll(
@@ -470,20 +523,22 @@ tasks.register("dist") {
// 清空目录 // 清空目录
exec { commandLine(gradlew, "clean") } exec { commandLine(gradlew, "clean") }
// 构建自带的插件
exec { commandLine(gradlew, ":plugins:migration:build") }
// 打包并复制依赖 // 打包并复制依赖
exec { exec {
commandLine(gradlew, "jar", "copy-dependencies") commandLine(gradlew, ":jar", ":copy-dependencies")
environment("ENABLE_BUILD" to true)
} }
// 检查依赖的开源协议 // 检查依赖的开源协议
exec { commandLine(gradlew, "check-license") } exec { commandLine(gradlew, ":check-license") }
// jlink // jlink
exec { commandLine(gradlew, "jlink") } exec { commandLine(gradlew, ":jlink") }
// 打包 // 打包
exec { commandLine(gradlew, "jpackage") } exec { commandLine(gradlew, ":jpackage") }
// 根据不同的系统构建不同的二进制包 // 根据不同的系统构建不同的二进制包
pack() pack()
@@ -558,7 +613,7 @@ fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: Str
"iscc", "iscc",
"/DMyAppId=${projectName}", "/DMyAppId=${projectName}",
"/DMyAppName=${projectName}", "/DMyAppName=${projectName}",
"/DMyAppVersion=${project.version}", "/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")}",
"/DMySourceDir=${layout.buildDirectory.dir("jpackage/images/win-msi.image/${projectName}").get().asFile}", "/DMySourceDir=${layout.buildDirectory.dir("jpackage/images/win-msi.image/${projectName}").get().asFile}",
@@ -571,7 +626,7 @@ fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: Str
exec { exec {
commandLine( commandLine(
"cmd", "/c", "move", "cmd", "/c", "move",
"${projectName}-${project.version}.msi", "${projectName}-${appVersion}.msi",
"${finalFilenameWithoutExtension}.msi" "${finalFilenameWithoutExtension}.msi"
) )
workingDir = distributionDir.asFile workingDir = distributionDir.asFile
@@ -587,7 +642,7 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
// rename // rename
// @formatter:off // @formatter:off
exec { commandLine("mv", distributionDir.file("${projectName}-${project.version}.dmg").asFile.absolutePath, dmgFile.absolutePath,) } exec { commandLine("mv", distributionDir.file("${projectName}-${appVersion}.dmg").asFile.absolutePath, dmgFile.absolutePath,) }
// @formatter:on // @formatter:on
// sign dmg // sign dmg
@@ -769,6 +824,10 @@ kotlin {
} }
} }
java {
withSourcesJar()
}
idea { idea {
module { module {
isDownloadJavadoc = true isDownloadJavadoc = true

View File

@@ -4,7 +4,7 @@ slf4j = "2.0.17"
pty4j = "0.13.6" pty4j = "0.13.6"
tinylog = "2.7.0" tinylog = "2.7.0"
kotlinx-coroutines = "1.10.2" kotlinx-coroutines = "1.10.2"
flatlaf = "3.6" flatlaf = "3.7-SNAPSHOT"
kotlinx-serialization-json = "1.8.1" kotlinx-serialization-json = "1.8.1"
commons-codec = "1.18.0" commons-codec = "1.18.0"
commons-lang3 = "3.17.0" commons-lang3 = "3.17.0"
@@ -41,6 +41,13 @@ jSerialComm = "2.11.0"
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-1"
h2 = "2.3.232"
sqlite = "3.49.1.0"
jug = "5.1.0"
semver4j = "5.7.0"
jsvg = "2.0.0"
dom4j = "2.1.4"
[libraries] [libraries]
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
@@ -59,9 +66,11 @@ commons-vfs2 = { group = "org.apache.commons", name = "commons-vfs2", version.re
pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" } pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" }
ini4j = { module = "org.jetbrains.intellij.deps:ini4j", version.ref = "ini4j" } ini4j = { module = "org.jetbrains.intellij.deps:ini4j", version.ref = "ini4j" }
flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" } flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
flatlaf-extras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" } flatlafextras = { group = "com.formdev", name = "flatlaf-extras", 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" }
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" }
@@ -73,7 +82,6 @@ oshi-core = { module = "com.github.oshi:oshi-core", version.ref = "oshi" }
commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" } commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
restart4j = { module = "com.github.hstyi:restart4j", version.ref = "restart4j" } restart4j = { module = "com.github.hstyi:restart4j", version.ref = "restart4j" }
jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" } jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" }
flatlaf-swingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
hutool = { module = "cn.hutool:hutool-all", version.ref = "hutool" } hutool = { module = "cn.hutool:hutool-all", version.ref = "hutool" }
jsch = { module = "com.github.mwiede:jsch", version.ref = "jsch" } jsch = { module = "com.github.mwiede:jsch", version.ref = "jsch" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
@@ -95,6 +103,16 @@ colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker"
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" } mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "jSerialComm" } jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "jSerialComm" }
eddsa = { module = "net.i2p.crypto:eddsa", version.ref = "eddsa" } eddsa = { module = "net.i2p.crypto:eddsa", version.ref = "eddsa" }
exposed-core = { module = "org.jetbrains.exposed:exposed-core", 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-migration = { module = "org.jetbrains.exposed:exposed-migration", version.ref = "exposed" }
h2 = { module = "com.h2database:h2", version.ref = "h2" }
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
jug = { module = "com.fasterxml.uuid:java-uuid-generator", version.ref = "jug" }
jsvg = { module = "com.github.weisj:jsvg", version.ref = "jsvg" }
dom4j = { module = "org.dom4j:dom4j", version.ref = "dom4j" }
semver4j = { module = "org.semver4j:semver4j", version.ref = "semver4j" }
[plugins] [plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.10.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

10
plugins/LICENSE Normal file
View File

@@ -0,0 +1,10 @@
Copyright (c) 2025-present hstyi
The files in this catalogue are for public access only. Specific descriptions are given below:
- You may view and study the contents of these files;
- You may NOT use them for any commercial purpose;
- You may NOT modify, copy, distribute, republish, or use them to create derivative works;
- Written permission must be obtained from the author for any use beyond personal viewing.
All rights reserved.

75
plugins/THIRDPARTY Normal file
View File

@@ -0,0 +1,75 @@
minio
Apache License 2.0
https://github.com/minio/minio-java/blob/master/LICENSE
aliyun-sdk-oss
Apache License 2.0
https://www.apache.org/licenses/LICENSE-2.0.html
jaxb-api
BSD 3-Clause "New" or "Revised" License
https://github.com/jakartaee/jaxb-api/blob/master/LICENSE.md
activation
COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.1
https://github.com/javaee/activation/blob/master/LICENSE.txt
jaxb-runtime
BSD 3-Clause "New" or "Revised" License
https://github.com/eclipse-ee4j/jaxb-ri/blob/master/LICENSE.md
esdk-obs-java-bundle
HUAWEI LICENSE
https://github.com/huaweicloud/huaweicloud-sdk-java-obs/blob/master/LICENSE
xodus-compress
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
xodus-environment
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
xodus-openAPI
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
xodus-utils
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
xodus-vfs
Apache License 2.0
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
kotlin-bip39
MIT License
https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/LICENSE
commons-compress
Apache License 2.0
https://github.com/apache/commons-compress/blob/master/LICENSE.txt
cos_api
MIT License
https://github.com/tencentyun/cos-java-sdk-v5/blob/master/LICENSE
AutoComplete
BSD-3-Clause license
https://github.com/bobbylight/AutoComplete/blob/master/LICENSE.md
RSTALanguageSupport
BSD-3-Clause license
https://github.com/bobbylight/RSTALanguageSupport/blob/master/README.md
RSyntaxTextArea
BSD-3-Clause license
https://github.com/bobbylight/RSyntaxTextArea/blob/master/LICENSE.md
MaxMind GeoIP2 API
Apache License, Version 2.0
https://www.apache.org/licenses/LICENSE-2.0.html
GeoLite2 (https://www.maxmind.com)
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
https://creativecommons.org/licenses/by-sa/4.0/

View File

@@ -0,0 +1,16 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.2"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,21 @@
package app.termora.plugins.bg
import app.termora.EnableManager
import app.termora.database.DatabaseManager
object Appearance {
private val enableManager get() = EnableManager.getInstance()
private val appearance get() = DatabaseManager.getInstance().appearance
var backgroundImage: String
get() = enableManager.getFlag("Plugins.bg.backgroundImage", appearance.backgroundImage)
set(value) {
enableManager.setFlag("Plugins.bg.backgroundImage", value)
}
var interval: Int
get() = enableManager.getFlag("Plugins.bg.interval", 360)
set(value) {
enableManager.setFlag("Plugins.bg.interval", value)
}
}

View File

@@ -0,0 +1,29 @@
package app.termora.plugins.bg
import app.termora.GlassPaneExtension
import com.formdev.flatlaf.FlatLaf
import java.awt.AlphaComposite
import java.awt.Graphics2D
import javax.swing.JComponent
class BGGlassPaneExtension private constructor() : GlassPaneExtension {
companion object {
val instance = BGGlassPaneExtension()
}
override fun paint(
c: JComponent,
g2d: Graphics2D
): Boolean {
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return false
g2d.composite = AlphaComposite.getInstance(
AlphaComposite.SRC_OVER,
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)
return true
}
}

View File

@@ -0,0 +1,26 @@
package app.termora.plugins.bg
import app.termora.AbstractI18n
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.*
object BGI18n : AbstractI18n() {
private val log = LoggerFactory.getLogger(BGI18n::class.java)
private val myBundle by lazy {
val bundle = ResourceBundle.getBundle("i18n/messages", Locale.getDefault(), BGI18n::class.java.classLoader)
if (log.isInfoEnabled) {
log.info("I18n: {}", bundle.baseBundleName ?: "null")
}
return@lazy bundle
}
override fun getBundle(): ResourceBundle {
return myBundle
}
override fun getLogger(): Logger {
return log
}
}

View File

@@ -0,0 +1,36 @@
package app.termora.plugins.bg
import app.termora.ApplicationRunnerExtension
import app.termora.GlassPaneAwareExtension
import app.termora.GlassPaneExtension
import app.termora.SettingsOptionExtension
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.Plugin
class BGPlugin : Plugin {
private val support = ExtensionSupport()
init {
support.addExtension(GlassPaneExtension::class.java) { BGGlassPaneExtension.instance }
support.addExtension(SettingsOptionExtension::class.java) { BackgroundSettingsOptionExtension.instance }
support.addExtension(ApplicationRunnerExtension::class.java) { BackgroundManager.getInstance() }
support.addExtension(GlassPaneAwareExtension::class.java) { BackgroundManager.getInstance() }
}
override fun getAuthor(): String {
return "TermoraDev"
}
override fun getName(): String {
return "Customize Background"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,167 @@
package app.termora.plugins.bg
import app.termora.*
import app.termora.database.DatabaseManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import okhttp3.Request
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import java.awt.Window
import java.awt.image.BufferedImage
import java.io.File
import java.lang.ref.WeakReference
import javax.imageio.ImageIO
import javax.swing.JComponent
import javax.swing.JPopupMenu
import javax.swing.SwingUtilities
import kotlin.math.max
import kotlin.time.Duration.Companion.seconds
internal class BackgroundManager private constructor() : Disposable, GlassPaneAwareExtension,
ApplicationRunnerExtension {
companion object {
private val log = LoggerFactory.getLogger(BackgroundManager::class.java)
fun getInstance(): BackgroundManager {
return ApplicationScope.Companion.forApplicationScope()
.getOrCreate(BackgroundManager::class) { BackgroundManager() }
}
}
private var bufferedImage: BufferedImage? = null
private var imageFilepath = StringUtils.EMPTY
private val glassPanes = mutableListOf<WeakReference<JComponent>>()
fun setBackgroundImage(url: String) {
clearBackgroundImage()
Appearance.backgroundImage = url
refreshBackgroundImage()
}
fun getBackgroundImage(): BufferedImage? {
val bg = doGetBackgroundImage()
if (bg == null) {
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
return null
} else {
JPopupMenu.setDefaultLightWeightPopupEnabled(true)
}
} else {
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
JPopupMenu.setDefaultLightWeightPopupEnabled(false)
}
}
return bg
}
private fun doGetBackgroundImage(): BufferedImage? {
synchronized(this) {
return bufferedImage
}
}
fun clearBackgroundImage() {
synchronized(this) {
bufferedImage = null
imageFilepath = StringUtils.EMPTY
Appearance.backgroundImage = StringUtils.EMPTY
}
refreshGlassPanes()
}
private fun refreshBackgroundImage() {
val backgroundImage = Appearance.backgroundImage
if (backgroundImage.isBlank()) {
return
}
var file: File? = null
// 从网络下载
if (backgroundImage.startsWith("http://") || backgroundImage.startsWith("https://")) {
file = Application.httpClient.newCall(
Request.Builder().get()
.url(backgroundImage).build()
).execute().use { response ->
val tempFile = File(Application.getTemporaryDir(), randomUUID())
if (response.isSuccessful.not()) {
if (log.isErrorEnabled) {
log.error("Request {} failed with code {}", backgroundImage, response.code)
}
return
}
val body = response.body
if (body != null) {
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
}
IOUtils.closeQuietly(body)
return@use tempFile
}
}
val backgroundImageFile = File(backgroundImage)
if (backgroundImageFile.isDirectory) {
val files = FileUtils.listFiles(backgroundImageFile, arrayOf("png", "jpg", "jpeg"), false)
if (files.isNotEmpty()) {
for (i in 0 until files.size) {
file = files.randomOrNull()
if (file == null) break
if (file.absolutePath == imageFilepath) continue
}
} else {
synchronized(this) {
imageFilepath = StringUtils.EMPTY
bufferedImage = null
refreshGlassPanes()
}
}
} else if (backgroundImageFile.isFile) {
file = backgroundImageFile
}
if (file == null || imageFilepath == file.absolutePath) {
return
}
bufferedImage = file.inputStream().use { ImageIO.read(it) }
imageFilepath = file.absolutePath
refreshGlassPanes()
}
private fun refreshGlassPanes() {
SwingUtilities.invokeLater {
glassPanes.removeIf {
val glassPane = it.get()
glassPane?.repaint()
glassPane == null
}
}
}
override fun dispose() {
}
override fun setGlassPane(window: Window, glassPane: JComponent) {
glassPanes.add(WeakReference(glassPane))
}
override fun ready() {
swingCoroutineScope.launch(Dispatchers.IO) {
while (isActive) {
runCatching { refreshBackgroundImage() }.onFailure {
if (log.isErrorEnabled) {
log.error("Refresh failed", it)
}
}
delay(max(Appearance.interval, 30).seconds)
}
}
}
}

View File

@@ -0,0 +1,152 @@
package app.termora.plugins.bg
import app.termora.*
import app.termora.OptionsPane.Companion.FORM_MARGIN
import app.termora.database.DatabaseManager
import app.termora.nv.FileChooser
import com.formdev.flatlaf.extras.components.FlatButton
import com.formdev.flatlaf.extras.components.FlatTextPane
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.exception.ExceptionUtils
import org.slf4j.LoggerFactory
import java.awt.BorderLayout
import java.io.File
import java.nio.file.StandardCopyOption
import javax.swing.*
import javax.swing.event.DocumentEvent
class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
companion object {
private val log = LoggerFactory.getLogger(BackgroundOption::class.java)
}
private val owner get() = SwingUtilities.getWindowAncestor(this)
val backgroundImageTextField = OutlineTextField()
val intervalSpinner = NumberSpinner(360, minimum = 30, maximum = 86400)
private val backgroundButton = JButton(Icons.folder)
private val backgroundClearButton = FlatButton()
init {
initView()
initEvents()
}
private fun initView() {
backgroundImageTextField.isEditable = false
backgroundImageTextField.trailingComponent = backgroundButton
backgroundImageTextField.text = Appearance.backgroundImage
backgroundImageTextField.document.addDocumentListener(object : DocumentAdaptor() {
override fun changedUpdate(e: DocumentEvent) {
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
}
})
backgroundClearButton.isFocusable = false
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
backgroundClearButton.icon = Icons.delete
backgroundClearButton.buttonType = FlatButton.ButtonType.toolBarButton
intervalSpinner.value = Appearance.interval
add(getFormPanel(), BorderLayout.CENTER)
}
private fun initEvents() {
backgroundButton.addActionListener {
val chooser = FileChooser()
chooser.osxAllowedFileTypes = listOf("png", "jpg", "jpeg")
chooser.allowsMultiSelection = false
chooser.win32Filters.add(Pair("Image files", listOf("png", "jpg", "jpeg")))
chooser.fileSelectionMode = JFileChooser.FILES_AND_DIRECTORIES
chooser.showOpenDialog(owner).thenAccept {
if (it.isNotEmpty()) {
onSelectedBackgroundImage(it.first())
}
}
}
backgroundClearButton.addActionListener {
BackgroundManager.getInstance().clearBackgroundImage()
backgroundImageTextField.text = StringUtils.EMPTY
}
intervalSpinner.addChangeListener {
val value = intervalSpinner.value
if (value is Int) {
Appearance.interval = value
}
}
}
private fun onSelectedBackgroundImage(file: File) {
try {
if (file.isFile) {
val destFile = FileUtils.getFile(Application.getBaseDataDir(), "background", file.name)
FileUtils.forceMkdirParent(destFile)
FileUtils.deleteQuietly(destFile)
FileUtils.copyFile(file, destFile, StandardCopyOption.REPLACE_EXISTING)
BackgroundManager.getInstance().setBackgroundImage(destFile.absolutePath)
} else if (file.isDirectory) {
BackgroundManager.getInstance().setBackgroundImage(file.absolutePath)
}
backgroundImageTextField.text = file.absolutePath
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
SwingUtilities.invokeLater {
OptionPane.showMessageDialog(
owner,
ExceptionUtils.getRootCauseMessage(e),
messageType = JOptionPane.ERROR_MESSAGE
)
}
}
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.imageGray
}
override fun getTitle(): String {
return BGI18n.getString("termora.plugins.bg.background-image")
}
override fun getJComponent(): JComponent {
return this
}
private fun getFormPanel(): JPanel {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default",
"pref, $FORM_MARGIN, pref"
)
var rows = 1
val step = 2
val builder = FormBuilder.create().layout(layout)
val bgClearBox = Box.createHorizontalBox()
bgClearBox.add(backgroundClearButton)
builder.add("${BGI18n.getString("termora.plugins.bg.background-image")}:").xy(1, rows)
.add(backgroundImageTextField).xy(3, rows)
.add(bgClearBox).xy(5, rows)
.apply { rows += step }
builder.add("${BGI18n.getString("termora.plugins.bg.interval")}:").xy(1, rows)
.add(intervalSpinner).xy(3, rows)
.apply { rows += step }
return builder.build()
}
}

View File

@@ -0,0 +1,14 @@
package app.termora.plugins.bg
import app.termora.OptionsPane
import app.termora.SettingsOptionExtension
class BackgroundSettingsOptionExtension private constructor(): SettingsOptionExtension {
companion object {
val instance by lazy { BackgroundSettingsOptionExtension() }
}
override fun createSettingsOption(): OptionsPane.Option {
return BackgroundOption()
}
}

View File

@@ -0,0 +1,23 @@
<termora-plugin>
<id>bg</id>
<name>Customize Background</name>
<version>${projectVersion}</version>
<entry>app.termora.plugins.bg.BGPlugin</entry>
<termora-version since=">=${rootProjectVersion}" until=""/>
<descriptions>
<description>Customize application background</description>
<description language="zh_CN">自定义应用程序背景</description>
<description language="zh_TW">自訂應用程式背景</description>
</descriptions>
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
</termora-plugin>

View File

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2023 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">
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#3574F0"/>
<path d="M2.5 9.33566L4.1822 7.66899C4.56052 7.29415 5.16625 7.28159 5.55979 7.64043L11.9861 13.5" stroke="#3574F0"/>
<circle cx="10" cy="6" r="1.5" stroke="#3574F0"/>
</svg>

After

Width:  |  Height:  |  Size: 472 B

View File

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2023 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">
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#548AF7"/>
<path d="M2.5 9.33566L4.1822 7.66899C4.56052 7.29415 5.16625 7.28159 5.55979 7.64043L11.9861 13.5" stroke="#548AF7"/>
<circle cx="10" cy="6" r="1.5" stroke="#548AF7"/>
</svg>

After

Width:  |  Height:  |  Size: 472 B

View File

@@ -0,0 +1,2 @@
termora.plugins.bg.interval=Interval
termora.plugins.bg.background-image=Background Image

View File

@@ -0,0 +1,2 @@
termora.plugins.bg.background-image=背景图
termora.plugins.bg.interval=切换间隔

View File

@@ -0,0 +1,2 @@
termora.plugins.bg.background-image=背景圖
termora.plugins.bg.interval=切換間隔

89
plugins/common.gradle.kts Normal file
View File

@@ -0,0 +1,89 @@
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
tasks.withType<Jar> {
manifest {
attributes(
"Implementation-Title" to project.name,
"Implementation-Version" to project.version,
)
}
from("${rootProject.projectDir}/plugins/LICENSE") {
into("META-INF")
}
from("${rootProject.projectDir}/plugins/THIRDPARTY") {
into("META-INF")
}
// archiveBaseName.set("${project.name}-${rootProject.version}")
destinationDirectory.set(file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}"))
}
tasks.named<Copy>("processResources") {
filesMatching("META-INF/plugin.xml") {
expand(
"projectName" to project.name,
"projectVersion" to project.version,
"rootProjectVersion" to rootProject.version,
)
}
}
tasks.register<Copy>("copy-dependencies") {
from(configurations.getByName("runtimeClasspath").filterNot {
it.name.startsWith("kotlin-stdlib") || it.name.startsWith("annotations")
})
into("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}")
}
tasks.named("build") {
dependsOn("copy-dependencies")
}
tasks.register("run-plugin") {
dependsOn("build")
doLast {
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
val runtimeCompileOnly by configurations.creating { extendsFrom(configurations.getByName("compileOnly")) }
val mainClass = "app.termora.MainKt"
val executable = System.getProperty("java.home") + "/bin/java"
val classpath = (configurations.getByName("compileClasspath") + configurations.getByName("runtimeClasspath")
+ runtimeCompileOnly).joinToString(if (os.isWindows) ";" else ":")
val commands = mutableListOf<String>(executable)
commands.add("-Dapp-version=${rootProject.version}")
commands.add("--add-exports java.base/sun.nio.ch=ALL-UNNAMED")
if (os.isMacOsX) {
// NSWindow
commands.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
commands.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
commands.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
commands.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
commands.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
commands.add("-Dapple.awt.application.appearance=system")
}
commands.addAll(listOf("-cp", classpath, mainClass))
exec {
commandLine = commands
environment(
"TERMORA_PLUGIN_DIRECTORY" to file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/"),
"TERMORA_BASE_DATA_DIR" to "${layout.buildDirectory.get().asFile.absolutePath}/data",
)
}
}
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
tasks.named("clean") {
doLast {
file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}").deleteRecursively()
}
}

View File

@@ -0,0 +1,16 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.1"
dependencies {
testImplementation(kotlin("test"))
implementation("com.qcloud:cos_api:5.6.245")
compileOnly(project(":"))
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,41 @@
package app.termora.plugins.cos
import org.apache.commons.vfs2.Capability
import org.apache.commons.vfs2.FileName
import org.apache.commons.vfs2.FileSystem
import org.apache.commons.vfs2.FileSystemOptions
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
class COSFileProvider private constructor() : AbstractOriginatingFileProvider() {
companion object {
val instance by lazy { COSFileProvider() }
val capabilities = listOf(
Capability.CREATE,
Capability.DELETE,
Capability.RENAME,
Capability.GET_TYPE,
Capability.LIST_CHILDREN,
Capability.READ_CONTENT,
Capability.URI,
Capability.WRITE_CONTENT,
Capability.GET_LAST_MODIFIED,
Capability.SET_LAST_MODIFIED_FILE,
Capability.RANDOM_ACCESS_READ,
Capability.APPEND_CONTENT
)
}
override fun getCapabilities(): Collection<Capability> {
return COSFileProvider.capabilities
}
override fun doCreateFileSystem(
rootFileName: FileName,
fileSystemOptions: FileSystemOptions
): FileSystem? {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,36 @@
package app.termora.plugins.cos
import app.termora.DynamicIcon
import app.termora.I18n
import app.termora.Icons
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.PaidPlugin
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProviderExtension
class COSPlugin : PaidPlugin {
private val support = ExtensionSupport()
init {
support.addExtension(ProtocolProviderExtension::class.java) { COSProtocolProviderExtension.Companion.instance }
support.addExtension(ProtocolHostPanelExtension::class.java) { COSProtocolHostPanelExtension.Companion.instance }
}
override fun getAuthor(): String {
return "TermoraDev"
}
override fun getName(): String {
return "Tencent COS"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,22 @@
package app.termora.plugins.cos
import app.termora.Host
import app.termora.protocol.ProtocolHostPanel
import org.apache.commons.lang3.StringUtils
class COSProtocolHostPanel : ProtocolHostPanel() {
override fun getHost(): Host {
return Host(
name = StringUtils.EMPTY,
protocol = COSProtocolProvider.Companion.PROTOCOL
)
}
override fun setHost(host: Host) {
}
override fun validateFields(): Boolean {
return true
}
}

View File

@@ -0,0 +1,19 @@
package app.termora.plugins.cos
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
class COSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
companion object {
val instance by lazy { COSProtocolHostPanelExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return COSProtocolProvider.Companion.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
return COSProtocolHostPanel()
}
}

View File

@@ -0,0 +1,33 @@
package app.termora.plugins.cos
import app.termora.DynamicIcon
import app.termora.Icons
import app.termora.protocol.FileObjectHandler
import app.termora.protocol.FileObjectRequest
import app.termora.protocol.TransferProtocolProvider
import org.apache.commons.vfs2.provider.FileProvider
class COSProtocolProvider private constructor() : TransferProtocolProvider {
companion object {
val instance by lazy { COSProtocolProvider() }
const val PROTOCOL = "COS"
}
override fun getProtocol(): String {
return PROTOCOL
}
override fun getIcon(width: Int, height: Int): DynamicIcon {
return Icons.tencent
}
override fun getFileProvider(): FileProvider {
return COSFileProvider.instance
}
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,14 @@
package app.termora.plugins.cos
import app.termora.protocol.ProtocolProvider
import app.termora.protocol.ProtocolProviderExtension
class COSProtocolProviderExtension private constructor() : ProtocolProviderExtension {
companion object {
val instance by lazy { COSProtocolProviderExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return COSProtocolProvider.Companion.instance
}
}

View File

@@ -0,0 +1,25 @@
<termora-plugin>
<id>cos</id>
<name>Tencent COS</name>
<paid/>
<version>${projectVersion}</version>
<termora-version since=">=${rootProjectVersion}" until=""/>
<entry>app.termora.plugins.cos.COSPlugin</entry>
<descriptions>
<description>Connecting to Tencent COS</description>
<description language="zh_CN">支持连接到腾讯云对象存储</description>
<description language="zh_TW">支援連接到騰訊雲物件存儲</description>
</descriptions>
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
</termora-plugin>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1,19 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.3"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation("com.fifesoft:rsyntaxtextarea:3.6.0")
implementation("com.fifesoft:languagesupport:3.3.0")
implementation("com.fifesoft:autocomplete:3.3.2")
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,74 @@
package app.termora.plugins.editor
import app.termora.DialogWrapper
import app.termora.Disposable
import app.termora.Disposer
import app.termora.OptionPane
import app.termora.sftp.absolutePathString
import org.apache.commons.vfs2.FileObject
import java.awt.Dimension
import java.awt.Window
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.io.File
import javax.swing.JComponent
import javax.swing.JOptionPane
import javax.swing.UIManager
class EditorDialog(file: FileObject, owner: Window, myDisposable: Disposable) : DialogWrapper(null) {
private val filename = file.name.baseName
private val filepath = File(file.absolutePathString())
private val editorPanel = EditorPanel(this, filepath)
init {
Disposer.register(disposable, myDisposable)
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?) {
doCancelAction()
}
})
}
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
}
}

View File

@@ -0,0 +1,225 @@
package app.termora.plugins.editor
import app.termora.DocumentAdaptor
import app.termora.DynamicColor
import app.termora.Icons
import app.termora.database.DatabaseManager
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.components.FlatTextField
import com.formdev.flatlaf.extras.components.FlatToolBar
import org.apache.commons.io.FilenameUtils
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea
import org.fife.ui.rsyntaxtextarea.SyntaxConstants
import org.fife.ui.rsyntaxtextarea.Theme
import org.fife.ui.rtextarea.RTextScrollPane
import org.fife.ui.rtextarea.SearchContext
import org.fife.ui.rtextarea.SearchEngine
import java.awt.BorderLayout
import java.awt.Insets
import java.awt.event.ActionEvent
import java.awt.event.KeyEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.io.File
import javax.swing.*
import javax.swing.event.DocumentEvent
import kotlin.math.max
class EditorPanel(private val window: JDialog, private val file: File) : JPanel(BorderLayout()) {
private var text = file.readText(Charsets.UTF_8)
private val layeredPane = LayeredPane()
private val textArea = RSyntaxTextArea()
private val scrollPane = RTextScrollPane(textArea)
private val findPanel = FlatToolBar().apply { isFloatable = false }
private val searchTextField = FlatTextField()
private val closeFindPanelBtn = JButton(Icons.close)
private val nextBtn = JButton(Icons.down)
private val prevBtn = JButton(Icons.up)
private val context = SearchContext()
init {
initView()
initEvents()
}
private fun initView() {
textArea.font = textArea.font.deriveFont(DatabaseManager.getInstance().terminal.fontSize.toFloat())
textArea.text = text
textArea.antiAliasingEnabled = true
val theme = if (FlatLaf.isLafDark())
Theme.load(javaClass.getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/dark.xml"))
else
Theme.load(javaClass.getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/idea.xml"))
theme.apply(textArea)
val extension = FilenameUtils.getExtension(file.name)?.lowercase()
textArea.syntaxEditingStyle = when (extension) {
"java" -> SyntaxConstants.SYNTAX_STYLE_JAVA
"kt" -> SyntaxConstants.SYNTAX_STYLE_KOTLIN
"properties" -> SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE
"cpp", "c++" -> SyntaxConstants.SYNTAX_STYLE_CPLUSPLUS
"c" -> SyntaxConstants.SYNTAX_STYLE_C
"cs" -> SyntaxConstants.SYNTAX_STYLE_CSHARP
"css" -> SyntaxConstants.SYNTAX_STYLE_CSS
"html", "htm", "htmlx" -> SyntaxConstants.SYNTAX_STYLE_HTML
"js" -> SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT
"ts" -> SyntaxConstants.SYNTAX_STYLE_TYPESCRIPT
"xml", "svg" -> SyntaxConstants.SYNTAX_STYLE_XML
"yaml", "yml" -> SyntaxConstants.SYNTAX_STYLE_YAML
"sh", "shell" -> SyntaxConstants.SYNTAX_STYLE_UNIX_SHELL
"sql" -> SyntaxConstants.SYNTAX_STYLE_SQL
"bat" -> SyntaxConstants.SYNTAX_STYLE_WINDOWS_BATCH
"py" -> SyntaxConstants.SYNTAX_STYLE_PYTHON
"php" -> SyntaxConstants.SYNTAX_STYLE_PHP
"lua" -> SyntaxConstants.SYNTAX_STYLE_LUA
"less" -> SyntaxConstants.SYNTAX_STYLE_LESS
"jsp" -> SyntaxConstants.SYNTAX_STYLE_JSP
"json" -> SyntaxConstants.SYNTAX_STYLE_JSON
"ini" -> SyntaxConstants.SYNTAX_STYLE_INI
"hosts" -> SyntaxConstants.SYNTAX_STYLE_HOSTS
"go" -> SyntaxConstants.SYNTAX_STYLE_GO
"dtd" -> SyntaxConstants.SYNTAX_STYLE_DTD
"dart" -> SyntaxConstants.SYNTAX_STYLE_DART
"csv" -> SyntaxConstants.SYNTAX_STYLE_CSV
"md" -> SyntaxConstants.SYNTAX_STYLE_MARKDOWN
else -> SyntaxConstants.SYNTAX_STYLE_NONE
}
textArea.discardAllEdits()
scrollPane.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
findPanel.isVisible = false
findPanel.isOpaque = true
findPanel.background = DynamicColor("window")
searchTextField.background = findPanel.background
searchTextField.padding = Insets(0, 4, 0, 0)
searchTextField.border = BorderFactory.createEmptyBorder()
findPanel.add(searchTextField)
findPanel.add(prevBtn)
findPanel.add(nextBtn)
findPanel.add(closeFindPanelBtn)
findPanel.border = BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 1, 1, 0, DynamicColor.BorderColor),
BorderFactory.createEmptyBorder(2, 2, 2, 2)
)
layeredPane.add(findPanel, JLayeredPane.MODAL_LAYER as Any)
layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER as Any)
add(layeredPane, BorderLayout.CENTER)
}
private fun initEvents() {
window.addWindowListener(object : WindowAdapter() {
override fun windowOpened(e: WindowEvent?) {
scrollPane.verticalScrollBar.value = 0
window.removeWindowListener(this)
}
})
textArea.inputMap.put(
KeyStroke.getKeyStroke(KeyEvent.VK_S, toolkit.menuShortcutKeyMaskEx),
"Save"
)
textArea.inputMap.put(
KeyStroke.getKeyStroke(KeyEvent.VK_F, toolkit.menuShortcutKeyMaskEx),
"Find"
)
searchTextField.inputMap.put(
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
"Esc"
)
searchTextField.actionMap.put("Esc", object : AbstractAction("Esc") {
override fun actionPerformed(e: ActionEvent) {
textArea.clearMarkAllHighlights()
textArea.requestFocusInWindow()
findPanel.isVisible = false
}
})
closeFindPanelBtn.addActionListener { searchTextField.actionMap.get("Esc").actionPerformed(it) }
textArea.actionMap.put("Save", object : AbstractAction("Save") {
override fun actionPerformed(e: ActionEvent) {
file.writeText(textArea.text, Charsets.UTF_8)
text = textArea.text
window.title = file.name
}
})
textArea.actionMap.put("Find", object : AbstractAction("Find") {
override fun actionPerformed(e: ActionEvent) {
findPanel.isVisible = true
searchTextField.selectAll()
searchTextField.requestFocusInWindow()
}
})
textArea.document.addDocumentListener(object : DocumentAdaptor() {
override fun changedUpdate(e: DocumentEvent) {
window.title = if (textArea.text.hashCode() != text.hashCode()) {
"${file.name} *"
} else {
file.name
}
}
})
searchTextField.document.addDocumentListener(object : DocumentAdaptor() {
override fun changedUpdate(e: DocumentEvent) {
search()
}
})
searchTextField.addActionListener { nextBtn.doClick(0) }
prevBtn.addActionListener { search(false) }
nextBtn.addActionListener { search(true) }
}
private fun search(searchForward: Boolean = true) {
textArea.clearMarkAllHighlights()
val text: String = searchTextField.getText()
if (text.isEmpty()) return
context.searchFor = text
context.searchForward = searchForward
context.wholeWord = false
val result = SearchEngine.find(textArea, context)
prevBtn.isEnabled = result.markedCount > 0
nextBtn.isEnabled = result.markedCount > 0
}
fun changes() = text != textArea.text
private inner class LayeredPane : JLayeredPane() {
override fun doLayout() {
synchronized(treeLock) {
for (c in components) {
if (c == findPanel) {
val height = max(findPanel.preferredSize.height, findPanel.height)
val x = width / 2
c.setBounds(x, 1, width - x, height)
} else {
c.setBounds(0, 0, width, height)
}
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
package app.termora.plugins.editor
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.Plugin
import app.termora.sftp.SFTPEditFileExtension
class EditorPlugin : Plugin {
private val support = ExtensionSupport()
init {
support.addExtension(SFTPEditFileExtension::class.java) { MySFTPEditFileExtension.instance }
}
override fun getAuthor(): String {
return "TermoraDev"
}
override fun getName(): String {
return "SFTP File Editor"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,21 @@
package app.termora.plugins.editor
import app.termora.Disposable
import app.termora.Disposer
import app.termora.sftp.SFTPEditFileExtension
import app.termora.sftp.absolutePathString
import org.apache.commons.vfs2.FileObject
import java.awt.Window
import javax.swing.SwingUtilities
class MySFTPEditFileExtension private constructor() : SFTPEditFileExtension {
companion object {
val instance = MySFTPEditFileExtension()
}
override fun edit(owner: Window, file: FileObject): Disposable {
val disposable = Disposer.newDisposable()
SwingUtilities.invokeLater { EditorDialog(file, owner, disposable).isVisible = true }
return disposable
}
}

View File

@@ -0,0 +1,22 @@
<termora-plugin>
<id>editor</id>
<name>SFTP File Editor</name>
<version>${projectVersion}</version>
<termora-version since=">=${rootProjectVersion}" until=""/>
<entry>app.termora.plugins.editor.EditorPlugin</entry>
<descriptions>
<description>Edit SFTP files using the built-in editor</description>
<description language="zh_CN">使用内置编辑器编辑 SFTP 文件</description>
<description language="zh_TW">使用內建編輯器編輯 SFTP 文件</description>
</descriptions>
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
</termora-plugin>

View File

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2023 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="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V7.47774C14.2142 6.80872 13.0333 6.84543 12.2909 7.58786L7 12.8787V14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" />
<path d="M8.09379 5H13C13.5523 5 14 5.44772 14 6V7.02381C14.3594 7.07711 14.7072 7.22842 15 7.47774V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H7V13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823L8.09379 5Z" fill="#6C707E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4122 8.29497C14.0217 7.90444 13.3885 7.90444 12.998 8.29497L11.6466 9.64633L8 13.2929V16H10.7071L15.7051 11.0021C16.0956 10.6116 16.0956 9.97839 15.7051 9.58786L14.4122 8.29497ZM14 11.2929L14.998 10.295L13.7051 9.00208L12.7071 10L14 11.2929ZM12 10.7072L13.2929 12L10.2929 15H9V13.7072L12 10.7072Z" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2023 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="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V7.47774C14.2142 6.80872 13.0333 6.84543 12.2909 7.58786L7 12.8787V14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" />
<path d="M8.09379 5H13C13.5523 5 14 5.44772 14 6V7.02381C14.3594 7.07711 14.7072 7.22842 15 7.47774V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H7V13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823L8.09379 5Z" fill="#CED0D6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4122 8.29497C14.0217 7.90444 13.3885 7.90444 12.998 8.29497L11.6466 9.64633L8 13.2929V16H10.7071L15.7051 11.0021C16.0956 10.6116 16.0956 9.97839 15.7051 9.58786L14.4122 8.29497ZM14 11.2929L14.998 10.295L13.7051 9.00208L12.7071 10L14 11.2929ZM12 10.7072L13.2929 12L10.2929 15H9V13.7072L12 10.7072Z" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,107 @@
package app.termora.plugins.editor;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import org.fife.ui.rtextarea.*;
import org.fife.ui.rsyntaxtextarea.*;
/**
* A simple example showing how to do search and replace in a RSyntaxTextArea.
* The toolbar isn't very user-friendly, but this is just to show you how to use
* the API.<p>
*
* This example uses RSyntaxTextArea 2.5.6.
*/
public class FindAndReplaceDemo extends JFrame implements ActionListener {
private static final long serialVersionUID = 1L;
private RSyntaxTextArea textArea;
private JTextField searchField;
private JCheckBox regexCB;
private JCheckBox matchCaseCB;
public FindAndReplaceDemo() {
JPanel cp = new JPanel(new BorderLayout());
textArea = new RSyntaxTextArea(20, 60);
textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
textArea.setCodeFoldingEnabled(true);
RTextScrollPane sp = new RTextScrollPane(textArea);
cp.add(sp);
// Create a toolbar with searching options.
JToolBar toolBar = new JToolBar();
searchField = new JTextField(30);
toolBar.add(searchField);
final JButton nextButton = new JButton("Find Next");
nextButton.setActionCommand("FindNext");
nextButton.addActionListener(this);
toolBar.add(nextButton);
searchField.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
nextButton.doClick(0);
}
});
JButton prevButton = new JButton("Find Previous");
prevButton.setActionCommand("FindPrev");
prevButton.addActionListener(this);
toolBar.add(prevButton);
regexCB = new JCheckBox("Regex");
toolBar.add(regexCB);
matchCaseCB = new JCheckBox("Match Case");
toolBar.add(matchCaseCB);
cp.add(toolBar, BorderLayout.NORTH);
setContentPane(cp);
setTitle("Find and Replace Demo");
setDefaultCloseOperation(EXIT_ON_CLOSE);
pack();
setLocationRelativeTo(null);
}
public void actionPerformed(ActionEvent e) {
// "FindNext" => search forward, "FindPrev" => search backward
String command = e.getActionCommand();
boolean forward = "FindNext".equals(command);
// Create an object defining our search parameters.
SearchContext context = new SearchContext();
String text = searchField.getText();
if (text.length() == 0) {
return;
}
context.setSearchFor(text);
context.setMatchCase(matchCaseCB.isSelected());
context.setRegularExpression(regexCB.isSelected());
context.setSearchForward(forward);
context.setWholeWord(false);
boolean found = SearchEngine.find(textArea, context).wasFound();
if (!found) {
JOptionPane.showMessageDialog(this, "Text not found");
}
}
public static void main(String[] args) {
// Start all Swing applications on the EDT.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
String laf = UIManager.getSystemLookAndFeelClassName();
UIManager.setLookAndFeel(laf);
} catch (Exception e) { /* never happens */ }
FindAndReplaceDemo demo = new FindAndReplaceDemo();
demo.setVisible(true);
demo.textArea.requestFocusInWindow();
}
});
}
}

View File

@@ -0,0 +1,15 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.1"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,41 @@
package app.termora.plugins.ftp
import org.apache.commons.vfs2.Capability
import org.apache.commons.vfs2.FileName
import org.apache.commons.vfs2.FileSystem
import org.apache.commons.vfs2.FileSystemOptions
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
class FTPFileProvider private constructor() : AbstractOriginatingFileProvider() {
companion object {
val instance by lazy { FTPFileProvider() }
val capabilities = listOf(
Capability.CREATE,
Capability.DELETE,
Capability.RENAME,
Capability.GET_TYPE,
Capability.LIST_CHILDREN,
Capability.READ_CONTENT,
Capability.URI,
Capability.WRITE_CONTENT,
Capability.GET_LAST_MODIFIED,
Capability.SET_LAST_MODIFIED_FILE,
Capability.RANDOM_ACCESS_READ,
Capability.APPEND_CONTENT
)
}
override fun getCapabilities(): Collection<Capability> {
return FTPFileProvider.capabilities
}
override fun doCreateFileSystem(
rootFileName: FileName,
fileSystemOptions: FileSystemOptions
): FileSystem? {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,35 @@
package app.termora.plugins.ftp
import app.termora.DynamicIcon
import app.termora.I18n
import app.termora.Icons
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.PaidPlugin
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProviderExtension
class FTPPlugin : PaidPlugin {
private val support = ExtensionSupport()
init {
support.addExtension(ProtocolProviderExtension::class.java) { FTPProtocolProviderExtension.Companion.instance }
support.addExtension(ProtocolHostPanelExtension::class.java) { FTPProtocolHostPanelExtension.Companion.instance }
}
override fun getAuthor(): String {
return "TermoraDev"
}
override fun getName(): String {
return "FTP"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,22 @@
package app.termora.plugins.ftp
import app.termora.Host
import app.termora.protocol.ProtocolHostPanel
import org.apache.commons.lang3.StringUtils
class FTPProtocolHostPanel : ProtocolHostPanel() {
override fun getHost(): Host {
return Host(
name = StringUtils.EMPTY,
protocol = FTPProtocolProvider.PROTOCOL
)
}
override fun setHost(host: Host) {
}
override fun validateFields(): Boolean {
return true
}
}

View File

@@ -0,0 +1,19 @@
package app.termora.plugins.ftp
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
class FTPProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
companion object {
val instance by lazy { FTPProtocolHostPanelExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return FTPProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
return FTPProtocolHostPanel()
}
}

View File

@@ -0,0 +1,33 @@
package app.termora.plugins.ftp
import app.termora.DynamicIcon
import app.termora.Icons
import app.termora.protocol.FileObjectHandler
import app.termora.protocol.FileObjectRequest
import app.termora.protocol.TransferProtocolProvider
import org.apache.commons.vfs2.provider.FileProvider
class FTPProtocolProvider private constructor() : TransferProtocolProvider {
companion object {
val instance by lazy { FTPProtocolProvider() }
const val PROTOCOL = "FTP"
}
override fun getProtocol(): String {
return PROTOCOL
}
override fun getIcon(width: Int, height: Int): DynamicIcon {
return Icons.ftp
}
override fun getFileProvider(): FileProvider {
return FTPFileProvider.instance
}
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,14 @@
package app.termora.plugins.ftp
import app.termora.protocol.ProtocolProvider
import app.termora.protocol.ProtocolProviderExtension
class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension {
companion object {
val instance by lazy { FTPProtocolProviderExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return FTPProtocolProvider.Companion.instance
}
}

View File

@@ -0,0 +1,24 @@
<termora-plugin>
<id>ftp</id>
<name>FTP</name>
<paid/>
<version>${projectVersion}</version>
<termora-version since=">=${rootProjectVersion}" until=""/>
<entry>app.termora.plugins.ftp.FTPPlugin</entry>
<descriptions>
<description>Connecting to FTP</description>
<description language="zh_CN">支持连接到到 FTP</description>
<description language="zh_TW">支援連接到 FTP</description>
</descriptions>
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
</termora-plugin>

View File

@@ -0,0 +1 @@
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#6C707E"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#6C707E"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#CED0D6"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#CED0D6"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,14 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.1"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation("com.maxmind.geoip2:geoip2:4.3.1")
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,97 @@
package app.termora.plugins.geo
import app.termora.Application
import app.termora.ApplicationScope
import app.termora.Disposable
import com.maxmind.db.CHMCache
import com.maxmind.geoip2.DatabaseReader
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
import java.io.File
import java.net.InetAddress
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.jvm.optionals.getOrNull
internal class Geo private constructor() : Disposable {
companion object {
private val log = LoggerFactory.getLogger(Geo::class.java)
fun getInstance(): Geo {
return ApplicationScope.forApplicationScope()
.getOrCreate(Geo::class) { Geo() }
}
}
private val initialized = AtomicBoolean(false)
private var reader: DatabaseReader? = null
private fun initialize() {
if (GeoApplicationRunnerExtension.instance.isReady().not()) return
if (isInitialized()) return
if (initialized.compareAndSet(false, true)) {
try {
val database = getDatabaseFile()
if ((database.exists() && database.isFile).not()) {
throw IllegalStateException("${database.absolutePath} not be found")
}
val locale = Locale.getDefault().toString().replace("_", "-")
try {
reader = DatabaseReader.Builder(database)
.locales(listOf(locale, "en"))
.withCache(CHMCache()).build()
} catch (e: Exception) {
// 打开数据失败一般都是数据文件顺坏,删除数据库
FileUtils.deleteQuietly(database)
// 重新下载
GeoApplicationRunnerExtension.instance.reload()
throw e
}
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error("Failed to initialize geo database", e)
}
initialized.set(false)
}
}
}
fun getDatabaseFile(): File {
val dir = FileUtils.getFile(Application.getBaseDataDir(), "config", "plugins", "geo")
return File(dir, "GeoLite2-Country.mmdb")
}
fun country(ip: String): Country? {
try {
initialize()
val reader = reader ?: return null
val response = reader.tryCountry(InetAddress.getByName(ip)).getOrNull() ?: return null
val isoCode = response.country.isoCode
var name = response.country.name
// 控制名称不要太长如果太长则使用缩写。例如United States
if (name != null && name.length > 6) name = isoCode
return Country(isoCode, name ?: isoCode)
} catch (e: Exception) {
if (log.isDebugEnabled) {
log.error("Failed to initialize geo database", e)
}
return null
}
}
fun isInitialized(): Boolean = initialized.get()
override fun dispose() {
IOUtils.closeQuietly(reader)
}
data class Country(val isoCode: String, val name: String)
}

View File

@@ -0,0 +1,108 @@
package app.termora.plugins.geo
import app.termora.Application
import app.termora.ApplicationRunnerExtension
import app.termora.randomUUID
import app.termora.swingCoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext
import okhttp3.Request
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
import java.io.File
import java.net.ProxySelector
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.seconds
class GeoApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
companion object {
private val log = LoggerFactory.getLogger(GeoApplicationRunnerExtension::class.java)
val instance = GeoApplicationRunnerExtension()
}
private var ready = false
private val httpClient by lazy {
Application.httpClient.newBuilder()
.callTimeout(15, TimeUnit.MINUTES)
.readTimeout(10, TimeUnit.MINUTES)
.proxySelector(ProxySelector.getDefault())
.build()
}
override fun ready() {
val databaseFile = Geo.getInstance().getDatabaseFile()
if (databaseFile.exists()) {
ready = true
return
}
// 重新加载
reload()
}
fun isReady() = ready
internal fun reload() {
ready = false
val databaseFile = Geo.getInstance().getDatabaseFile()
swingCoroutineScope.launch(Dispatchers.IO) {
var timeout = 3
while (ready.not()) {
try {
FileUtils.forceMkdirParent(databaseFile)
downloadGeoLite2(databaseFile)
withContext(Dispatchers.Swing) { GeoHostTreeShowMoreEnableExtension.instance.updateComponentTreeUI() }
} catch (e: Exception) {
if (log.isWarnEnabled) {
log.warn(e.message, e)
}
}
delay(timeout.seconds)
timeout = timeout * 2
}
}
}
private fun downloadGeoLite2(dbFile: File) {
val url = "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb"
val response = httpClient.newCall(
Request.Builder().get().url(url)
.build()
).execute()
log.info("Fetched GeoLite2-Country.mmdb from {} status {}", url, response.code)
if (response.isSuccessful.not()) {
IOUtils.closeQuietly(response)
throw IllegalStateException("GeoLite2-Country.mmdb could not be downloaded, HTTP ${response.code}")
}
val body = response.body
val input = body?.byteStream()
val file = FileUtils.getFile(Application.getTemporaryDir(), randomUUID())
val output = file.outputStream()
val downloaded = runCatching { IOUtils.copy(input, output) }.isSuccess
IOUtils.closeQuietly(input, output, body, response)
log.info("Downloaded GeoLite2-Country.mmdb from {} , result: {}", url, downloaded)
if (downloaded) {
FileUtils.moveFile(file, dbFile)
ready = true
} else {
throw IllegalStateException("GeoLite2-Country.mmdb could not be downloaded")
}
}
}

View File

@@ -0,0 +1,47 @@
package app.termora.plugins.geo
import app.termora.EnableManager
import app.termora.FrameExtension
import app.termora.OptionPane
import app.termora.TermoraFrame
import java.awt.Window
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.swing.JOptionPane
import javax.swing.SwingUtilities
class GeoFrameExtension private constructor() : FrameExtension {
companion object {
val instance = GeoFrameExtension()
private const val FIRST_KEY = "Plugins.Geo.isFirst"
}
private val enableManager get() = EnableManager.getInstance()
override fun customize(frame: TermoraFrame) {
// 已经加载完毕,那么不需要提示
if (GeoApplicationRunnerExtension.instance.isReady()) return
// 已经提示过了,直接退出
val isFirst = enableManager.getFlag(FIRST_KEY, true)
if (isFirst.not()) return
frame.addWindowListener(object : WindowAdapter() {
override fun windowOpened(e: WindowEvent) {
enableManager.setFlag(FIRST_KEY, false)
frame.removeWindowListener(this)
SwingUtilities.invokeLater { showMessageDialog(frame) }
}
})
}
private fun showMessageDialog(window: Window) {
OptionPane.showMessageDialog(
window,
GeoI18n.getString("termora.plugins.geo.first-message"),
messageType = JOptionPane.INFORMATION_MESSAGE
)
}
}

View File

@@ -0,0 +1,48 @@
package app.termora.plugins.geo
import app.termora.EnableManager
import app.termora.I18n
import app.termora.SwingUtils
import app.termora.TermoraFrameManager
import app.termora.tree.HostTreeShowMoreEnableExtension
import app.termora.tree.NewHostTree
import javax.swing.JCheckBoxMenuItem
import javax.swing.JTree
import javax.swing.SwingUtilities
internal class GeoHostTreeShowMoreEnableExtension private constructor() : HostTreeShowMoreEnableExtension {
companion object {
private const val KEY = "Plugins.Geo.ShowMore.Enable"
val instance = GeoHostTreeShowMoreEnableExtension()
}
private val enableManager get() = EnableManager.getInstance()
override fun createJCheckBoxMenuItem(tree: JTree): JCheckBoxMenuItem {
val item = JCheckBoxMenuItem("Geo")
item.isEnabled = GeoApplicationRunnerExtension.instance.isReady()
item.isSelected = item.isEnabled && enableManager.getFlag(KEY, true)
if (item.isEnabled.not()) {
item.text = GeoI18n.getString("termora.plugins.geo.coming-soon")
}
item.addActionListener {
enableManager.setFlag(KEY, item.isSelected)
updateComponentTreeUI()
}
return item
}
fun updateComponentTreeUI() {
// reload all tree
for (frame in TermoraFrameManager.getInstance().getWindows()) {
for (tree in SwingUtils.getDescendantsOfClass(NewHostTree::class.java, frame)) {
SwingUtilities.updateComponentTreeUI(tree)
}
}
}
fun isShowMore(): Boolean {
return enableManager.getFlag(KEY, true)
}
}

View File

@@ -0,0 +1,26 @@
package app.termora.plugins.geo
import app.termora.AbstractI18n
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.*
object GeoI18n : AbstractI18n() {
private val log = LoggerFactory.getLogger(GeoI18n::class.java)
private val myBundle by lazy {
val bundle = ResourceBundle.getBundle("i18n/messages", Locale.getDefault(), GeoI18n::class.java.classLoader)
if (log.isInfoEnabled) {
log.info("I18n: {}", bundle.baseBundleName ?: "null")
}
return@lazy bundle
}
override fun getBundle(): ResourceBundle {
return myBundle
}
override fun getLogger(): Logger {
return log
}
}

View File

@@ -0,0 +1,37 @@
package app.termora.plugins.geo
import app.termora.ApplicationRunnerExtension
import app.termora.FrameExtension
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.Plugin
import app.termora.tree.HostTreeShowMoreEnableExtension
import app.termora.tree.SimpleTreeCellRendererExtension
class GeoPlugin : Plugin {
private val support = ExtensionSupport()
init {
support.addExtension(ApplicationRunnerExtension::class.java) { GeoApplicationRunnerExtension.instance }
support.addExtension(SimpleTreeCellRendererExtension::class.java) { GeoSimpleTreeCellRendererExtension.instance }
support.addExtension(HostTreeShowMoreEnableExtension::class.java) { GeoHostTreeShowMoreEnableExtension.instance }
support.addExtension(FrameExtension::class.java) { GeoFrameExtension.instance }
}
override fun getAuthor(): String {
return "TermoraDev"
}
override fun getName(): String {
return "Geo"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,58 @@
package app.termora.plugins.geo
import app.termora.ColorHash
import app.termora.tree.HostTreeNode
import app.termora.tree.MarkerSimpleTreeCellAnnotation
import app.termora.tree.SimpleTreeCellAnnotation
import app.termora.tree.SimpleTreeCellRendererExtension
import java.awt.Color
import javax.swing.JTree
class GeoSimpleTreeCellRendererExtension private constructor() : SimpleTreeCellRendererExtension {
companion object {
val instance = GeoSimpleTreeCellRendererExtension()
}
private val geo get() = Geo.getInstance()
override fun createAnnotations(
tree: JTree,
value: Any?,
sel: Boolean,
expanded: Boolean,
leaf: Boolean,
row: Int,
hasFocus: Boolean
): List<SimpleTreeCellAnnotation> {
val node = value as HostTreeNode? ?: return emptyList()
if (node.isFolder) return emptyList()
val protocol = node.data.protocol
if ((protocol == "SSH" || protocol == "RDP").not()) return emptyList()
if (GeoHostTreeShowMoreEnableExtension.instance.isShowMore().not()) return emptyList()
val country = geo.country(node.data.host) ?: return emptyList()
val text = "${countryCodeToFlagEmoji(country.isoCode)}${country.name}"
return listOf(
MarkerSimpleTreeCellAnnotation(
text,
foreground = Color.white,
background = ColorHash.hash(country.isoCode),
)
)
}
private fun countryCodeToFlagEmoji(code: String): String {
if (code.length < 2) return ""
val upper = code.take(2).uppercase()
val first = Character.codePointAt(upper, 0) - 'A'.code + 0x1F1E6
val second = Character.codePointAt(upper, 1) - 'A'.code + 0x1F1E6
return String(Character.toChars(first)) + String(Character.toChars(second))
}
override fun ordered(): Long {
return 1
}
}

View File

@@ -0,0 +1,23 @@
<termora-plugin>
<id>geo</id>
<name>Geo</name>
<version>${projectVersion}</version>
<entry>app.termora.plugins.geo.GeoPlugin</entry>
<termora-version since=">=${rootProjectVersion}" until=""/>
<descriptions>
<description>Display the geographical location of the host</description>
<description language="zh_CN">显示主机的地理位置</description>
<description language="zh_TW">顯示主機的地理位置</description>
</descriptions>
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
</termora-plugin>

View File

@@ -0,0 +1,5 @@
<!-- Copyright 2000-2023 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">
<circle cx="8" cy="8" r="6.5" stroke="#6C707E"/>
<path d="M10.5 8C10.5 9.38071 9.38071 10.5 8 10.5C6.61929 10.5 5.5 9.38071 5.5 8C5.5 6.61929 6.61929 5.5 8 5.5C9.38071 5.5 10.5 6.61929 10.5 8Z" stroke="#3574F0"/>
</svg>

After

Width:  |  Height:  |  Size: 453 B

View File

@@ -0,0 +1,5 @@
<!-- Copyright 2000-2023 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">
<circle cx="8" cy="8" r="6.5" stroke="#CED0D6"/>
<path d="M10.5 8C10.5 9.38071 9.38071 10.5 8 10.5C6.61929 10.5 5.5 9.38071 5.5 8C5.5 6.61929 6.61929 5.5 8 5.5C9.38071 5.5 10.5 6.61929 10.5 8Z" stroke="#548AF7"/>
</svg>

After

Width:  |  Height:  |  Size: 453 B

View File

@@ -0,0 +1,2 @@
termora.plugins.geo.first-message=The first time you use the <b>Geo</b> plugin, it will download the <b>GeoLite2.mmdb</b> database. <br/>Once the download is complete, it will display the host region information.
termora.plugins.geo.coming-soon=Geo loading

View File

@@ -0,0 +1,2 @@
termora.plugins.geo.first-message=首次使用 <b>Geo</b> 插件会下载 <b>GeoLite2.mmdb</b> 数据库,下载完成后会显示主机地域信息
termora.plugins.geo.coming-soon=Geo 加载中

View File

@@ -0,0 +1,2 @@
termora.plugins.geo.first-message=首次使用 <b>Geo</b> 外掛程式會下載 <b>GeoLite2.mmdb</b> 資料庫,下載完成後會顯示主機地域訊息
termora.plugins.geo.coming-soon=Geo 加载中

View File

@@ -0,0 +1,22 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.2"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation(libs.xodus.vfs)
implementation(libs.xodus.openAPI)
implementation(libs.xodus.environment)
implementation(libs.bip39)
implementation(libs.commons.compress)
}
ext.set("Termora-Plugin-Entry", "app.termora.plugins.migration.MigrationPlugin")
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -1,13 +1,12 @@
package app.termora package app.termora.plugins.migration
import app.termora.*
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.highlight.KeywordHighlight import app.termora.highlight.KeywordHighlight
import app.termora.keymap.Keymap import app.termora.keymap.Keymap
import app.termora.keymgr.OhKeyPair import app.termora.keymgr.OhKeyPair
import app.termora.macro.Macro import app.termora.macro.Macro
import app.termora.snippet.Snippet import app.termora.snippet.Snippet
import app.termora.sync.SyncManager
import app.termora.sync.SyncType
import app.termora.terminal.CursorStyle import app.termora.terminal.CursorStyle
import jetbrains.exodus.bindings.StringBinding import jetbrains.exodus.bindings.StringBinding
import jetbrains.exodus.env.* import jetbrains.exodus.env.*
@@ -47,7 +46,7 @@ class Database private constructor(private val env: Environment) : Disposable {
fun getDatabase(): Database { fun getDatabase(): Database {
return ApplicationScope.forApplicationScope() return ApplicationScope.forApplicationScope()
.getOrCreate(Database::class) { open(Application.getDatabaseFile()) } .getOrCreate(Database::class) { open(MigrationApplicationRunnerExtension.instance.getDatabaseFile()) }
} }
} }
@@ -289,18 +288,6 @@ class Database private constructor(private val env: Environment) : Disposable {
val k = StringBinding.stringToEntry(key) val k = StringBinding.stringToEntry(key)
val v = StringBinding.stringToEntry(value) val v = StringBinding.stringToEntry(value)
store.put(tx, k, v) store.put(tx, k, v)
// 数据变动时触发一次同步
if (name == HOST_STORE ||
name == KEYMAP_STORE ||
name == SNIPPET_STORE ||
name == KEYWORD_HIGHLIGHT_STORE ||
name == MACRO_STORE ||
name == KEY_PAIR_STORE ||
name == DELETED_DATA_STORE
) {
SyncManager.getInstance().triggerOnChanged()
}
} }
private fun delete(tx: Transaction, name: String, key: String) { private fun delete(tx: Transaction, name: String, key: String) {
@@ -356,7 +343,7 @@ class Database private constructor(private val env: Environment) : Disposable {
} }
abstract inner class Property(private val name: String) { abstract inner class Property(val name: String) {
private val properties = Collections.synchronizedMap(mutableMapOf<String, String>()) private val properties = Collections.synchronizedMap(mutableMapOf<String, String>())
init { init {

View File

@@ -1,5 +1,6 @@
package app.termora package app.termora.plugins.migration
import app.termora.*
import app.termora.AES.decodeBase64 import app.termora.AES.decodeBase64
import app.termora.actions.AnAction import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
@@ -86,7 +87,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
.layout( .layout(
FormLayout( FormLayout(
"$formMargin, default:grow, 4dlu, pref, $formMargin", "$formMargin, default:grow, 4dlu, pref, $formMargin",
"${if (SystemInfo.isWindows) "20dlu" else "0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin" "${"0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
) )
) )
.add(icon).xyw(2, rows, 4).apply { rows += step } .add(icon).xyw(2, rows, 4).apply { rows += step }
@@ -114,7 +115,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
I18n.getString("termora.doorman.delete-data"), I18n.getString("termora.doorman.delete-data"),
messageType = JOptionPane.WARNING_MESSAGE messageType = JOptionPane.WARNING_MESSAGE
) )
Application.browse(Application.getDatabaseFile().toURI()) Application.browse(MigrationApplicationRunnerExtension.instance.getDatabaseFile().toURI())
} }
} }
}).apply { isFocusable = false }).xyw(2, rows, 4, "center, fill") }).apply { isFocusable = false }).xyw(2, rows, 4, "center, fill")
@@ -136,6 +137,9 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
val key = AES.ECB.decrypt(entropy, keyBackup.decodeBase64()) val key = AES.ECB.decrypt(entropy, keyBackup.decodeBase64())
Doorman.getInstance().work(key) Doorman.getInstance().work(key)
} catch (e: Exception) { } catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
OptionPane.showMessageDialog( OptionPane.showMessageDialog(
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"), this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
messageType = JOptionPane.ERROR_MESSAGE messageType = JOptionPane.ERROR_MESSAGE
@@ -219,7 +223,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
) )
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin") val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
.layout(layout).debug(true) .layout(layout).debug(false)
val iterator = textFields.iterator() val iterator = textFields.iterator()
for (i in 1..5 step 2) { for (i in 1..5 step 2) {
for (j in 1..7 step 2) { for (j in 1..7 step 2) {

View File

@@ -0,0 +1,198 @@
package app.termora.plugins.migration
import app.termora.*
import app.termora.account.AccountManager
import app.termora.account.AccountOwner
import app.termora.database.DatabaseManager
import app.termora.database.OwnerType
import app.termora.highlight.KeywordHighlightManager
import app.termora.keymap.KeymapManager
import app.termora.keymgr.KeyManager
import app.termora.macro.MacroManager
import app.termora.snippet.SnippetManager
import org.apache.commons.io.FileUtils
import org.slf4j.LoggerFactory
import java.io.File
import java.util.concurrent.CountDownLatch
import javax.swing.JOptionPane
import javax.swing.SwingUtilities
import kotlin.system.exitProcess
class MigrationApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
companion object {
private val log = LoggerFactory.getLogger(MigrationApplicationRunnerExtension::class.java)
val instance by lazy { MigrationApplicationRunnerExtension() }
}
override fun ready() {
val file = getDatabaseFile()
if (file.exists().not()) return
// 如果数据库文件存在,那么需要迁移文件
val countDownLatch = CountDownLatch(1)
SwingUtilities.invokeAndWait {
try {
// 打开数据
openDatabase()
// 尝试解锁
openDoor()
// 询问是否迁移
if (askMigrate()) {
// 迁移
migrate()
// 移动到旧的目录
moveOldDirectory()
// 重启
restart()
}
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
} finally {
countDownLatch.countDown()
}
}
countDownLatch.await()
}
private fun openDoor() {
if (Doorman.getInstance().isWorking()) {
if (DoormanDialog(null).open().not()) {
Disposer.dispose(TermoraFrameManager.getInstance())
}
}
}
private fun openDatabase() {
try {
// 初始化数据库
Database.getDatabase()
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
JOptionPane.showMessageDialog(
null, "Unable to open database",
I18n.getString("termora.title"), JOptionPane.ERROR_MESSAGE
)
exitProcess(1)
}
}
private fun migrate() {
val database = Database.getDatabase()
val accountManager = AccountManager.getInstance()
val databaseManager = DatabaseManager.getInstance()
val ownerId = accountManager.getAccountId()
val hostManager = HostManager.getInstance()
val snippetManager = SnippetManager.getInstance()
val macroManager = MacroManager.getInstance()
val keymapManager = KeymapManager.getInstance()
val keyManager = KeyManager.getInstance()
val highlightManager = KeywordHighlightManager.getInstance()
val accountOwner = AccountOwner(
id = accountManager.getAccountId(),
name = accountManager.getEmail(),
type = OwnerType.User
)
for (host in database.getHosts()) {
if (host.deleted) continue
hostManager.addHost(host.copy(ownerId = accountManager.getAccountId(), ownerType = OwnerType.User.name))
}
for (snippet in database.getSnippets()) {
if (snippet.deleted) continue
snippetManager.addSnippet(snippet)
}
for (macro in database.getMacros()) {
macroManager.addMacro(macro)
}
for (keymap in database.getKeymaps()) {
keymapManager.addKeymap(keymap)
}
for (keypair in database.getKeyPairs()) {
keyManager.addOhKeyPair(keypair, accountOwner)
}
for (e in database.getKeywordHighlights()) {
highlightManager.addKeywordHighlight(e, accountOwner)
}
val list = listOf(
database.sync,
database.properties,
database.terminal,
database.sftp,
database.appearance,
)
for (e in list) {
for (k in e.getProperties()) {
databaseManager.setSetting(e.name + "." + k.key, k.value)
}
}
for (e in database.safetyProperties.getProperties()) {
databaseManager.setSetting(database.properties.name + "." + e.key, e.value)
}
}
private fun askMigrate(): Boolean {
if (MigrationDialog(null).open()) {
return true
}
// 移动到旧的目录
moveOldDirectory()
// 重启
restart()
return false
}
private fun moveOldDirectory() {
// 关闭数据库
Disposer.dispose(Database.getDatabase())
val file = getDatabaseFile()
FileUtils.moveDirectory(
file,
FileUtils.getFile(file.parentFile, file.name + "-old-" + System.currentTimeMillis())
)
}
private fun restart() {
// 重启
TermoraRestarter.getInstance().scheduleRestart(null, ask = false)
// 退出程序
Disposer.dispose(TermoraFrameManager.getInstance())
}
fun getDatabaseFile(): File {
return FileUtils.getFile(Application.getBaseDataDir(), "storage")
}
}

View File

@@ -0,0 +1,112 @@
package app.termora.plugins.migration
import app.termora.*
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.FlatSVGIcon
import com.formdev.flatlaf.util.SystemInfo
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.JXEditorPane
import java.awt.Dimension
import java.awt.Window
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.imageio.ImageIO
import javax.swing.*
import javax.swing.event.HyperlinkEvent
class MigrationDialog(owner: Window?) : DialogWrapper(owner) {
private var isOpened = false
init {
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
isModal = true
isResizable = false
controlsVisible = false
escapeDispose = false
if (SystemInfo.isWindows || SystemInfo.isLinux) {
title = StringUtils.EMPTY
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
}
if (SystemInfo.isWindows || SystemInfo.isLinux) {
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
val loader = TermoraFrame::class.java.classLoader
val images = sizes.mapNotNull { e ->
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
}
iconImages = images
}
setLocationRelativeTo(null)
init()
}
override fun createCenterPanel(): JComponent {
var rows = 2
val step = 2
val formMargin = "7dlu"
val icon = JLabel()
icon.horizontalAlignment = SwingConstants.CENTER
icon.icon = FlatSVGIcon(Icons.newUI.name, 80, 80)
val editorPane = JXEditorPane()
editorPane.contentType = "text/html"
editorPane.text = MigrationI18n.getString("termora.plugins.migration.message")
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(Int.MAX_VALUE, 225)
addWindowListener(object : WindowAdapter() {
override fun windowOpened(e: WindowEvent) {
removeWindowListener(this)
SwingUtilities.invokeLater { scrollPane.verticalScrollBar.value = 0 }
}
})
return FormBuilder.create().debug(false)
.layout(
FormLayout(
"$formMargin, default:grow, 4dlu, pref, $formMargin",
"${"0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
)
)
.add(icon).xyw(2, rows, 4).apply { rows += step }
.add(scrollPane).xyw(2, rows, 4).apply { rows += step }
.build()
}
fun open(): Boolean {
isModal = true
isVisible = true
return isOpened
}
override fun doOKAction() {
isOpened = true
super.doOKAction()
}
override fun doCancelAction() {
isOpened = false
super.doCancelAction()
}
override fun createOkAction(): AbstractAction {
return OkAction(MigrationI18n.getString("termora.plugins.migration.migrate"))
}
}

View File

@@ -0,0 +1,13 @@
package app.termora.plugins.migration
import app.termora.NamedI18n
import org.slf4j.Logger
import org.slf4j.LoggerFactory
object MigrationI18n : NamedI18n("i18n/messages") {
private val log = LoggerFactory.getLogger(MigrationI18n::class.java)
override fun getLogger(): Logger {
return log
}
}

View File

@@ -0,0 +1,33 @@
package app.termora.plugins.migration
import app.termora.ApplicationRunnerExtension
import app.termora.DynamicIcon
import app.termora.I18n
import app.termora.Icons
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.Plugin
class MigrationPlugin : Plugin {
private val support = ExtensionSupport()
init {
support.addExtension(ApplicationRunnerExtension::class.java) { MigrationApplicationRunnerExtension.instance }
}
override fun getAuthor(): String {
return "TermoraDev"
}
override fun getName(): String {
return "Migration"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,37 @@
package app.termora.plugins.migration
import org.slf4j.LoggerFactory
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
import kotlin.time.measureTime
object PBKDF2 {
private const val ALGORITHM = "PBKDF2WithHmacSHA512"
private val log = LoggerFactory.getLogger(PBKDF2::class.java)
fun generateSecret(
password: CharArray,
salt: ByteArray,
iterationCount: Int = 150000,
keyLength: Int = 256
): ByteArray {
val bytes: ByteArray
val time = measureTime {
bytes = SecretKeyFactory.getInstance(ALGORITHM)
.generateSecret(PBEKeySpec(password, salt, iterationCount, keyLength))
.encoded
}
if (log.isDebugEnabled) {
log.debug("Secret generated $time")
}
return bytes
}
fun hash(slat: ByteArray, password: CharArray, iterationCount: Int, keyLength: Int): ByteArray {
val spec = PBEKeySpec(password, slat, iterationCount, keyLength)
val secretKeyFactory = SecretKeyFactory.getInstance(ALGORITHM)
return secretKeyFactory.generateSecret(spec).encoded
}
}

View File

@@ -1,5 +1,6 @@
package app.termora package app.termora.plugins.migration
import app.termora.*
import app.termora.AES.decodeBase64 import app.termora.AES.decodeBase64
import app.termora.AES.encodeBase64String import app.termora.AES.encodeBase64String

View File

@@ -0,0 +1,7 @@
package app.termora.plugins.migration
enum class SyncType {
GitLab,
GitHub,
Gitee,
WebDAV,
}

View File

@@ -0,0 +1,24 @@
<termora-plugin>
<id>migration</id>
<name>Migration</name>
<version>${projectVersion}</version>
<!-- since: >=xxx , or >xxx -->
<!-- until: <=xxx , or <xxx -->
<termora-version since=">=${rootProjectVersion}" until=""/>
<entry>app.termora.plugins.migration.MigrationPlugin</entry>
<descriptions>
<description>Migrate version 1.x configuration files to 2.x</description>
<description language="zh_CN">将 1.x 版本的配置文件迁移到 2.x</description>
<description language="zh_TW">將 1.x 版本的設定檔移轉到 2.x</description>
</descriptions>
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
</termora-plugin>

View File

@@ -0,0 +1,19 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_659_75852)">
<path d="M7.49998 0.523674C8.50002 5.49999 10.5 7.49999 15.554 8.5C10.5 9.49999 8.5 11.5 7.50005 16.4763C6.50002 11.5 4.50002 9.49999 -0.553986 8.49998C4.5 7.49999 6.5 5.49999 7.49998 0.523674Z" fill="url(#paint0_linear_659_75852)"/>
<path d="M12.9933 4.90705C14.0451 4.90705 14.8979 4.05433 14.8979 3.00245C14.8979 1.95056 14.0451 1.09784 12.9933 1.09784C11.9414 1.09784 11.0886 1.95056 11.0886 3.00245C11.0886 4.05433 11.9414 4.90705 12.9933 4.90705Z" fill="url(#paint1_linear_659_75852)"/>
</g>
<defs>
<linearGradient id="paint0_linear_659_75852" x1="7.50002" y1="0.523674" x2="7.50002" y2="16.4763" gradientUnits="userSpaceOnUse">
<stop stop-color="#3573F0"/>
<stop offset="1" stop-color="#EA33EC"/>
</linearGradient>
<linearGradient id="paint1_linear_659_75852" x1="7.50002" y1="0.523674" x2="7.50002" y2="16.4763" gradientUnits="userSpaceOnUse">
<stop stop-color="#3573F0"/>
<stop offset="1" stop-color="#EA33EC"/>
</linearGradient>
<clipPath id="clip0_659_75852">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,9 @@
termora.plugins.migration.message=<html> \
<h1 align="center">2.0 is ready.</h1> \
<br/> \
<h3>1. The storage structure has been updated. Existing data needs to be migrated. Just click <font color="#3573F0">“Migrate”</font> to complete the process.</h3> \
<h3>2. The <font color="#3573F0">Sync feature</font> is now provided as a plugin. If needed, please <font color="#EA33EC">manually install</font> it from Settings.</h3> \
<h3>3. The <font color="#3573F0">Data Encryption</font> feature has been <font color="#EA33EC">removed</font> (local data will now be stored with basic encryption). Please ensure your device is in a trusted environment.</h3> \
<h3 align="center">📎 For more information, please see: <a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
</html>
termora.plugins.migration.migrate=Migrate

View File

@@ -0,0 +1,9 @@
termora.plugins.migration.message=<html> \
<h1 align="center">2.0 已就绪。</h1> \
<br/> \
<h3>1. 存储结构已更新,需迁移现有数据。只需点击 <font color="#3573F0">“迁移”</font> 即可完成操作。</h3> \
<h3>2. <font color="#3573F0">同步功能</font> 现作为插件提供,如需使用,请前往设置中 <font color="#EA33EC">手动安装</font>。</h3> \
<h3>3. <font color="#3573F0">数据加密</font> 功能已被 <font color="#EA33EC">移除</font>(本地数据将以简单加密方式存储),请确保你的设备处于可信环境中。</h3> \
<h3 align="center">📎 更多信息请查看:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
</html>
termora.plugins.migration.migrate=迁移

View File

@@ -0,0 +1,9 @@
termora.plugins.migration.message=<html> \
<h1 align="center">2.0 已準備就緒。</h1> \
<br/> \
<h3>1. 儲存結構已更新,需要遷移現有資料。只需點擊 <font color="#3573F0">「遷移」</font> 即可完成操作。</h3> \
<h3>2. <font color="#3573F0">同步功能</font> 現以外掛形式提供,如需使用,請至設定中 <font color="#EA33EC">手動安裝</font>。</h3> \
<h3>3. <font color="#3573F0">資料加密</font> 功能已被 <font color="#EA33EC">移除</font>(本機資料將以簡易加密方式儲存),請確保你的裝置處於可信環境中。</h3> \
<h3 align="center">📎 更多資訊請參見:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
</html>
termora.plugins.migration.migrate=遷移

View File

@@ -0,0 +1,16 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.1"
dependencies {
testImplementation(kotlin("test"))
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.4")
compileOnly(project(":"))
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,41 @@
package app.termora.plugins.obs
import org.apache.commons.vfs2.Capability
import org.apache.commons.vfs2.FileName
import org.apache.commons.vfs2.FileSystem
import org.apache.commons.vfs2.FileSystemOptions
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
class OBSFileProvider private constructor() : AbstractOriginatingFileProvider() {
companion object {
val instance by lazy { OBSFileProvider() }
val capabilities = listOf(
Capability.CREATE,
Capability.DELETE,
Capability.RENAME,
Capability.GET_TYPE,
Capability.LIST_CHILDREN,
Capability.READ_CONTENT,
Capability.URI,
Capability.WRITE_CONTENT,
Capability.GET_LAST_MODIFIED,
Capability.SET_LAST_MODIFIED_FILE,
Capability.RANDOM_ACCESS_READ,
Capability.APPEND_CONTENT
)
}
override fun getCapabilities(): Collection<Capability> {
return OBSFileProvider.capabilities
}
override fun doCreateFileSystem(
rootFileName: FileName,
fileSystemOptions: FileSystemOptions
): FileSystem? {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,35 @@
package app.termora.plugins.obs
import app.termora.DynamicIcon
import app.termora.I18n
import app.termora.Icons
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.PaidPlugin
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProviderExtension
class OBSPlugin : PaidPlugin {
private val support = ExtensionSupport()
init {
support.addExtension(ProtocolProviderExtension::class.java) { OBSProtocolProviderExtension.instance }
support.addExtension(ProtocolHostPanelExtension::class.java) { OBSProtocolHostPanelExtension.instance }
}
override fun getAuthor(): String {
return "TermoraDev"
}
override fun getName(): String {
return "Huawei OBS"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,22 @@
package app.termora.plugins.obs
import app.termora.Host
import app.termora.protocol.ProtocolHostPanel
import org.apache.commons.lang3.StringUtils
class OBSProtocolHostPanel : ProtocolHostPanel() {
override fun getHost(): Host {
return Host(
name = StringUtils.EMPTY,
protocol = OBSProtocolProvider.PROTOCOL
)
}
override fun setHost(host: Host) {
}
override fun validateFields(): Boolean {
return true
}
}

View File

@@ -0,0 +1,19 @@
package app.termora.plugins.obs
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
class OBSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
companion object {
val instance by lazy { OBSProtocolHostPanelExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return OBSProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
return OBSProtocolHostPanel()
}
}

View File

@@ -0,0 +1,33 @@
package app.termora.plugins.obs
import app.termora.DynamicIcon
import app.termora.Icons
import app.termora.protocol.FileObjectHandler
import app.termora.protocol.FileObjectRequest
import app.termora.protocol.TransferProtocolProvider
import org.apache.commons.vfs2.provider.FileProvider
class OBSProtocolProvider private constructor() : TransferProtocolProvider {
companion object {
val instance by lazy { OBSProtocolProvider() }
const val PROTOCOL = "OBS"
}
override fun getProtocol(): String {
return PROTOCOL
}
override fun getIcon(width: Int, height: Int): DynamicIcon {
return Icons.huawei
}
override fun getFileProvider(): FileProvider {
return OBSFileProvider.instance
}
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,14 @@
package app.termora.plugins.obs
import app.termora.protocol.ProtocolProvider
import app.termora.protocol.ProtocolProviderExtension
class OBSProtocolProviderExtension private constructor() : ProtocolProviderExtension {
companion object {
val instance by lazy { OBSProtocolProviderExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return OBSProtocolProvider.Companion.instance
}
}

View File

@@ -0,0 +1,24 @@
<termora-plugin>
<id>obs</id>
<name>Huawei OBS</name>
<paid/>
<version>${projectVersion}</version>
<termora-version since=">=${rootProjectVersion}" until=""/>
<entry>app.termora.plugins.obs.OBSPlugin</entry>
<descriptions>
<description>Connecting to Huawei OBS</description>
<description language="zh_CN">支持连接到华为云对象存储</description>
<description language="zh_TW">支援連接到華為雲端物件存儲</description>
</descriptions>
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
</termora-plugin>

View File

@@ -0,0 +1 @@
<svg t="1747212780529" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1350" width="16" height="16"><path d="M843.5 91.90625H180.59375c-48.65625 0-88.40625 39.75-88.40625 88.40625v662.90625c0 48.65625 39.75 88.40625 88.40625 88.40625h662.90625c48.65625 0 88.40625-39.75 88.40625-88.40625V180.21875c0-48.5625-39.75-88.3125-88.40625-88.3125z m-441.5625 636.375c-16.5 11.15625-76.21875 49.6875-114.46875 32.53125-38.34375-17.15625-58.875-60-58.875-60l217.03125-5.8125s-28.3125 21.5625-43.6875 33.28125z m-125.15625-49.78125c-22.3125-3.375-71.0625-16.21875-96-58.125-25.03125-41.90625-12.9375-97.5-9.84375-98.34375 3.09375-0.84375 83.0625 43.78125 121.875 63.28125 38.90625 19.40625 154.875 89.625 155.71875 92.15625 1.125 3-146.625 4.59375-171.75 1.03125z m5.53125-126.9375c-52.875-35.25-71.90625-64.6875-67.78125-108.9375 4.125-44.25 49.5-89.53125 55.875-90.28125 6.28125-0.65625 60.9375 70.40625 103.03125 136.78125 42 66.375 93.46875 157.6875 91.125 163.6875-2.34375 6-129-63.46875-182.25-101.25z m212.34375 80.34375c-0.1875 2.8125-4.125 4.125-7.3125 1.03125-3.28125-3.09375-91.59375-144.1875-108.375-180.09375-16.78125-35.90625-43.3125-105.65625-7.96875-151.21875 35.34375-45.65625 90.65625-43.125 90.65625-43.125 3.5625 5.4375 27.09375 68.8125 35.0625 118.6875 8.0625 49.6875-1.875 251.90625-2.0625 254.71875z m34.78125 0c-0.1875-2.8125-10.125-204.9375-2.15625-254.8125 7.96875-49.875 31.5-113.15625 35.0625-118.6875 0 0 55.3125-2.53125 90.65625 43.125s8.8125 115.3125-7.96875 151.21875C628.25 488.65625 539.9375 629.75 536.65625 632.84375c-3.09375 3.09375-7.03125 1.78125-7.21875-0.9375z m30.1875 21c-2.34375-6 49.03125-97.21875 91.125-163.6875 42-66.375 96.65625-137.53125 103.03125-136.78125 6.28125 0.65625 51.75 45.9375 55.875 90.28125 4.125 44.25-14.90625 73.78125-67.78125 108.9375-53.34375 37.6875-179.90625 107.15625-182.25 101.25z m177.09375 107.90625c-38.34375 17.15625-98.0625-21.375-114.5625-32.53125-15.375-11.71875-43.6875-33.28125-43.6875-33.28125l217.03125 5.8125c0.09375-0.09375-20.53125 42.84375-58.78125 60z m106.59375-140.4375c-25.03125 41.90625-73.6875 54.65625-96 58.125-25.125 3.5625-172.78125 1.96875-171.75-1.03125 0.84375-2.625 116.90625-72.75 155.71875-92.15625 38.8125-19.40625 118.78125-64.125 121.875-63.28125 3.09375 0.9375 15.1875 56.53125-9.84375 98.34375z" fill="#6C707E" p-id="1351"></path></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1 @@
<svg t="1747212780529" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1350" width="16" height="16"><path d="M843.5 91.90625H180.59375c-48.65625 0-88.40625 39.75-88.40625 88.40625v662.90625c0 48.65625 39.75 88.40625 88.40625 88.40625h662.90625c48.65625 0 88.40625-39.75 88.40625-88.40625V180.21875c0-48.5625-39.75-88.3125-88.40625-88.3125z m-441.5625 636.375c-16.5 11.15625-76.21875 49.6875-114.46875 32.53125-38.34375-17.15625-58.875-60-58.875-60l217.03125-5.8125s-28.3125 21.5625-43.6875 33.28125z m-125.15625-49.78125c-22.3125-3.375-71.0625-16.21875-96-58.125-25.03125-41.90625-12.9375-97.5-9.84375-98.34375 3.09375-0.84375 83.0625 43.78125 121.875 63.28125 38.90625 19.40625 154.875 89.625 155.71875 92.15625 1.125 3-146.625 4.59375-171.75 1.03125z m5.53125-126.9375c-52.875-35.25-71.90625-64.6875-67.78125-108.9375 4.125-44.25 49.5-89.53125 55.875-90.28125 6.28125-0.65625 60.9375 70.40625 103.03125 136.78125 42 66.375 93.46875 157.6875 91.125 163.6875-2.34375 6-129-63.46875-182.25-101.25z m212.34375 80.34375c-0.1875 2.8125-4.125 4.125-7.3125 1.03125-3.28125-3.09375-91.59375-144.1875-108.375-180.09375-16.78125-35.90625-43.3125-105.65625-7.96875-151.21875 35.34375-45.65625 90.65625-43.125 90.65625-43.125 3.5625 5.4375 27.09375 68.8125 35.0625 118.6875 8.0625 49.6875-1.875 251.90625-2.0625 254.71875z m34.78125 0c-0.1875-2.8125-10.125-204.9375-2.15625-254.8125 7.96875-49.875 31.5-113.15625 35.0625-118.6875 0 0 55.3125-2.53125 90.65625 43.125s8.8125 115.3125-7.96875 151.21875C628.25 488.65625 539.9375 629.75 536.65625 632.84375c-3.09375 3.09375-7.03125 1.78125-7.21875-0.9375z m30.1875 21c-2.34375-6 49.03125-97.21875 91.125-163.6875 42-66.375 96.65625-137.53125 103.03125-136.78125 6.28125 0.65625 51.75 45.9375 55.875 90.28125 4.125 44.25-14.90625 73.78125-67.78125 108.9375-53.34375 37.6875-179.90625 107.15625-182.25 101.25z m177.09375 107.90625c-38.34375 17.15625-98.0625-21.375-114.5625-32.53125-15.375-11.71875-43.6875-33.28125-43.6875-33.28125l217.03125 5.8125c0.09375-0.09375-20.53125 42.84375-58.78125 60z m106.59375-140.4375c-25.03125 41.90625-73.6875 54.65625-96 58.125-25.125 3.5625-172.78125 1.96875-171.75-1.03125 0.84375-2.625 116.90625-72.75 155.71875-92.15625 38.8125-19.40625 118.78125-64.125 121.875-63.28125 3.09375 0.9375 15.1875 56.53125-9.84375 98.34375z" fill="#CED0D6" p-id="1351"></path></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,17 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.1"
dependencies {
testImplementation(kotlin("test"))
implementation("com.aliyun.oss:aliyun-sdk-oss:3.18.2")
implementation("javax.xml.bind:jaxb-api:2.3.1")
implementation("javax.activation:activation:1.1.1")
implementation("org.glassfish.jaxb:jaxb-runtime:2.3.3")
compileOnly(project(":"))
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,41 @@
package app.termora.plugins.oss
import org.apache.commons.vfs2.Capability
import org.apache.commons.vfs2.FileName
import org.apache.commons.vfs2.FileSystem
import org.apache.commons.vfs2.FileSystemOptions
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
class OSSFileProvider private constructor() : AbstractOriginatingFileProvider() {
companion object {
val instance by lazy { OSSFileProvider() }
val capabilities = listOf(
Capability.CREATE,
Capability.DELETE,
Capability.RENAME,
Capability.GET_TYPE,
Capability.LIST_CHILDREN,
Capability.READ_CONTENT,
Capability.URI,
Capability.WRITE_CONTENT,
Capability.GET_LAST_MODIFIED,
Capability.SET_LAST_MODIFIED_FILE,
Capability.RANDOM_ACCESS_READ,
Capability.APPEND_CONTENT
)
}
override fun getCapabilities(): Collection<Capability> {
return OSSFileProvider.capabilities
}
override fun doCreateFileSystem(
rootFileName: FileName,
fileSystemOptions: FileSystemOptions
): FileSystem? {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,35 @@
package app.termora.plugins.oss
import app.termora.DynamicIcon
import app.termora.I18n
import app.termora.Icons
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionSupport
import app.termora.plugin.PaidPlugin
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProviderExtension
class OSSPlugin : PaidPlugin {
private val support = ExtensionSupport()
init {
support.addExtension(ProtocolProviderExtension::class.java) { OSSProtocolProviderExtension.instance }
support.addExtension(ProtocolHostPanelExtension::class.java) { OSSProtocolHostPanelExtension.instance }
}
override fun getAuthor(): String {
return "TermoraDev"
}
override fun getName(): String {
return "Alibaba OSS"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,22 @@
package app.termora.plugins.oss
import app.termora.Host
import app.termora.protocol.ProtocolHostPanel
import org.apache.commons.lang3.StringUtils
class OSSProtocolHostPanel : ProtocolHostPanel() {
override fun getHost(): Host {
return Host(
name = StringUtils.EMPTY,
protocol = OSSProtocolProvider.PROTOCOL
)
}
override fun setHost(host: Host) {
}
override fun validateFields(): Boolean {
return true
}
}

View File

@@ -0,0 +1,19 @@
package app.termora.plugins.oss
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
class OSSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
companion object {
val instance by lazy { OSSProtocolHostPanelExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return OSSProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
return OSSProtocolHostPanel()
}
}

View File

@@ -0,0 +1,33 @@
package app.termora.plugins.oss
import app.termora.DynamicIcon
import app.termora.Icons
import app.termora.protocol.FileObjectHandler
import app.termora.protocol.FileObjectRequest
import app.termora.protocol.TransferProtocolProvider
import org.apache.commons.vfs2.provider.FileProvider
class OSSProtocolProvider private constructor() : TransferProtocolProvider {
companion object {
val instance by lazy { OSSProtocolProvider() }
const val PROTOCOL = "OSS"
}
override fun getProtocol(): String {
return PROTOCOL
}
override fun getIcon(width: Int, height: Int): DynamicIcon {
return Icons.aliyun
}
override fun getFileProvider(): FileProvider {
return OSSFileProvider.instance
}
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,14 @@
package app.termora.plugins.oss
import app.termora.protocol.ProtocolProvider
import app.termora.protocol.ProtocolProviderExtension
class OSSProtocolProviderExtension private constructor() : ProtocolProviderExtension {
companion object {
val instance by lazy { OSSProtocolProviderExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return OSSProtocolProvider.Companion.instance
}
}

View File

@@ -0,0 +1,24 @@
<termora-plugin>
<id>oss</id>
<name>Alibaba OSS</name>
<paid/>
<version>${projectVersion}</version>
<termora-version since=">=${rootProjectVersion}" until=""/>
<entry>app.termora.plugins.oss.OSSPlugin</entry>
<descriptions>
<description>Connecting to Alibaba OSS</description>
<description language="zh_CN">支持连接到阿里云对象存储</description>
<description language="zh_TW">支援連接到阿里雲物件存儲</description>
</descriptions>
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
</termora-plugin>

View File

@@ -0,0 +1 @@
<svg t="1747212946112" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1350" width="16" height="16"><path d="M853.33333333 79.6444448H170.66666667C120.6044448 79.6444448 79.6444448 120.6044448 79.6444448 170.66666667v682.66666666c0 50.06222187 40.96 91.02222187 91.02222187 91.02222187h682.66666666c50.06222187 0 91.02222187-40.96 91.02222187-91.02222187V170.66666667c0-50.06222187-40.96-91.02222187-91.02222187-91.02222187zM298.09777813 700.87111147c-56.88888853 0-104.6755552-45.51111147-104.67555626-102.4v-175.21777814c0-56.88888853 45.51111147-102.4 104.67555626-102.4v-2.2755552H466.48888853l-13.65333333 59.16444374-145.6355552 31.85777813c-13.65333333 4.55111147-22.7555552 13.65333333-22.7555552 27.30666667v147.91111146c0 13.65333333 11.37777813 25.03111147 22.7555552 27.30666667l143.36 29.58222187 13.65333333 59.1644448h-166.1155552z m273.06666667-202.5244448v29.58222186h-116.05333333v-29.58222186h116.05333333zM830.57777813 600.74666667c0 56.88888853-45.51111147 102.4-104.67555626 102.4H557.51111147l13.65333333-59.1644448 143.36-29.58222187c13.65333333-4.55111147 22.7555552-13.65333333 22.7555552-27.30666667v-147.91111146c0-13.65333333-11.37777813-25.03111147-22.7555552-27.30666667l-143.36-29.58222187-13.65333333-59.1644448h166.1155552c56.88888853 0 104.6755552 45.51111147 104.6755552 102.4v175.21777814z" p-id="1351" fill="#6C707E"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg t="1747211795611" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1290" width="16" height="16"><path d="M832 106.666667H192C145.066667 106.666667 106.666667 145.066667 106.666667 192v640c0 46.933333 38.4 85.333333 85.333333 85.333333h640c46.933333 0 85.333333-38.4 85.333333-85.333333V192c0-46.933333-38.4-85.333333-85.333333-85.333333zM311.466667 689.066667c-53.333333 0-98.133333-42.666667-98.133334-96v-164.266667c0-53.333333 42.666667-96 98.133334-96v-2.133333H469.333333l-12.8 55.466666-136.533333 29.866667c-12.8 4.266667-21.333333 12.8-21.333333 25.6v138.666667c0 12.8 10.666667 23.466667 21.333333 25.6l134.4 27.733333 12.8 55.466667h-155.733333z m256-189.866667v27.733333h-108.8v-27.733333h108.8zM810.666667 595.2c0 53.333333-42.666667 96-98.133334 96H554.666667l12.8-55.466667 134.4-27.733333c12.8-4.266667 21.333333-12.8 21.333333-25.6v-138.666667c0-12.8-10.666667-23.466667-21.333333-25.6l-134.4-27.733333-12.8-55.466667h155.733333c53.333333 0 98.133333 42.666667 98.133333 96v164.266667z" p-id="1291" fill="#CED0D6"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Some files were not shown because too many files have changed in this diff Show More