Compare commits

...

160 Commits

Author SHA1 Message Date
hstyi
f329ef60df release: 2.0.0-beta.8 2025-07-14 14:20:44 +08:00
hstyi
8acfdb8bca feat: transfer supports copy and paste 2025-07-14 13:50:12 +08:00
hstyi
a7aec52f2a fix: password text field status 2025-07-14 11:56:59 +08:00
hstyi
7f1317a9a7 chore: improve terminal options 2025-07-14 11:11:20 +08:00
hstyi
a8a1fea91b feat: support focus mode 2025-07-14 11:02:40 +08:00
hstyi
675ad4608a chore: improve keymap refresh 2025-07-11 14:57:24 +08:00
hstyi
72ba3757e2 chore: discussion group 2025-07-11 11:24:29 +08:00
hstyi
c58e84d2ae chore: windows action 2025-07-11 10:05:38 +08:00
hstyi
18a7a5059b feat: keyword highlight support import and export 2025-07-11 09:20:24 +08:00
hstyi
f0102b6f13 fix: windows tray icon size 2025-07-10 16:13:50 +08:00
hstyi
0cf8eb3c17 chore: README 2025-07-10 12:14:33 +08:00
hstyi
c08a9f2b18 release: 2.0.0-beta.7 2025-07-10 11:50:43 +08:00
hstyi
728f1f2802 fix: xterm ScrollRegion 2025-07-10 11:30:18 +08:00
hstyi
7310211fba feat: telnet support login scripts 2025-07-10 11:30:05 +08:00
dependabot[bot]
1f3267de0a chore(deps): bump org.apache.commons:commons-lang3 from 3.17.0 to 3.18.0
Bumps org.apache.commons:commons-lang3 from 3.17.0 to 3.18.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-lang3
  dependency-version: 3.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-10 11:25:28 +08:00
hstyi
8ddad59c70 chore: use docker jbr 2025-07-10 09:30:12 +08:00
hstyi
9ff6d0afa1 feat: telnet character mode 2025-07-09 19:57:56 +08:00
hstyi
2341b09f81 fix: osx.yml 2025-07-09 19:57:22 +08:00
hstyi
5830aa937a feat: improve GitHub actions 2025-07-09 18:15:36 +08:00
hstyi
56a9361e86 release: 2.0.0-beta.7 2025-07-09 12:22:34 +08:00
hstyi
5868aa4d2f fix: unable to update automatically 2025-07-09 12:18:19 +08:00
dependabot[bot]
45135b7299 chore(deps): bump exposed from 1.0.0-beta-3 to 1.0.0-beta-4
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-09 12:14:23 +08:00
hstyi
a0020fede1 release: 2.0.0-beta.6 2025-07-09 10:27:28 +08:00
hstyi
6f1eaab456 chore: ru_RU i18n 2025-07-09 09:52:02 +08:00
ForumLiker
6173eae772 chore: messages_ru_RU.properties 2025-07-09 09:23:09 +08:00
hstyi
0bb366b1f7 chore: quick connect typo 2025-07-08 21:30:45 +08:00
hstyi
9a4d6f7f4d chore: temporary host disabled editing 2025-07-08 21:22:34 +08:00
hstyi
a4ae11e301 feat: quick connect 2025-07-08 18:21:19 +08:00
dependabot[bot]
5af0acb619 chore(deps): bump cn.hutool:hutool-all from 5.8.37 to 5.8.39
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 16:16:42 +08:00
hstyi
042434b8f8 chore: find everywhere 2025-07-08 16:10:14 +08:00
hstyi
eddc7ef0c6 refactor: frame toolbar 2025-07-08 16:05:31 +08:00
dependabot[bot]
c96ca2d424 chore(deps): bump com.github.mwiede:jsch from 0.2.26 to 2.27.2
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 16:05:01 +08:00
hstyi
45be9008fd refactor: key shortcuts 2025-07-08 14:47:37 +08:00
hstyi
057da4e297 chore: okhttp 5.1.0 2025-07-08 14:01:11 +08:00
hstyi
e4e41667ff chore: osx actions 2025-07-08 12:24:25 +08:00
dependabot[bot]
95ca0a4af7 chore(deps): bump com.github.hstyi:geolite2 from v1.0-202506280327 to v1.0-202507040118
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hstyi <hstyi@foxmail.com>
2025-07-08 12:09:42 +08:00
hstyi
702dee7983 feat: serial plugin 2025-07-08 12:00:02 +08:00
hstyi
165d544448 feat: ftp plugin 2025-07-08 11:38:40 +08:00
hstyi
ecf61bedc4 chore: remove button 2025-07-07 15:50:48 +08:00
hstyi
66a81a5da3 feat: transfer support compress and extract 2025-07-07 15:48:04 +08:00
hstyi
574c816ebb fix: system disk cannot be entered 2025-07-07 11:06:53 +08:00
hstyi
e7cafb74e4 chore: clone session order 2025-07-07 10:34:01 +08:00
hstyi
5050aa37f5 release: 2.0.0-beta.5 2025-07-07 09:47:35 +08:00
hstyi
53d3d96a06 chore: dynamically modify icons 2025-07-07 09:35:55 +08:00
hstyi
d40b8a4c9c fix: windows drive list failure 2025-07-06 16:48:50 +08:00
hstyi
728671509c feat: transfer support disconnection and reconnection 2025-07-06 16:16:43 +08:00
hstyi
b7178a30fb chore: telnet supports backspace key setting 2025-07-06 11:01:44 +08:00
hstyi
939d6a1fd7 chore: improve flatlaf 2025-07-05 16:02:56 +08:00
hstyi
2986a9cc46 fix: binary compatibility 2025-07-05 14:35:42 +08:00
hstyi
f36afaf5d3 chore: telnet default port 23 2025-07-05 14:07:10 +08:00
hstyi
8cec835583 feat: support telnet 2025-07-05 14:07:10 +08:00
hstyi
a32838dad6 feat: support clone session 2025-07-05 12:07:33 +08:00
hstyi
d54671757e fix: tab drag and drop 2025-07-05 10:17:57 +08:00
hstyi
d1dba56bcd chore: improve sidebar 2025-07-04 16:36:37 +08:00
hstyi
919c06779d chore: improve rm -rf 2025-07-04 15:32:51 +08:00
hstyi
1c90fb4e18 release: 2.0.0-beta.4 2025-07-04 14:35:50 +08:00
dependabot[bot]
c1f1d5185e chore(deps): bump com.fazecast:jSerialComm from 2.11.0 to 2.11.2
Bumps [com.fazecast:jSerialComm](https://github.com/Fazecast/jSerialComm) from 2.11.0 to 2.11.2.
- [Release notes](https://github.com/Fazecast/jSerialComm/releases)
- [Commits](https://github.com/Fazecast/jSerialComm/compare/v2.11.0...v2.11.2)

---
updated-dependencies:
- dependency-name: com.fazecast:jSerialComm
  dependency-version: 2.11.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-04 12:20:45 +08:00
dependabot[bot]
aa4863712d chore(deps): bump org.jetbrains.kotlinx:kotlinx-serialization-json
Bumps [org.jetbrains.kotlinx:kotlinx-serialization-json](https://github.com/Kotlin/kotlinx.serialization) from 1.8.1 to 1.9.0.
- [Release notes](https://github.com/Kotlin/kotlinx.serialization/releases)
- [Changelog](https://github.com/Kotlin/kotlinx.serialization/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Kotlin/kotlinx.serialization/compare/v1.8.1...v1.9.0)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlinx:kotlinx-serialization-json
  dependency-version: 1.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-04 12:20:34 +08:00
dependabot[bot]
247640f2e5 chore(deps): bump org.dom4j:dom4j from 2.1.4 to 2.2.0
Bumps [org.dom4j:dom4j](https://github.com/dom4j/dom4j) from 2.1.4 to 2.2.0.
- [Release notes](https://github.com/dom4j/dom4j/releases)
- [Commits](https://github.com/dom4j/dom4j/compare/version-2.1.4...version/2.2.0)

---
updated-dependencies:
- dependency-name: org.dom4j:dom4j
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-04 12:20:24 +08:00
dependabot[bot]
5b1f803fa8 chore(deps): bump com.aliyun.oss:aliyun-sdk-oss from 3.18.2 to 3.18.3
Bumps [com.aliyun.oss:aliyun-sdk-oss](https://github.com/aliyun/aliyun-oss-java-sdk) from 3.18.2 to 3.18.3.
- [Release notes](https://github.com/aliyun/aliyun-oss-java-sdk/releases)
- [Commits](https://github.com/aliyun/aliyun-oss-java-sdk/commits)

---
updated-dependencies:
- dependency-name: com.aliyun.oss:aliyun-sdk-oss
  dependency-version: 3.18.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-04 12:20:15 +08:00
hstyi
accf590c17 feat: support fallback font 2025-07-04 12:19:56 +08:00
hstyi
19fbeab817 fix: shortcut keys cannot be saved 2025-07-04 12:13:01 +08:00
hstyi
a785ab4680 chore: disable opengl 2025-07-04 09:58:08 +08:00
hstyi
5ee23cb379 fix: cursor style not working 2025-07-04 09:14:58 +08:00
hstyi
145d2de001 chore: tiny images 2025-07-03 15:54:38 +08:00
hstyi
8d3f5fe622 chore: README 2025-07-03 15:47:19 +08:00
hstyi
9ce4a88041 feat: use extension for floating toolbar 2025-07-03 12:00:02 +08:00
dependabot[bot]
c0ecc9fa7d chore(deps): bump exposed from 1.0.0-beta-2 to 1.0.0-beta-3
Bumps `exposed` from 1.0.0-beta-2 to 1.0.0-beta-3.

Updates `org.jetbrains.exposed:exposed-core` from 1.0.0-beta-2 to 1.0.0-beta-3
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/compare/1.0.0-beta-2...1.0.0-beta-3)

Updates `org.jetbrains.exposed:exposed-crypt` from 1.0.0-beta-2 to 1.0.0-beta-3
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/compare/1.0.0-beta-2...1.0.0-beta-3)

Updates `org.jetbrains.exposed:exposed-jdbc` from 1.0.0-beta-2 to 1.0.0-beta-3
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/compare/1.0.0-beta-2...1.0.0-beta-3)

Updates `org.jetbrains.exposed:exposed-migration` from 1.0.0-beta-2 to 1.0.0-beta-3
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/compare/1.0.0-beta-2...1.0.0-beta-3)

---
updated-dependencies:
- dependency-name: org.jetbrains.exposed:exposed-core
  dependency-version: 1.0.0-beta-3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.exposed:exposed-crypt
  dependency-version: 1.0.0-beta-3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.exposed:exposed-jdbc
  dependency-version: 1.0.0-beta-3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.exposed:exposed-migration
  dependency-version: 1.0.0-beta-3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 11:38:43 +08:00
hstyi
cb33a4468a chore: show close button 2025-07-03 10:48:15 +08:00
hstyi
168c4c5c64 chore: improve team sync 2025-07-03 08:49:00 +08:00
hstyi
9916edbd13 feat: support custom layout 2025-07-02 12:15:28 +08:00
hstyi
ab6b6a2127 chore: improve Windows task icon 2025-07-01 13:42:50 +08:00
hstyi
c45f5f4c92 fix: plugins compatibility 2025-07-01 13:21:53 +08:00
hstyi
92ee2d72f2 chore: only show flags on macOS 2025-07-01 12:03:27 +08:00
hstyi
a4364bcd6a release: 2.0.0-beta.3 2025-07-01 11:00:55 +08:00
hstyi
d0827c3b0c chore: improve connect-with 2025-07-01 10:52:45 +08:00
hstyi
036a04b0b3 feat: support SMB 2025-07-01 10:49:27 +08:00
hstyi
eee016c643 chore: supports retaining file modification date 2025-07-01 10:46:17 +08:00
hstyi
472bf6e81f feat: support login scripts 2025-07-01 10:40:06 +08:00
hstyi
21229e352f chore: do not refresh during installation 2025-06-30 17:34:02 +08:00
hstyi
1138f48a6e fix: host deletion query error 2025-06-30 17:18:05 +08:00
hstyi
f044e0480e chore: password show caps lock 2025-06-30 17:16:52 +08:00
hstyi
7047f17783 fix: quick open transfer failure 2025-06-30 14:33:55 +08:00
dependabot[bot]
9308f15abb chore(deps): bump com.qcloud:cos_api from 5.6.245 to 5.6.247
Bumps [com.qcloud:cos_api](https://github.com/tencentyun/cos-java-sdk-v5) from 5.6.245 to 5.6.247.
- [Release notes](https://github.com/tencentyun/cos-java-sdk-v5/releases)
- [Changelog](https://github.com/tencentyun/cos-java-sdk-v5/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tencentyun/cos-java-sdk-v5/commits)

---
updated-dependencies:
- dependency-name: com.qcloud:cos_api
  dependency-version: 5.6.247
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 14:29:41 +08:00
dependabot[bot]
b2672f11fc chore(deps): bump org.testcontainers:testcontainers-bom
Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 1.21.2 to 1.21.3.
- [Release notes](https://github.com/testcontainers/testcontainers-java/releases)
- [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.21.2...1.21.3)

---
updated-dependencies:
- dependency-name: org.testcontainers:testcontainers-bom
  dependency-version: 1.21.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 14:29:29 +08:00
dependabot[bot]
f92c6586b2 chore(deps): bump org.semver4j:semver4j from 5.8.0 to 6.0.0
Bumps [org.semver4j:semver4j](https://github.com/semver4j/semver4j) from 5.8.0 to 6.0.0.
- [Release notes](https://github.com/semver4j/semver4j/releases)
- [Commits](https://github.com/semver4j/semver4j/compare/v5.8.0...v6.0.0)

---
updated-dependencies:
- dependency-name: org.semver4j:semver4j
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 14:29:20 +08:00
dependabot[bot]
69e07a9bd9 chore(deps): bump com.huaweicloud:esdk-obs-java-bundle
Bumps [com.huaweicloud:esdk-obs-java-bundle](https://github.com/huaweicloud/huaweicloud-sdk-java-obs) from 3.25.4 to 3.25.5.
- [Release notes](https://github.com/huaweicloud/huaweicloud-sdk-java-obs/releases)
- [Commits](https://github.com/huaweicloud/huaweicloud-sdk-java-obs/compare/v3.25.4...v3.25.5)

---
updated-dependencies:
- dependency-name: com.huaweicloud:esdk-obs-java-bundle
  dependency-version: 3.25.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 14:29:07 +08:00
dependabot[bot]
cdec60fd25 chore(deps): bump org.xerial:sqlite-jdbc from 3.50.1.0 to 3.50.2.0
Bumps [org.xerial:sqlite-jdbc](https://github.com/xerial/sqlite-jdbc) from 3.50.1.0 to 3.50.2.0.
- [Release notes](https://github.com/xerial/sqlite-jdbc/releases)
- [Changelog](https://github.com/xerial/sqlite-jdbc/blob/master/CHANGELOG)
- [Commits](https://github.com/xerial/sqlite-jdbc/compare/3.50.1.0...3.50.2.0)

---
updated-dependencies:
- dependency-name: org.xerial:sqlite-jdbc
  dependency-version: 3.50.2.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 14:28:47 +08:00
hstyi
7c30933794 fix: wsl reg exception 2025-06-30 13:36:35 +08:00
hstyi
b892d2fe13 release: 2.0.0-beta.2 2025-06-30 12:50:10 +08:00
hstyi
91ee463d41 fix: data migration not working 2025-06-30 12:43:31 +08:00
hstyi
169b66334c release: 2.0.0-beta.1 2025-06-30 10:02:04 +08:00
hstyi
729eb99730 fix: extension name retrieval failure 2025-06-30 09:40:40 +08:00
hstyi
b1e62952f5 fix: data sync delay 2025-06-30 00:36:10 +08:00
hstyi
e21e9f9ed9 fix: saved data causes tags to be lost 2025-06-30 00:36:01 +08:00
hstyi
885c0a6337 fix: tree root node not displaying 2025-06-30 00:12:57 +08:00
hstyi
09d837f5b8 chore: improve code 2025-06-29 13:45:07 +08:00
hstyi
b1e1f38b50 chore: improve plugin settings 2025-06-29 13:01:37 +08:00
hstyi
efa9613d26 fix: editor plugin popup 2025-06-29 12:37:44 +08:00
hstyi
287f6973f0 chore: improve editor 2025-06-29 12:12:30 +08:00
hstyi
70fc5e3228 chore: improve plugin restart prompt 2025-06-28 17:04:21 +08:00
hstyi
4bca15dbb0 fix: wsl reg 2025-06-28 17:04:12 +08:00
hstyi
ef2c57bb29 fix: geo class cast 2025-06-28 16:14:05 +08:00
hstyi
1135ecc5a3 chore: improve code 2025-06-28 15:55:40 +08:00
hstyi
e1b2e7b4db chore: upgrade geo version 2025-06-28 11:32:12 +08:00
hstyi
eec9154aeb chore: remove transfer Append 2025-06-28 11:30:17 +08:00
hstyi
fff2dd89c7 chore: improve geo 2025-06-28 11:13:26 +08:00
hstyi
54116a4bf5 feat: support Huawei OBS 2025-06-27 15:42:00 +08:00
hstyi
f28e785301 feat: support WebDAV 2025-06-27 12:18:01 +08:00
hstyi
39b9bba9cf feat: support tencent OSS 2025-06-27 10:36:43 +08:00
hstyi
d7120cabe0 chore: classes plugins 2025-06-26 18:18:27 +08:00
hstyi
007318dae3 feat: support tencent cos 2025-06-26 18:05:39 +08:00
hstyi
e25bd485ac chore: detection installation type 2025-06-26 15:40:40 +08:00
hstyi
7ba8e177b1 chore: account MFA 2025-06-26 10:07:06 +08:00
hstyi
17082c5fb8 chore: transfer supports custom type 2025-06-26 09:46:25 +08:00
hstyi
00dfb4ce39 chore: support s3 proxy 2025-06-25 18:12:50 +08:00
hstyi
cee0c863f8 feat: support S3 transfer protocol 2025-06-25 16:47:56 +08:00
hstyi
a2a02c0bad chore: improve wsl 2025-06-25 12:05:24 +08:00
hstyi
891688d0ca fix: displaying WSL on other platforms 2025-06-25 12:05:24 +08:00
hstyi
3d47840aa8 feat: WSL support on Windows 2025-06-25 12:05:24 +08:00
hstyi
01d0f9d4bd chore: improve account 2025-06-24 17:02:20 +08:00
hstyi
1c8abf9cba chore: improve badge 2025-06-24 15:50:23 +08:00
hstyi
efd01da6f1 chore: transfer support contextmenu 2025-06-24 12:15:41 +08:00
dependabot[bot]
c929a794d5 chore(deps): bump org.semver4j:semver4j from 5.7.1 to 5.8.0
Bumps [org.semver4j:semver4j](https://github.com/semver4j/semver4j) from 5.7.1 to 5.8.0.
- [Release notes](https://github.com/semver4j/semver4j/releases)
- [Commits](https://github.com/semver4j/semver4j/compare/v5.7.1...v5.8.0)

---
updated-dependencies:
- dependency-name: org.semver4j:semver4j
  dependency-version: 5.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-24 12:15:18 +08:00
dependabot[bot]
85b2f222f4 chore(deps): bump kotlin from 2.1.21 to 2.2.0
Bumps `kotlin` from 2.1.21 to 2.2.0.

Updates `org.jetbrains.kotlin.jvm` from 2.1.21 to 2.2.0
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/v2.2.0/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.1.21...v2.2.0)

Updates `org.jetbrains.kotlin.plugin.serialization` from 2.1.21 to 2.2.0
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/v2.2.0/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.1.21...v2.2.0)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlin.jvm
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.jetbrains.kotlin.plugin.serialization
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-24 11:51:21 +08:00
hstyi
899bd5a356 fix: i18n typo 2025-06-23 12:04:03 +08:00
dependabot[bot]
ded16873b0 chore(deps): bump org.testcontainers:testcontainers-bom
---
updated-dependencies:
- dependency-name: org.testcontainers:testcontainers-bom
  dependency-version: 1.21.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-23 11:35:04 +08:00
dependabot[bot]
501013ba31 chore(deps): bump org.commonmark:commonmark from 0.24.0 to 0.25.0
Bumps [org.commonmark:commonmark](https://github.com/commonmark/commonmark-java) from 0.24.0 to 0.25.0.
- [Release notes](https://github.com/commonmark/commonmark-java/releases)
- [Changelog](https://github.com/commonmark/commonmark-java/blob/main/CHANGELOG.md)
- [Commits](https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.24.0...commonmark-parent-0.25.0)

---
updated-dependencies:
- dependency-name: org.commonmark:commonmark
  dependency-version: 0.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-23 11:18:41 +08:00
dependabot[bot]
76b1a2f4f8 chore(deps): bump com.fifesoft:languagesupport from 3.3.0 to 3.4.0
Bumps [com.fifesoft:languagesupport](https://github.com/bobbylight/RSTALanguageSupport) from 3.3.0 to 3.4.0.
- [Release notes](https://github.com/bobbylight/RSTALanguageSupport/releases)
- [Commits](https://github.com/bobbylight/RSTALanguageSupport/compare/3.3.0...3.4.0)

---
updated-dependencies:
- dependency-name: com.fifesoft:languagesupport
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-23 11:18:29 +08:00
hstyi
e34fea0ecb chore: linux deb 2025-06-22 18:55:48 +08:00
hstyi
7532546c77 fix: abnormal operation of transfer dialog 2025-06-22 14:37:37 +08:00
hstyi
187d5be658 feat: support transfer virtual window 2025-06-22 14:37:37 +08:00
hstyi
2bf4d277be chore: transfer shows error message 2025-06-22 12:42:33 +08:00
hstyi
389c243ada feat: transfer supports side button 2025-06-21 18:25:16 +08:00
hstyi
bb39178b88 fix: OTP dialog width 2025-06-21 18:03:18 +08:00
hstyi
e1eab9db06 refactor: transfer 2025-06-21 17:01:15 +08:00
hstyi
e6a45d25cd chore: improve sync plugin 2025-06-16 17:45:48 +08:00
hstyi
a64aef24b2 chore: build before dist 2025-06-16 15:31:10 +08:00
hstyi
c9c8aa8f2a chore: improve code 2025-06-16 15:23:02 +08:00
hstyi
d3cfde5238 fix: host filtering may not work 2025-06-16 15:22:43 +08:00
hstyi
b4e82a4a0e chore: add a badge to the plugin update button 2025-06-16 10:13:39 +08:00
hstyi
40b4848cc8 chore: bg 0.0.3 2025-06-16 09:41:54 +08:00
hstyi
0c4268a194 feat: support account registration 2025-06-16 09:32:22 +08:00
hstyi
866c823c30 fix: title not showing on Linux
(cherry picked from commit 071a091347)
2025-06-16 09:26:19 +08:00
hstyi
6f9cfc3b32 chore: improve native 2025-06-15 18:00:31 +08:00
hstyi
3717393ad9 chore: rename ssh-agent.sock 2025-06-15 17:00:08 +08:00
hstyi
10e7681ac2 chore: improve SSH_AUTH_SOCK reading on macOS 2025-06-15 16:45:20 +08:00
hstyi
51047e60f4 chore: FindEverywhere using extension 2025-06-15 12:19:25 +08:00
hstyi
02984bb2a2 fix: marketplace retry not working 2025-06-15 11:47:26 +08:00
hstyi
ea6b2d6a66 feat: support plugin repository 2025-06-15 08:39:01 +08:00
hstyi
95bf08b0da chore: user agent 2025-06-15 08:38:50 +08:00
hstyi
15131fefd1 chore: kotlin reflect 2025-06-14 16:47:30 +08:00
hstyi
26a06b1c91 chore: improve host tree filtering 2025-06-14 16:26:31 +08:00
dependabot[bot]
3cca89b917 chore(deps): bump org.semver4j:semver4j from 5.7.0 to 5.7.1
Bumps [org.semver4j:semver4j](https://github.com/semver4j/semver4j) from 5.7.0 to 5.7.1.
- [Release notes](https://github.com/semver4j/semver4j/releases)
- [Commits](https://github.com/semver4j/semver4j/compare/v5.7.0...v5.7.1)

---
updated-dependencies:
- dependency-name: org.semver4j:semver4j
  dependency-version: 5.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-13 16:54:03 +08:00
dependabot[bot]
d31510eb86 chore(deps): bump org.xerial:sqlite-jdbc from 3.49.1.0 to 3.50.1.0
Bumps [org.xerial:sqlite-jdbc](https://github.com/xerial/sqlite-jdbc) from 3.49.1.0 to 3.50.1.0.
- [Release notes](https://github.com/xerial/sqlite-jdbc/releases)
- [Changelog](https://github.com/xerial/sqlite-jdbc/blob/master/CHANGELOG)
- [Commits](https://github.com/xerial/sqlite-jdbc/compare/3.49.1.0...3.50.1.0)

---
updated-dependencies:
- dependency-name: org.xerial:sqlite-jdbc
  dependency-version: 3.50.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-13 16:45:22 +08:00
dependabot[bot]
feaec3e223 chore(deps): bump exposed from 1.0.0-beta-1 to 1.0.0-beta-2
Bumps `exposed` from 1.0.0-beta-1 to 1.0.0-beta-2.

Updates `org.jetbrains.exposed:exposed-core` from 1.0.0-beta-1 to 1.0.0-beta-2
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/compare/1.0.0-beta-1...1.0.0-beta-2)

Updates `org.jetbrains.exposed:exposed-crypt` from 1.0.0-beta-1 to 1.0.0-beta-2
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/compare/1.0.0-beta-1...1.0.0-beta-2)

Updates `org.jetbrains.exposed:exposed-jdbc` from 1.0.0-beta-1 to 1.0.0-beta-2
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/compare/1.0.0-beta-1...1.0.0-beta-2)

Updates `org.jetbrains.exposed:exposed-migration` from 1.0.0-beta-1 to 1.0.0-beta-2
- [Release notes](https://github.com/JetBrains/Exposed/releases)
- [Changelog](https://github.com/JetBrains/Exposed/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/Exposed/compare/1.0.0-beta-1...1.0.0-beta-2)

---
updated-dependencies:
- dependency-name: org.jetbrains.exposed:exposed-core
  dependency-version: 1.0.0-beta-2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.exposed:exposed-crypt
  dependency-version: 1.0.0-beta-2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.exposed:exposed-jdbc
  dependency-version: 1.0.0-beta-2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.exposed:exposed-migration
  dependency-version: 1.0.0-beta-2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-13 16:45:09 +08:00
hstyi
97886e019f fix: mixpanel endpoint 2025-06-13 16:35:52 +08:00
hstyi
7cb6696bba chore: improve geo 2025-06-13 16:26:41 +08:00
hstyi
ab017be855 chore: improve database secret 2025-06-13 16:08:16 +08:00
hstyi
6177bbdc68 chore!: migrate to version 2.x 2025-06-13 15:16:56 +08:00
720 changed files with 35438 additions and 10453 deletions

View File

@@ -1,47 +0,0 @@
name: Linux aarch64
on: [ push, pull_request ]
jobs:
build:
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-aarch64-b1034.51.tar.gz
# appimagetool
- run: sudo apt install libfuse2
# install jdk
- name: Installing Java
uses: actions/setup-java@v4
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.7'
architecture: aarch64
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
# dist
- run: |
./gradlew dist --no-daemon
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-linux-aarch64
path: |
build/distributions/*.tar.gz
build/distributions/*.AppImage

View File

@@ -1,47 +0,0 @@
name: Linux x86-64
on: [ push, pull_request ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-x64-b1034.51.tar.gz
# appimagetool
- run: sudo apt install libfuse2
# install jdk
- name: Installing Java
uses: actions/setup-java@v4
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.7'
architecture: x64
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
# dist
- run: |
./gradlew dist --no-daemon
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-linux-x86-64
path: |
build/distributions/*.tar.gz
build/distributions/*.AppImage

69
.github/workflows/linux.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
name: Linux
on: [ push, pull_request ]
env:
JBR_MAJOR: 21.0.7
JBR_PATCH: b1038.58
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ ubuntu-24.04-arm, ubuntu-latest ]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradlexyz-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradlexyz-
- name: Set dynamic DOCKER_NAME
run: |
echo "DOCKER_NAME=hstyi/jbr:${{ env.JBR_MAJOR }}${{ env.JBR_PATCH }}" >> $GITHUB_ENV
- name: Create docker-run.sh helper script
shell: bash
run: |
cat <<'EOF' > docker-run.sh
#!/bin/bash
docker run --rm -v $HOME/.gradle:/root/.gradle -v "$(pwd)":/app -w /app "$@"
EOF
chmod +x docker-run.sh
- name: Compile
shell: bash
run: ./docker-run.sh $DOCKER_NAME bash -c './gradlew :check-license && ./gradlew classes -x test'
- name: JLink
shell: bash
run: ./docker-run.sh $DOCKER_NAME bash -c './gradlew :jar :copy-dependencies :plugins:migration:build :jlink'
- name: Package Deb
shell: bash
run: ./docker-run.sh -e TERMORA_TYPE=deb $DOCKER_NAME bash -c './gradlew :jpackage && ./gradlew :dist'
- name: Package AppImage
shell: bash
run: ./docker-run.sh --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor:unconfined $DOCKER_NAME bash -c 'rm -rf build/jpackage && ./gradlew :jpackage && ./gradlew :dist'
- name: Make ~/.gradle world-writable
shell: bash
run: sudo chmod -R 777 ~/.gradle
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-linux-${{ runner.arch }}
path: |
build/distributions/*.tar.gz
build/distributions/*.AppImage
build/distributions/*.deb

View File

@@ -1,84 +0,0 @@
name: macOS aarch64
on: [ push, pull_request ]
jobs:
build:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install the Apple certificate
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# create variables
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# import certificate from secrets
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Setup the Notary information
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
STORE_CREDENTIALS: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
run: |
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
# download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-aarch64-b1034.51.tar.gz
# install jdk
- name: Installing Java
uses: actions/setup-java@v4
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.7'
architecture: aarch64
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
# dist
- name: Dist
env:
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
# 只有发布版本时才需要公证
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
run: |
./gradlew dist --no-daemon
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-osx-aarch64
path: |
build/distributions/*.zip
build/distributions/*.dmg

View File

@@ -1,17 +1,29 @@
name: macOS x86-64
name: macOS
on: [ push, pull_request ]
env:
TERMORA_MAC_SIGN: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
# 只有发布版本时才需要公证
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
JBR_MAJOR: 21.0.7
JBR_PATCH: b1038.58
jobs:
build:
runs-on: macos-13
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ macos-15, macos-13 ]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install the Apple certificate
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''"
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
@@ -34,7 +46,7 @@ jobs:
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Setup the Notary information
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.APPLE_ID != ''"
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
@@ -43,8 +55,14 @@ jobs:
run: |
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
# download jdk
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-x64-b1034.51.tar.gz
- name: Download Java
run: |
if [[ "$(uname -m)" == "arm64" ]]; then
ARCH="aarch64"
else
ARCH="x64"
fi
wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${{ env.JBR_MAJOR }}-osx-$ARCH-${{ env.JBR_PATCH }}.tar.gz
# install jdk
- name: Installing Java
@@ -53,8 +71,6 @@ jobs:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
java-version: '21.0.7'
architecture: x64
- uses: actions/cache@v4
with:
@@ -65,22 +81,22 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
- name: Compile
shell: bash
run: ./gradlew :check-license && ./gradlew classes -x test
# dist
- name: Dist
env:
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
# 只有发布版本时才需要公证
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
run: |
./gradlew dist --no-daemon
- name: JLink
shell: bash
run: ./gradlew :jar :copy-dependencies :plugins:migration:build :jlink
- name: Package
shell: bash
run: ./gradlew :jpackage && ./gradlew :dist
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-osx-x86-64
name: termora-osx-${{ runner.arch }}
path: |
build/distributions/*.zip
build/distributions/*.dmg

View File

@@ -1,48 +0,0 @@
name: Windows x86-64
on: [ push, pull_request ]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install zip
run: |
$system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32"
Invoke-WebRequest -Uri "http://stahlworks.com/dev/zip.exe" -OutFile "$system32\zip.exe"
Invoke-WebRequest -Uri "http://stahlworks.com/dev/unzip.exe" -OutFile "$system32\unzip.exe"
- name: Install 7z
uses: milliewalky/setup-7-zip@v2
- name: Installing Java
run: |
curl -s --output ${{ runner.temp }}\java_package.zip -L https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-windows-x64-b1034.51.zip
unzip -q ${{ runner.temp }}\java_package.zip -d ${{ runner.temp }}\jbr
echo "JAVA_HOME=${{ runner.temp }}\jbr\jbrsdk-21.0.7-windows-x64-b1034.51" >> $env:GITHUB_ENV
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
# dist
- run: |
.\gradlew.bat dist --no-daemon
.\gradlew.bat --stop
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-windows-x86-64
path: |
build/distributions/*.zip
build/distributions/*.exe

75
.github/workflows/windows.yml vendored Normal file
View File

@@ -0,0 +1,75 @@
name: Windows
on: [ push, pull_request ]
env:
JBR_MAJOR: 21.0.7
JBR_PATCH: b1038.58
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ windows-11-arm, windows-latest ]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set architecture
id: set-arch
run: |
if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
echo "ARCH=aarch64" >> $env:GITHUB_ENV
} else {
echo "ARCH=x64" >> $env:GITHUB_ENV
}
- name: Install zip
run: |
$system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32"
Invoke-WebRequest -Uri "http://stahlworks.com/dev/zip.exe" -OutFile "$system32\zip.exe"
Invoke-WebRequest -Uri "http://stahlworks.com/dev/unzip.exe" -OutFile "$system32\unzip.exe"
- name: Install 7z
uses: milliewalky/setup-7-zip@v2
- name: Installing Java
run: |
$zipPath = "${{ runner.temp }}\java_package.zip"
$extractDir = "${{ runner.temp }}\jbr"
$url = "https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${{ env.JBR_MAJOR }}-windows-${{ env.ARCH }}-${{ env.JBR_PATCH }}.zip"
curl -s --output $zipPath -L $url
unzip -q $zipPath -d $extractDir
$jbrDir = Get-ChildItem $extractDir | Select-Object -First 1
echo "JAVA_HOME=$($jbrDir.FullName)" >> $env:GITHUB_ENV
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
- name: Compile
run: .\gradlew :check-license && .\gradlew classes -x test
- name: JLink
run: .\gradlew :jar :copy-dependencies :plugins:migration:build :jlink
- name: Package
run: .\gradlew :jpackage && .\gradlew :dist
- name: Stop Gradle
run: .\gradlew.bat --stop
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: termora-windows-${{ runner.arch }}
path: |
build/distributions/*.zip
build/distributions/*.exe

124
README.md
View File

@@ -1,52 +1,100 @@
<div align="center">
<a href="./README.zh_CN.md">🇨🇳 简体中文</a>
<a href="./README.zh_CN.md">简体中文</a>
</div>
# Termora
**Termora** is a terminal emulator and SSH client for Windows, macOS and Linux.
**Termora** is a cross-platform terminal emulator and SSH client, available on **Windows, macOS, and Linux**.
<div align="center">
<img src="./docs/readme.png" alt="termora" />
<img src="docs/readme.png" alt="Readme" />
</div>
**Termora** is developed using [Kotlin/JVM](https://kotlinlang.org) and partially implements the [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) protocol (with ongoing improvements). Its ultimate vision is to achieve full platform support (including Android, iOS, and iPadOS) through [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html).
## Features
- SSH and local terminal support
- Serial port protocol support
- [SFTP](./docs/sftp.png?raw=1) & [Command](./docs/sftp-command.png?raw=1) file transfer support
- Compatible with Windows, macOS, and Linux
- Zmodem protocol support
- SSH port forwarding & Jump hosts
- Support for X11 and SSH-Agent
- Terminal log
- Configuration synchronization via [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
- Macro support (record and replay scripts)
- Keyword highlighting
- Key management
- Broadcast commands to multiple sessions
- [Find Everywhere](./docs/findeverywhere.png?raw=1) quick navigation
- Data encryption
- ...
## Download
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
## Development
It is recommended to use the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) version of the JDK and run the program via `./gradlew :run` to run the program.
The program can be run via `./gradlew dist` to automatically build the local version. On macOS: `dmg`, on Windows: `zip`, on Linux: `tar.gz`.
Termora is developed using [**Kotlin/JVM**](https://kotlinlang.org/) and partially implements the [**XTerm control sequence protocol**](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html). Its long-term goal is to achieve **full platform support** (including Android, iOS, and iPadOS) via [**Kotlin Multiplatform**](https://kotlinlang.org/docs/multiplatform.html).
## LICENSE
## ✨ Features
- 🧬 Cross-platform support
- 🔐 Built-in key manager
- 🖼️ X11 forwarding
- 🧑‍💻 SSH-Agent integration
- 💻 System information display
- 📁 GUI-based SFTP file management
- 📊 Nvidia GPU usage monitoring
- ⚡ Quick command shortcuts
## 🚀 File Transfer
- Direct transfers between server A ↔ B
- Recursive folder support
- Up to **6 concurrent transfer tasks**
<div align="center">
<img src="docs/transfer.png" alt="Transfer" />
</div>
## 📝 File Editing
- Auto-upload after editing and saving
- Rename files and folders
- Quick deletion of large folders (`rm -rf` supported)
- Visual permission editing
- Create new files and folders
<div align="center">
<img src="docs/transfer-edit.png" alt="Transfer Edit" />
</div>
## 💻 Hosts
- Tree-like hierarchical structure, similar to folders
- Assign tags to individual hosts
- Import hosts from other tools
- Open with the transfer tool
<div align="center">
<img src="docs/host.png" alt="Transfer Edit" />
</div>
## 🧩 Plugins
- 🌍 Geo: Display geolocation of hosts
- 🔄 Sync: Sync settings to Gist or WebDAV
- 🗂️ WebDAV: Connect to WebDAV storage
- 📝 Editor: Built-in SFTP file editor
- 📡 SMB: Connect to [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
- ☁️ S3: Connect to S3 object storage
- ☁️ Huawei OBS: Connect to Huawei Cloud OBS
- ☁️ Tencent COS: Connect to Tencent Cloud COS
- ☁️ Alibaba OSS: Connect to Alibaba Cloud OSS
- 👉 [View all plugins...](https://www.termora.app/plugins)
## 📦 Download
- 🧾 [Latest Release](https://github.com/TermoraDev/termora/releases/latest)
- 🍺 **Homebrew**: `brew install --cask termora`
- 🔨 **WinGet**: `winget install termora`
## 🛠️ Development
We recommend using the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK for development.
- Run locally: `./gradlew :run`
## 📄 License
This software is distributed under a dual-license model. You may choose one of the following options:
- AGPL-3.0: Use, distribute, and modify the software under the terms of the [AGPL-3.0](https://opensource.org/license/agpl-v3).
- Proprietary License: For closed-source or proprietary use, please contact the author to obtain a commercial license.
- **AGPL-3.0**: Use, distribute, and modify the software under the terms of the [AGPL-3.0](https://opensource.org/license/agpl-v3).
- **Proprietary License**: For closed-source or proprietary use, please contact the author to obtain a commercial license.

View File

@@ -1,47 +1,98 @@
# Termora
**Termora** 是一终端模拟器和 SSH 客户端,支持 WindowsmacOSLinux。
**Termora** 是一款跨平台终端模拟器和 SSH 客户端,支持 **WindowsmacOSLinux**
<div align="center">
<img src="./docs/readme-zh_CN.png" alt="termora" />
<img src="docs/readme-zh_CN.png" alt="Readme" />
</div>
**Termora**用 [Kotlin/JVM](https://kotlinlang.org/) 开发并实现了 [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) 协议(尚未完全实现),它的最终目标是通过 [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) 实现全平台(含 Android、iOS、iPadOS 等
Termora 使用 [**Kotlin/JVM**](https://kotlinlang.org/) 开发,支持(正在实现中) [**XTerm 控制序列协议**](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)。未来目标是借助 [**Kotlin Multiplatform**](https://kotlinlang.org/docs/multiplatform.html) 实现 **全平台支持**,包括 Android、iOS、iPadOS 等。
## 功能特性
- 支持 SSH 和本地终端
- 支持串口协议
- 支持 [SFTP](./docs/sftp-zh_CN.png?raw=1) & [命令行](./docs/sftp-command.png?raw=1) 文件传输
- 支持 Windows、macOS、Linux 平台
- 支持 Zmodem 协议
- 支持 SSH 端口转发和跳板机
- 支持 X11 和 SSH-Agent
- 终端日志记录
- 支持配置同步到 [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
- 支持宏(录制脚本并回放)
- 支持关键词高亮
- 支持密钥管理器
- 支持将命令发送到多个会话
- 支持 [Find Everywhere](./docs/findeverywhere-zh_CN.png?raw=1) 快速跳转
- 支持数据加密
- ...
## 下载
## ✨ 功能特性
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
- 🧬 跨平台运行
- 🔐 内建密钥管理器
- 🖼️ 支持 X11 转发
- 🧑‍💻 SSH-Agent 集成
- 💻 系统信息展示
- 📁 图形化 SFTP 文件管理
- 📊 Nvidia 显卡使用率查看
- ⚡ 快捷指令支持
## 开发
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) 的 JDK 版本,通过 `./gradlew :run` 即可运行程序。
## 🚀 文件传输
通过 `./gradlew dist` 可以自动构建适用于本机的版本。在 macOS 上是:`dmg`,在 Windows 上是:`zip`,在 Linux 上是:`tar.gz`
- 支持 A ↔ B 服务器间直接传输
- 文件夹递归复制支持
- 最多可同时运行 **6 个传输任务**
## 协议
<div align="center">
<img src="docs/transfer-zh_CN.png" alt="Transfer" />
</div>
本软件采用双重许可模式,您可以选择以下任意一种许可方式:
- AGPL-3.0:根据 [AGPL-3.0](https://opensource.org/license/agpl-v3) 的条款,您可以自由使用、分发和修改本软件。
- 专有许可:如果希望在闭源或专有环境中使用,请联系作者获取许可。
## 📝 文件编辑功能
- 保存后自动上传修改内容
- 文件 / 文件夹 重命名
- 快速删除大文件夹:`rm -rf` 支持
- 可视化更改权限
- 支持新建文件 / 文件夹
<div align="center">
<img src="docs/transfer-edit-zh_CN.png" alt="Transfer Edit" />
</div>
## 💻 主机
- 类似文件夹树形结构
- 给主机添加标签
- 从其它软件导入
- 使用传输工具打开
<div align="center">
<img src="docs/host-zh_CN.png" alt="Transfer Edit" />
</div>
## 🧩 插件
- 🌍 Geo显示主机位置信息
- 🔄 Sync将配置同步至 Gist 或 WebDAV
- 🗂️ WebDAV连接 WebDAV 对象存储
- 📝 Editor内置 SFTP 文件编辑器
- 📡 SMB: 连接 [SMB](https://baike.baidu.com/item/smb/4750512) 文件共享协议
- ☁️ S3连接 S3 对象存储
- ☁️ Huawei OBS连接华为云对象存储
- ☁️ Tencent COS连接腾讯云 COS
- ☁️ Alibaba OSS连接阿里云 OSS
- 👉 [查看所有插件...](https://www.termora.cn/plugins)
## 📦 下载
- 🧾 [Latest release](https://github.com/TermoraDev/termora/releases/latest)
- 🍺 **Homebrew**`brew install --cask termora`
- 🪟 **WinGet**`winget install termora`
## 🛠️ 开发指南
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK 运行环境。
- 本地运行:`./gradlew :run`
## 📄 授权协议
Termora 采用双重许可方式,您可以选择:
- **AGPL-3.0**:自由使用、修改、分发(遵循 [AGPL 条款](https://opensource.org/license/agpl-v3)
- **专有许可**:如需闭源或商业用途,请联系作者获取授权

View File

@@ -2,10 +2,6 @@ annotations
Apache License 2.0
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
BSD 3-Clause "New" or "Revised" License
https://github.com/dheid/colorpicker/blob/main/LICENSE
@@ -18,10 +14,6 @@ commons-codec
Apache License 2.0
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
Apache License 2.0
https://github.com/apache/commons-vfs/blob/master/LICENSE.txt
@@ -126,6 +118,10 @@ kotlin-stdlib
Apache License 2.0
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
kotlin-reflect
Apache License 2.0
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
kotlin-stdlib-jdk7
Apache License 2.0
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
@@ -226,26 +222,6 @@ versioncompare
Apache License 2.0
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
Apache License 2.0
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
@@ -261,3 +237,31 @@ https://github.com/stleary/JSON-java/blob/master/LICENSE
jSerialComm
Apache License 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.8

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.filefilter.FileFilterUtils
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.nio.file.Files
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.Future
@@ -21,10 +23,12 @@ plugins {
group = "app.termora"
version = "1.0.16"
version = rootProject.projectDir.resolve("VERSION").readText().trim()
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
val appVersion = project.version.toString().split("-")[0]
val isDeb = os.isLinux && System.getenv("TERMORA_TYPE") == "deb"
// macOS 签名信息
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
@@ -36,15 +40,16 @@ val macOSNotaryKeychainProfile = System.getenv("TERMORA_MAC_NOTARY_KEYCHAIN_PROF
val macOSNotary = macOSSign && macOSNotaryKeychainProfile.isNotBlank()
&& System.getenv("TERMORA_MAC_NOTARY").toBoolean()
repositories {
allprojects {
repositories {
mavenCentral()
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies")
maven("https://www.jitpack.io")
maven("https://central.sonatype.com/repository/maven-snapshots")
}
}
dependencies {
// 由于签名和公证macOS 不携带 natives
val useNoNativesFlatLaf = os.isMacOsX && System.getenv("ENABLE_BUILD").toBoolean()
testImplementation(kotlin("test"))
testImplementation(libs.hutool)
@@ -54,9 +59,10 @@ dependencies {
testImplementation(libs.delight.rhino.sandbox)
testImplementation(platform(libs.testcontainers.bom))
testImplementation(libs.testcontainers)
testImplementation(libs.h2)
testImplementation(libs.exposed.migration)
// implementation(platform(libs.koin.bom))
// implementation(libs.koin.core)
api(kotlin("reflect"))
api(libs.slf4j.api)
api(libs.pty4j)
api(libs.slf4j.tinylog)
@@ -67,28 +73,12 @@ dependencies {
api(libs.commons.csv)
api(libs.commons.net)
api(libs.commons.text)
api(libs.commons.compress)
api(libs.commons.vfs2) { exclude(group = "*", module = "*") }
api(libs.kotlinx.coroutines.swing)
api(libs.kotlinx.coroutines.core)
api(libs.flatlaf) {
artifact {
if (useNoNativesFlatLaf) {
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.flatlaf)
api(libs.flatlafextras)
api(libs.flatlafswingx)
api(libs.kotlinx.serialization.json)
api(libs.swingx)
@@ -109,24 +99,26 @@ dependencies {
api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
api(libs.eddsa)
api(libs.jnafilechooser)
api(libs.xodus.vfs)
api(libs.xodus.openAPI)
api(libs.xodus.environment)
api(libs.bip39)
api(libs.colorpicker)
api(libs.mixpanel)
api(libs.jSerialComm)
api(libs.ini4j)
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 {
val args = mutableListOf(
"-Xmx2g",
"-XX:+UseZGC",
"-XX:+ZUncommit",
"-XX:+ZGenerational",
"-XX:ZUncommitDelay=60",
"-Xmx2048m",
"-Drelease-date=${DateFormatUtils.format(Date(), "yyyy-MM-dd")}",
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
)
if (os.isMacOsX) {
@@ -139,11 +131,7 @@ application {
args.add("-Dapple.awt.application.appearance=system")
}
args.add("-Dapp-version=${project.version}")
if (os.isLinux) {
args.add("-Dsun.java2d.opengl=true")
}
args.add("-DTERMORA_PLUGIN_DIRECTORY=${layout.buildDirectory.get().asFile.absolutePath}${File.separator}plugins")
applicationDefaultJvmArgs = args
mainClass = "app.termora.MainKt"
@@ -153,6 +141,7 @@ publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
pom {
name = project.name
description = "Termora is a terminal emulator and SSH client for Windows, macOS and Linux"
@@ -189,12 +178,14 @@ tasks.register<Copy>("copy-dependencies") {
from(configurations.runtimeClasspath).into(dir)
val jna = libs.jna.asProvider().get()
val pty4j = libs.pty4j.get()
val flatlaf = libs.flatlaf.get()
val jSerialComm = libs.jSerialComm.get()
val restart4j = libs.restart4j.get()
val sqlite = libs.sqlite.get()
// 对 JNA 和 PTY4J 的本地库提取
// 提取出来是为了单独签名,不然无法通过公证
if (os.isMacOsX && macOSSign) {
if (os.isMacOsX) {
doLast {
val archName = if (arch.isArm) "aarch64" else "x86_64"
val dylib = dir.get().dir("dylib").asFile
@@ -254,6 +245,22 @@ tasks.register<Copy>("copy-dependencies") {
)) {
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 +337,48 @@ tasks.register<Copy>("copy-dependencies") {
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 +392,7 @@ tasks.register<Exec>("jlink") {
"java.logging",
"java.management",
"java.rmi",
"java.sql",
"java.security.jgss",
"jdk.crypto.ec",
"jdk.unsupported",
@@ -368,38 +418,38 @@ tasks.register<Exec>("jpackage") {
val buildDir = layout.buildDirectory.get()
val options = mutableListOf(
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
"-Xmx2g",
"-XX:+UseZGC",
"-XX:+ZUncommit",
"-XX:+ZGenerational",
"-XX:ZUncommitDelay=60",
"-Xmx2048m",
"-XX:+HeapDumpOnOutOfMemoryError",
"-Dlogger.console.level=off",
"-Dkotlinx.coroutines.debug=off",
"-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")
if (os.isMacOsX) {
// NSWindow
options.add("-Dapple.awt.application.appearance=system")
options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
options.add("--add-opens java.desktop/sun.font=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("-Dapple.awt.application.appearance=system")
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
options.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
}
if (os.isLinux) {
options.add("-Dsun.java2d.opengl=true")
if (isDeb) {
options.add("-Djpackage.app-layout=deb")
}
}
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage")
arguments.addAll(listOf("--runtime-image", "${buildDir}/jlink"))
arguments.addAll(listOf("--name", project.name.uppercaseFirstChar()))
arguments.addAll(listOf("--app-version", "${project.version}"))
arguments.addAll(listOf("--app-version", appVersion))
arguments.addAll(listOf("--main-jar", tasks.jar.get().archiveFileName.get()))
arguments.addAll(listOf("--main-class", application.mainClass.get()))
arguments.addAll(listOf("--input", "$buildDir/libs"))
@@ -408,6 +458,7 @@ tasks.register<Exec>("jpackage") {
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
arguments.addAll(listOf("--vendor", "TermoraDev"))
arguments.addAll(listOf("--copyright", "TermoraDev"))
arguments.addAll(listOf("--app-content", "$buildDir/plugins"))
if (os.isWindows) {
arguments.addAll(
@@ -429,10 +480,6 @@ tasks.register<Exec>("jpackage") {
}
if (os.isWindows) {
arguments.add("--win-dir-chooser")
arguments.add("--win-shortcut")
arguments.add("--win-shortcut-prompt")
arguments.addAll(listOf("--win-upgrade-uuid", "E1D93CAD-5BF8-442E-93BA-6E90DE601E4C"))
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
}
@@ -445,9 +492,13 @@ tasks.register<Exec>("jpackage") {
if (os.isMacOsX) {
arguments.add("dmg")
} else if (os.isWindows) {
arguments.add("msi")
} else if (os.isLinux) {
arguments.add("app-image")
} else if (os.isLinux) {
arguments.add(if (isDeb) "deb" else "app-image")
if (isDeb) {
arguments.add("--linux-deb-maintainer")
arguments.add("support@termora.app")
}
} else {
throw UnsupportedOperationException()
}
@@ -464,29 +515,20 @@ tasks.register<Exec>("jpackage") {
tasks.register("dist") {
doLast {
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
val distributionDir = layout.buildDirectory.dir("distributions").get()
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
val projectName = project.name.uppercaseFirstChar()
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
// 清空目录
exec { commandLine(gradlew, "clean") }
// 打包并复制依赖
exec {
commandLine(gradlew, "jar", "copy-dependencies")
environment("ENABLE_BUILD" to true)
if (os.isWindows) {
packOnWindows(distributionDir, finalFilenameWithoutExtension, projectName)
} else if (os.isLinux) {
packOnLinux(distributionDir, finalFilenameWithoutExtension, projectName)
} else if (os.isMacOsX) {
packOnMac(distributionDir, finalFilenameWithoutExtension, projectName)
} else {
throw GradleException("${os.name} is not supported")
}
// 检查依赖的开源协议
exec { commandLine(gradlew, "check-license") }
// jlink
exec { commandLine(gradlew, "jlink") }
// 打包
exec { commandLine(gradlew, "jpackage") }
// 根据不同的系统构建不同的二进制包
pack()
}
}
@@ -517,65 +559,42 @@ tasks.register("check-license") {
}
}
/**
* 构建包
*/
fun pack() {
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
val distributionDir = layout.buildDirectory.dir("distributions").get()
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
val projectName = project.name.uppercaseFirstChar()
if (os.isWindows) {
packOnWindows(distributionDir, finalFilenameWithoutExtension, projectName)
} else if (os.isLinux) {
packOnLinux(distributionDir, finalFilenameWithoutExtension, projectName)
} else if (os.isMacOsX) {
packOnMac(distributionDir, finalFilenameWithoutExtension, projectName)
} else {
throw GradleException("${os.name} is not supported")
}
}
/**
* 创建 zip、7z、msi
* 创建 zip、msi
*/
fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
val dir = layout.buildDirectory.dir("distributions").get().asFile
val cfg = FileUtils.getFile(dir, projectName, "app", "${projectName}.cfg")
val configText = cfg.readText()
// zip
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=zip").toString())
exec {
commandLine(
"tar", "-vacf",
distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath,
projectName
)
workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
workingDir = dir
}
// exe
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=exe").toString())
exec {
commandLine(
"iscc",
"/DMyAppId=${projectName}",
"/DMyAppName=${projectName}",
"/DMyAppVersion=${project.version}",
"/DMyAppVersion=${appVersion}",
"/DMyOutputDir=${distributionDir.asFile.absolutePath}",
"/DMySetupIconFile=${FileUtils.getFile(projectDir, "src", "main", "resources", "icons", "termora.ico")}",
"/DMySourceDir=${layout.buildDirectory.dir("jpackage/images/win-msi.image/${projectName}").get().asFile}",
"/DMySourceDir=${FileUtils.getFile(dir, projectName).absolutePath}",
"/F${finalFilenameWithoutExtension}",
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
)
}
// msi
exec {
commandLine(
"cmd", "/c", "move",
"${projectName}-${project.version}.msi",
"${finalFilenameWithoutExtension}.msi"
)
workingDir = distributionDir.asFile
}
}
/**
@@ -587,11 +606,11 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
// rename
// @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
// sign dmg
if (macOSSign) signMacOSLocalFile(dmgFile)
signMacOSLocalFile(dmgFile)
// 找到 .app
val imageFile = layout.buildDirectory.dir("jpackage/images/").get().asFile
@@ -604,7 +623,7 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
// @formatter:on
// sign zip
if (macOSSign) signMacOSLocalFile(zipFile)
signMacOSLocalFile(zipFile)
// 公证
if (macOSNotary) {
@@ -648,7 +667,19 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
* 创建 tar.gz 和 AppImage
*/
fun packOnLinux(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
if (isDeb) {
val arch = if (arch.isArm) "arm" else "amd"
distributionDir.file("${project.name}_${appVersion}_${arch}64.deb").asFile
.renameTo(distributionDir.file("${finalFilenameWithoutExtension}.deb").asFile)
return
}
val cfg = FileUtils.getFile(distributionDir.asFile, projectName, "lib", "app", "${projectName}.cfg")
val configText = cfg.readText()
// tar.gz
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=tar.gz").toString())
exec {
commandLine(
"tar", "-czvf",
@@ -703,6 +734,7 @@ Terminal=false
appRun.setExecutable(true)
// AppImage
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=AppImage").toString())
exec {
commandLine(appimagetool.absolutePath, termoraName, "${finalFilenameWithoutExtension}.AppImage")
workingDir = distributionDir.asFile
@@ -769,6 +801,10 @@ kotlin {
}
}
java {
withSourcesJar()
}
idea {
module {
isDownloadJavadoc = true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

BIN
docs/host-zh_CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
docs/host.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
docs/plugins-zh_CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
docs/plugins.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/tags-zh_CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
docs/tags.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
docs/transfer-edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
docs/transfer-zh_CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

BIN
docs/transfer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -1,18 +1,18 @@
[versions]
kotlin = "2.1.21"
kotlin = "2.2.0"
slf4j = "2.0.17"
pty4j = "0.13.6"
tinylog = "2.7.0"
kotlinx-coroutines = "1.10.2"
flatlaf = "3.6"
kotlinx-serialization-json = "1.8.1"
flatlaf = "3.6.1-SNAPSHOT"
kotlinx-serialization-json = "1.9.0"
commons-codec = "1.18.0"
commons-lang3 = "3.17.0"
commons-lang3 = "3.18.0"
commons-csv = "1.14.0"
commons-net = "3.11.1"
commons-text = "1.13.1"
commons-compress = "1.27.1"
commons-vfs2="2.10.0"
commons-vfs2 = "2.10.0"
swingx = "1.6.5-1"
jgoodies-forms = "1.9.0"
jfa = "1.2.0"
@@ -22,25 +22,32 @@ jna = "5.17.0"
jSystemThemeDetector = "3.9.1"
commons-io = "2.19.0"
jbr-api = "17.1.10.1"
hutool = "5.8.37"
jsch = "0.2.26"
okhttp = "4.12.0"
hutool = "5.8.39"
jsch = "2.27.2"
okhttp = "5.1.0"
sshj = "0.39.0"
sshd-core = "2.15.0"
jgit = "7.2.0.202503040940-r"
commonmark = "0.24.0"
commonmark = "0.25.0"
jnafilechooser = "1.1.2"
xodus = "2.0.1"
bip39 = "1.0.9"
colorpicker = "2.0.1"
rhino = "1.8.0"
delight-rhino-sandbox = "0.0.17"
testcontainers = "1.21.1"
testcontainers = "1.21.3"
mixpanel = "1.5.3"
jSerialComm = "2.11.0"
jSerialComm = "2.11.2"
ini4j = "0.5.5-2"
restart4j = "0.0.1"
eddsa = "0.3.0"
exposed = "1.0.0-beta-4"
h2 = "2.3.232"
sqlite = "3.50.2.0"
jug = "5.1.0"
semver4j = "6.0.0"
jsvg = "2.0.0"
dom4j = "2.2.0"
[libraries]
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" }
ini4j = { module = "org.jetbrains.intellij.deps:ini4j", version.ref = "ini4j" }
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 = { module = "org.testcontainers:testcontainers" }
testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter" }
swingx = { module = "org.swinglabs.swingx:swingx-all", version.ref = "swingx" }
jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" }
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" }
restart4j = { module = "com.github.hstyi:restart4j", version.ref = "restart4j" }
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" }
jsch = { module = "com.github.mwiede:jsch", version.ref = "jsch" }
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" }
jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "jSerialComm" }
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]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
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
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.

79
plugins/THIRDPARTY Normal file
View File

@@ -0,0 +1,79 @@
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/
smbj
Apache License, Version 2.0
https://github.com/hierynomus/smbj/blob/master/LICENSE_HEADER

View File

@@ -0,0 +1,16 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.5"
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,26 @@
package app.termora.plugins.bg
import app.termora.GlassPaneExtension
import app.termora.WindowScope
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(scope: WindowScope, c: JComponent, g2d: Graphics2D) {
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
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)
}
}

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,164 @@
package app.termora.plugins.bg
import app.termora.*
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
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,149 @@
package app.termora.plugins.bg
import app.termora.*
import app.termora.OptionsPane.Companion.FORM_MARGIN
import com.formdev.flatlaf.extras.components.FlatButton
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.3"
dependencies {
testImplementation(kotlin("test"))
implementation("com.qcloud:cos_api:5.6.247")
compileOnly(project(":"))
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,69 @@
package app.termora.plugins.cos
import app.termora.AuthenticationType
import app.termora.Proxy
import app.termora.ProxyType
import com.qcloud.cos.COSClient
import com.qcloud.cos.ClientConfig
import com.qcloud.cos.auth.BasicCOSCredentials
import com.qcloud.cos.model.Bucket
import com.qcloud.cos.region.Region
import java.io.Closeable
import java.util.concurrent.atomic.AtomicBoolean
class COSClientHandler(
private val cred: BasicCOSCredentials,
private val proxy: Proxy,
val buckets: List<Bucket>
) : Closeable {
companion object {
fun createCOSClient(cred: BasicCOSCredentials, region: String, proxy: Proxy): COSClient {
val clientConfig = ClientConfig()
if (region.isNotBlank()) {
clientConfig.region = Region(region)
}
clientConfig.isPrintShutdownStackTrace = false
if (proxy.type == ProxyType.HTTP) {
clientConfig.httpProxyIp = proxy.host
clientConfig.httpProxyPort = proxy.port
if (proxy.authenticationType == AuthenticationType.Password) {
clientConfig.proxyPassword = proxy.password
clientConfig.proxyUsername = proxy.username
}
}
return COSClient(cred, clientConfig)
}
}
/**
* key: Region
* value: Client
*/
private val clients = mutableMapOf<String, COSClient>()
private val closed = AtomicBoolean(false)
fun getClientForBucket(bucket: String): COSClient {
if (closed.get()) throw IllegalStateException("Client already closed")
synchronized(this) {
val bucket = buckets.first { it.name == bucket }
if (clients.containsKey(bucket.location)) {
return clients.getValue(bucket.location)
}
clients[bucket.location] = createCOSClient(cred, bucket.location, proxy)
return clients.getValue(bucket.location)
}
}
override fun close() {
if (closed.compareAndSet(false, true)) {
synchronized(this) {
clients.forEach { it.value.shutdown() }
clients.clear()
}
}
}
}

View File

@@ -0,0 +1,16 @@
package app.termora.plugins.cos
import app.termora.transfer.s3.S3FileSystem
import org.apache.commons.io.IOUtils
/**
* key: region
*/
class COSFileSystem(private val clientHandler: COSClientHandler) :
S3FileSystem(COSFileSystemProvider(clientHandler)) {
override fun close() {
IOUtils.closeQuietly(clientHandler)
super.close()
}
}

View File

@@ -0,0 +1,142 @@
package app.termora.plugins.cos
import app.termora.transfer.s3.S3FileAttributes
import app.termora.transfer.s3.S3FileSystemProvider
import app.termora.transfer.s3.S3Path
import com.qcloud.cos.model.ListObjectsRequest
import com.qcloud.cos.model.ObjectMetadata
import com.qcloud.cos.model.PutObjectRequest
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import java.io.InputStream
import java.io.OutputStream
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.nio.file.AccessMode
import java.nio.file.NoSuchFileException
import java.util.concurrent.atomic.AtomicReference
import kotlin.io.path.absolutePathString
class COSFileSystemProvider(private val clientHandler: COSClientHandler) : S3FileSystemProvider() {
override fun getScheme(): String? {
return "cos"
}
override fun getOutputStream(path: S3Path): OutputStream {
return createStreamer(path)
}
override fun getInputStream(path: S3Path): InputStream {
val client = clientHandler.getClientForBucket(path.bucketName)
return client.getObject(path.bucketName, path.objectName).objectContent
}
private fun createStreamer(path: S3Path): OutputStream {
val pis = PipedInputStream()
val pos = PipedOutputStream(pis)
val exception = AtomicReference<Throwable>()
val thread = Thread.ofVirtual().start {
try {
val client = clientHandler.getClientForBucket(path.bucketName)
client.putObject(PutObjectRequest(path.bucketName, path.objectName, pis, ObjectMetadata()))
} catch (e: Exception) {
exception.set(e)
} finally {
IOUtils.closeQuietly(pis)
}
}
return object : OutputStream() {
override fun write(b: Int) {
val exception = exception.get()
if (exception != null) throw exception
pos.write(b)
}
override fun close() {
pos.close()
if (thread.isAlive) thread.join()
}
}
}
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
val paths = mutableListOf<S3Path>()
// root
if (path.isRoot) {
for (bucket in clientHandler.buckets) {
val p = path.resolve(bucket.name)
p.attributes = S3FileAttributes(
directory = true,
lastModifiedTime = bucket.creationDate.toInstant().toEpochMilli()
)
paths.add(p)
}
return paths
}
var nextMarker = StringUtils.EMPTY
val maxKeys = 100
val bucketName = path.bucketName
while (true) {
val request = ListObjectsRequest()
.withBucketName(bucketName)
.withMaxKeys(maxKeys)
.withDelimiter(path.fileSystem.separator)
if (path.objectName.isNotBlank()) request.withPrefix(path.objectName + path.fileSystem.separator)
if (nextMarker.isNotBlank()) request.withMarker(nextMarker)
val objectListing = clientHandler.getClientForBucket(bucketName).listObjects(request)
for (e in objectListing.commonPrefixes) {
val p = path.bucket.resolve(e)
p.attributes = p.attributes.copy(directory = true)
delete(p)
paths.add(p)
}
for (e in objectListing.objectSummaries) {
val p = path.bucket.resolve(e.key)
p.attributes = p.attributes.copy(
regularFile = true, size = e.size,
lastModifiedTime = e.lastModified.time
)
paths.add(p)
}
if (objectListing.isTruncated.not()) {
break
}
nextMarker = objectListing.nextMarker
}
paths.addAll(directories[path.absolutePathString()] ?: emptyList())
return paths
}
override fun delete(path: S3Path, isDirectory: Boolean) {
if (isDirectory.not())
clientHandler.getClientForBucket(path.bucketName).deleteObject(path.bucketName, path.objectName)
}
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
try {
val client = clientHandler.getClientForBucket(path.bucketName)
if (client.doesObjectExist(path.bucketName, path.objectName).not()) {
throw NoSuchFileException(path.objectName)
}
} catch (e: Exception) {
if (e is NoSuchFileException) throw e
throw NoSuchFileException(e.message)
}
}
}

View File

@@ -0,0 +1,313 @@
package app.termora.plugins.cos
import app.termora.*
import app.termora.plugin.internal.BasicProxyOption
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.ui.FlatTextBorder
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import org.apache.commons.lang3.StringUtils
import java.awt.BorderLayout
import java.awt.KeyboardFocusManager
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.*
class COSHostOptionsPane : OptionsPane() {
private val generalOption = GeneralOption()
private val proxyOption = BasicProxyOption(listOf(ProxyType.HTTP))
private val sftpOption = SFTPOption()
init {
addOption(generalOption)
addOption(proxyOption)
addOption(sftpOption)
}
fun getHost(): Host {
val name = generalOption.nameTextField.text
val protocol = COSProtocolProvider.PROTOCOL
val port = 0
var authentication = Authentication.Companion.No
var proxy = Proxy.Companion.No
val authenticationType = AuthenticationType.Password
authentication = authentication.copy(
type = authenticationType,
password = String(generalOption.passwordTextField.password)
)
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
proxy = proxy.copy(
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
host = proxyOption.proxyHostTextField.text,
username = proxyOption.proxyUsernameTextField.text,
password = String(proxyOption.proxyPasswordTextField.password),
port = proxyOption.proxyPortTextField.value as Int,
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
)
}
val options = Options.Default.copy(
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
extras = mutableMapOf(
"cos.delimiter" to generalOption.delimiterTextField.text,
)
)
return Host(
name = name,
protocol = protocol,
port = port,
username = generalOption.usernameTextField.text,
authentication = authentication,
proxy = proxy,
sort = System.currentTimeMillis(),
remark = generalOption.remarkTextArea.text,
options = options,
)
}
fun setHost(host: Host) {
generalOption.nameTextField.text = host.name
generalOption.usernameTextField.text = host.username
generalOption.remarkTextArea.text = host.remark
generalOption.passwordTextField.text = host.authentication.password
generalOption.delimiterTextField.text = host.options.extras["cos.delimiter"] ?: StringUtils.EMPTY
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
proxyOption.proxyHostTextField.text = host.proxy.host
proxyOption.proxyPasswordTextField.text = host.proxy.password
proxyOption.proxyUsernameTextField.text = host.proxy.username
proxyOption.proxyPortTextField.value = host.proxy.port
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
}
fun validateFields(): Boolean {
val host = getHost()
// general
if (validateField(generalOption.nameTextField)) {
return false
}
if (validateField(generalOption.usernameTextField)) {
return false
}
if (host.authentication.type == AuthenticationType.Password) {
if (validateField(generalOption.passwordTextField)) {
return false
}
}
// proxy
if (host.proxy.type != ProxyType.No) {
if (validateField(proxyOption.proxyHostTextField)
) {
return false
}
if (host.proxy.authenticationType != AuthenticationType.No) {
if (validateField(proxyOption.proxyUsernameTextField)
|| validateField(proxyOption.proxyPasswordTextField)
) {
return false
}
}
}
return true
}
/**
* 返回 true 表示有错误
*/
private fun validateField(textField: JTextField): Boolean {
if (textField.isEnabled && textField.text.isBlank()) {
setOutlineError(textField)
return true
}
return false
}
private fun setOutlineError(c: JComponent) {
selectOptionJComponent(c)
c.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
c.requestFocusInWindow()
}
private inner class GeneralOption : JPanel(BorderLayout()), Option {
val nameTextField = OutlineTextField(128)
val usernameTextField = OutlineTextField(128)
val passwordTextField = OutlinePasswordField(255)
val remarkTextArea = FixedLengthTextArea(512)
// val regionComboBox = OutlineComboBox<String>()
val delimiterTextField = OutlineTextField(128)
init {
initView()
initEvents()
}
private fun initView() {
/*regionComboBox.addItem("ap-beijing-1")
regionComboBox.addItem("ap-beijing")
regionComboBox.addItem("ap-nanjing")
regionComboBox.addItem("ap-shanghai")
regionComboBox.addItem("ap-guangzhou")
regionComboBox.addItem("ap-chengdu")
regionComboBox.addItem("ap-chongqing")
regionComboBox.addItem("ap-shenzhen-fsi")
regionComboBox.addItem("ap-shanghai-fsi")
regionComboBox.addItem("ap-beijing-fsi")
regionComboBox.addItem("ap-hongkong")
regionComboBox.addItem("ap-singapore")
regionComboBox.addItem("ap-jakarta")
regionComboBox.addItem("ap-seoul")
regionComboBox.addItem("ap-bangkok")
regionComboBox.addItem("ap-tokyo")
regionComboBox.addItem("na-siliconvalley")
regionComboBox.addItem("na-ashburn")
regionComboBox.addItem("sa-saopaulo")
regionComboBox.addItem("eu-frankfurt")
regionComboBox.isEditable = true*/
delimiterTextField.text = "/"
delimiterTextField.isEditable = false
add(getCenterComponent(), BorderLayout.CENTER)
}
private fun initEvents() {
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
removeComponentListener(this)
}
})
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.settings
}
override fun getTitle(): String {
return I18n.getString("termora.new-host.general")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
remarkTextArea.setFocusTraversalKeys(
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
)
remarkTextArea.setFocusTraversalKeys(
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
)
remarkTextArea.rows = 8
remarkTextArea.lineWrap = true
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
var rows = 1
val step = 2
val panel = FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
.add("SecretId:").xy(1, rows)
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
.add("SecretKey:").xy(1, rows)
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
.add("Delimiter:").xy(1, rows)
.add(delimiterTextField).xyw(3, rows, 5).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
.xyw(3, rows, 5).apply { rows += step }
.build()
return panel
}
}
private inner class SFTPOption : JPanel(BorderLayout()), Option {
val defaultDirectoryField = OutlineTextField(255)
init {
initView()
initEvents()
}
private fun initView() {
add(getCenterComponent(), BorderLayout.CENTER)
}
private fun initEvents() {
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.folder
}
override fun getTitle(): String {
return I18n.getString("termora.transport.sftp")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
var rows = 1
val step = 2
val panel = FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
.build()
return panel
}
}
}

View File

@@ -0,0 +1,33 @@
package app.termora.plugins.cos
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.instance }
support.addExtension(ProtocolHostPanelExtension::class.java) { COSProtocolHostPanelExtension.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,36 @@
package app.termora.plugins.cos
import app.termora.Disposer
import app.termora.Host
import app.termora.protocol.ProtocolHostPanel
import java.awt.BorderLayout
class COSProtocolHostPanel : ProtocolHostPanel() {
private val pane = COSHostOptionsPane()
init {
initView()
initEvents()
}
private fun initView() {
add(pane, BorderLayout.CENTER)
Disposer.register(this, pane)
}
private fun initEvents() {}
override fun getHost(): Host {
return pane.getHost()
}
override fun setHost(host: Host) {
pane.setHost(host)
}
override fun validateFields(): Boolean {
return pane.validateFields()
}
}

View File

@@ -0,0 +1,20 @@
package app.termora.plugins.cos
import app.termora.account.AccountOwner
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.instance
}
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return COSProtocolHostPanel()
}
}

View File

@@ -0,0 +1,52 @@
package app.termora.plugins.cos
import app.termora.DynamicIcon
import app.termora.Icons
import app.termora.protocol.PathHandler
import app.termora.protocol.PathHandlerRequest
import app.termora.protocol.TransferProtocolProvider
import com.qcloud.cos.ClientConfig
import com.qcloud.cos.auth.BasicCOSCredentials
import com.qcloud.cos.model.Bucket
import org.apache.commons.lang3.StringUtils
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 createPathHandler(requester: PathHandlerRequest): PathHandler {
val host = requester.host
val secretId = host.username
val secretKey = host.authentication.password
val cred = BasicCOSCredentials(secretId, secretKey)
val clientConfig = ClientConfig()
clientConfig.isPrintShutdownStackTrace = false
val cosClient = COSClientHandler.createCOSClient(cred, StringUtils.EMPTY, host.proxy)
val buckets: List<Bucket>
try {
buckets = cosClient.listBuckets()
} finally {
cosClient.shutdown()
}
val defaultPath = host.options.sftpDefaultDirectory
val fs = COSFileSystem(COSClientHandler(cred, host.proxy, buckets))
return PathHandler(fs, fs.getPath(defaultPath))
}
}

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.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.6"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation("com.fifesoft:rsyntaxtextarea:3.6.0")
implementation("com.fifesoft:languagesupport:3.4.0")
implementation("com.fifesoft:autocomplete:3.3.2")
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,94 @@
package app.termora.plugins.editor
import app.termora.DialogWrapper
import app.termora.Disposable
import app.termora.Disposer
import app.termora.OptionPane
import java.awt.Dimension
import java.awt.Window
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.io.File
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.JComponent
import javax.swing.JOptionPane
import javax.swing.UIManager
import kotlin.io.path.absolutePathString
import kotlin.io.path.name
class EditorDialog(file: Path, owner: Window, private val myDisposable: Disposable) : DialogWrapper(null) {
private val filename = file.name
private val filepath = File(file.absolutePathString())
private val editorPanel = EditorPanel(this, filepath)
private val disposed = AtomicBoolean()
init {
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
isModal = false
controlsVisible = true
isResizable = true
title = filename
iconImages = owner.iconImages
escapeDispose = false
defaultCloseOperation = DO_NOTHING_ON_CLOSE
initEvents()
setLocationRelativeTo(owner)
init()
}
private fun initEvents() {
addWindowListener(object : WindowAdapter() {
override fun windowClosing(e: WindowEvent?) {
if (disposed.compareAndSet(false, true)) {
doCancelAction()
}
}
})
Disposer.register(myDisposable, object : Disposable {
override fun dispose() {
if (disposed.compareAndSet(false, true)) {
doCancelAction()
}
}
})
Disposer.register(disposable, object : Disposable {
override fun dispose() {
if (disposed.compareAndSet(false, true)) {
Disposer.dispose(myDisposable)
}
}
})
}
override fun doCancelAction() {
if (editorPanel.changes()) {
if (OptionPane.showConfirmDialog(
this,
"文件尚未保存,你确定要退出吗?",
optionType = JOptionPane.OK_CANCEL_OPTION,
) != JOptionPane.OK_OPTION
) {
return
}
}
super.doCancelAction()
}
override fun createCenterPanel(): JComponent {
return editorPanel
}
override fun createSouthPanel(): JComponent? {
return null
}
}

View File

@@ -0,0 +1,334 @@
package app.termora.plugins.editor
import app.termora.DocumentAdaptor
import app.termora.DynamicColor
import app.termora.EnableManager
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 kotlinx.serialization.json.Json
import org.apache.commons.io.FilenameUtils
import org.dom4j.io.OutputFormat
import org.dom4j.io.SAXReader
import org.dom4j.io.XMLWriter
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 org.slf4j.LoggerFactory
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 java.io.StringReader
import java.io.StringWriter
import javax.swing.*
import javax.swing.SwingConstants.VERTICAL
import javax.swing.event.DocumentEvent
import kotlin.math.max
import kotlin.math.min
class EditorPanel(private val window: JDialog, private val file: File) : JPanel(BorderLayout()) {
companion object {
private val log = LoggerFactory.getLogger(EditorPanel::class.java)
}
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 toolbar = 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()
private val softWrapBtn = JToggleButton(Icons.softWrap)
private val scrollUpBtn = JButton(Icons.scrollUp)
private val scrollEndBtn = JButton(Icons.scrollDown)
private val prettyBtn = JButton(Icons.reformatCode)
private val enableManager get() = EnableManager.getInstance()
private val prettyJson = Json {
prettyPrint = true
}
init {
initView()
initEvents()
}
private fun initView() {
textArea.font = textArea.font.deriveFont(DatabaseManager.getInstance().terminal.fontSize.toFloat())
textArea.text = text
textArea.antiAliasingEnabled = true
softWrapBtn.isSelected = enableManager.getFlag("Plugins.editor.softWrap", false)
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
}
// 只有 JSON 才可以格式化
prettyBtn.isVisible = textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_JSON ||
textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_XML
textArea.discardAllEdits()
scrollPane.border = BorderFactory.createMatteBorder(0, 0, 0, 1, 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)
)
toolbar.orientation = VERTICAL
toolbar.add(scrollUpBtn)
toolbar.add(prettyBtn)
toolbar.add(softWrapBtn)
toolbar.add(scrollEndBtn)
val viewPanel = JPanel(BorderLayout())
viewPanel.add(scrollPane, BorderLayout.CENTER)
viewPanel.add(toolbar, BorderLayout.EAST)
viewPanel.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
layeredPane.add(findPanel, JLayeredPane.MODAL_LAYER as Any)
layeredPane.add(viewPanel, 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)
}
})
softWrapBtn.addActionListener {
enableManager.getFlag("Plugins.editor.softWrap", softWrapBtn.isSelected)
textArea.lineWrap = softWrapBtn.isSelected
}
scrollUpBtn.addActionListener { scrollPane.verticalScrollBar.value = 0 }
scrollEndBtn.addActionListener { scrollPane.verticalScrollBar.value = scrollPane.verticalScrollBar.maximum }
textArea.inputMap.put(
KeyStroke.getKeyStroke(KeyEvent.VK_S, toolkit.menuShortcutKeyMaskEx),
"Save"
)
textArea.inputMap.put(
KeyStroke.getKeyStroke(KeyEvent.VK_F, toolkit.menuShortcutKeyMaskEx),
"Find"
)
textArea.inputMap.put(
KeyStroke.getKeyStroke(KeyEvent.VK_F, toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK),
"Format"
)
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("Format", object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
format()
}
})
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) }
prettyBtn.addActionListener(textArea.actionMap.get("Format"))
prevBtn.addActionListener { search(false) }
nextBtn.addActionListener { search(true) }
}
private fun format() {
val vertical = scrollPane.verticalScrollBar.value
val horizontal = scrollPane.horizontalScrollBar.value
val caretPosition = textArea.caretPosition
val c = if (textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_JSON) {
runCatching {
val json = prettyJson.parseToJsonElement(textArea.text)
textArea.text = prettyJson.encodeToString(json)
}.onFailure {
if (log.isErrorEnabled) {
log.error(it.message, it)
}
}
} else if (textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_XML) {
runCatching {
val document = SAXReader().read(StringReader(textArea.text))
val sw = StringWriter()
val writer = XMLWriter(sw, OutputFormat.createPrettyPrint())
writer.write(document)
textArea.text = sw.toString()
}.onFailure {
if (log.isErrorEnabled) {
log.error(it.message, it)
}
}
} else {
null
} ?: return
c.onSuccess {
SwingUtilities.invokeLater {
scrollPane.verticalScrollBar.value = min(
vertical,
scrollPane.verticalScrollBar.maximum
)
scrollPane.horizontalScrollBar.value = min(
horizontal,
scrollPane.horizontalScrollBar.maximum
)
if (caretPosition >= 0 && caretPosition < textArea.document.length) {
textArea.caretPosition = caretPosition
}
}
}
}
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.transfer.TransportEditFileExtension
class EditorPlugin : Plugin {
private val support = ExtensionSupport()
init {
support.addExtension(TransportEditFileExtension::class.java) { MyTransportEditFileExtension.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,20 @@
package app.termora.plugins.editor
import app.termora.Disposable
import app.termora.Disposer
import app.termora.transfer.TransportEditFileExtension
import java.awt.Window
import java.nio.file.Path
import javax.swing.SwingUtilities
class MyTransportEditFileExtension private constructor() : TransportEditFileExtension {
companion object {
val instance = MyTransportEditFileExtension()
}
override fun edit(owner: Window, path: Path): Disposable {
val disposable = Disposer.newDisposable()
SwingUtilities.invokeLater { EditorDialog(path, 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.2"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation("org.apache.commons:commons-pool2:2.12.1")
testImplementation(project(":"))
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,23 @@
package app.termora.plugins.ftp
import app.termora.transfer.s3.S3FileSystem
import app.termora.transfer.s3.S3Path
import org.apache.commons.io.IOUtils
import org.apache.commons.net.ftp.FTPClient
import org.apache.commons.pool2.impl.GenericObjectPool
class FTPFileSystem(private val pool: GenericObjectPool<FTPClient>) : S3FileSystem(FTPSystemProvider(pool)) {
override fun create(root: String?, names: List<String>): S3Path {
val path = FTPPath(this, root, names)
if (names.isEmpty()) {
path.attributes = path.attributes.copy(directory = true)
}
return path
}
override fun close() {
IOUtils.closeQuietly(pool)
super.close()
}
}

View File

@@ -0,0 +1,393 @@
package app.termora.plugins.ftp
import app.termora.*
import app.termora.keymgr.KeyManager
import app.termora.plugin.internal.BasicProxyOption
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatComboBox
import com.formdev.flatlaf.ui.FlatTextBorder
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import org.apache.commons.lang3.StringUtils
import java.awt.BorderLayout
import java.awt.Component
import java.awt.KeyboardFocusManager
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.awt.event.ItemEvent
import java.nio.charset.Charset
import javax.swing.*
class FTPHostOptionsPane : OptionsPane() {
private val generalOption = GeneralOption()
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
private val sftpOption = SFTPOption()
init {
addOption(generalOption)
addOption(proxyOption)
addOption(sftpOption)
}
fun getHost(): Host {
val name = generalOption.nameTextField.text
val protocol = FTPProtocolProvider.PROTOCOL
val port = generalOption.portTextField.value as Int
var authentication = Authentication.Companion.No
var proxy = Proxy.Companion.No
val authenticationType = AuthenticationType.Password
authentication = authentication.copy(
type = authenticationType,
password = String(generalOption.passwordTextField.password)
)
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
proxy = proxy.copy(
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
host = proxyOption.proxyHostTextField.text,
username = proxyOption.proxyUsernameTextField.text,
password = String(proxyOption.proxyPasswordTextField.password),
port = proxyOption.proxyPortTextField.value as Int,
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
)
}
val options = Options.Default.copy(
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
encoding = sftpOption.charsetComboBox.selectedItem as String,
extras = mutableMapOf("passive" to (sftpOption.passiveComboBox.selectedItem as PassiveMode).name)
)
return Host(
name = name,
protocol = protocol,
port = port,
host = generalOption.hostTextField.text,
username = generalOption.usernameTextField.text,
authentication = authentication,
proxy = proxy,
sort = System.currentTimeMillis(),
remark = generalOption.remarkTextArea.text,
options = options,
)
}
fun setHost(host: Host) {
generalOption.nameTextField.text = host.name
generalOption.usernameTextField.text = host.username
generalOption.remarkTextArea.text = host.remark
generalOption.passwordTextField.text = host.authentication.password
generalOption.hostTextField.text = host.host
generalOption.portTextField.value = host.port
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
proxyOption.proxyHostTextField.text = host.proxy.host
proxyOption.proxyPasswordTextField.text = host.proxy.password
proxyOption.proxyUsernameTextField.text = host.proxy.username
proxyOption.proxyPortTextField.value = host.proxy.port
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
val passive = host.options.extras["passive"] ?: PassiveMode.Local.name
sftpOption.charsetComboBox.selectedItem = host.options.encoding
sftpOption.passiveComboBox.selectedItem = runCatching { PassiveMode.valueOf(passive) }
.getOrNull() ?: PassiveMode.Local
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
}
fun validateFields(): Boolean {
val host = getHost()
// general
if (validateField(generalOption.nameTextField)) {
return false
}
if (validateField(generalOption.hostTextField)) {
return false
}
if (StringUtils.isNotBlank(generalOption.usernameTextField.text) || generalOption.passwordTextField.password.isNotEmpty()) {
if (validateField(generalOption.usernameTextField)) {
return false
}
if (validateField(generalOption.passwordTextField)) {
return false
}
}
// proxy
if (host.proxy.type != ProxyType.No) {
if (validateField(proxyOption.proxyHostTextField)
) {
return false
}
if (host.proxy.authenticationType != AuthenticationType.No) {
if (validateField(proxyOption.proxyUsernameTextField)
|| validateField(proxyOption.proxyPasswordTextField)
) {
return false
}
}
}
return true
}
/**
* 返回 true 表示有错误
*/
private fun validateField(textField: JTextField): Boolean {
if (textField.isEnabled && (if (textField is JPasswordField) textField.password.isEmpty() else textField.text.isBlank())) {
setOutlineError(textField)
return true
}
return false
}
private fun setOutlineError(c: JComponent) {
selectOptionJComponent(c)
c.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
c.requestFocusInWindow()
}
inner class GeneralOption : JPanel(BorderLayout()), Option {
val portTextField = PortSpinner(21)
val nameTextField = OutlineTextField(128)
val usernameTextField = OutlineTextField(128)
val hostTextField = OutlineTextField(255)
val passwordTextField = OutlinePasswordField(255)
val publicKeyComboBox = OutlineComboBox<String>()
val remarkTextArea = FixedLengthTextArea(512)
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
init {
initView()
initEvents()
}
private fun initView() {
add(getCenterComponent(), BorderLayout.CENTER)
publicKeyComboBox.isEditable = false
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component {
var text = StringUtils.EMPTY
if (value is String) {
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
}
return super.getListCellRendererComponent(
list,
text,
index,
isSelected,
cellHasFocus
)
}
}
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component {
var text = value?.toString() ?: ""
when (value) {
AuthenticationType.Password -> {
text = "Password"
}
AuthenticationType.PublicKey -> {
text = "Public Key"
}
AuthenticationType.KeyboardInteractive -> {
text = "Keyboard Interactive"
}
}
return super.getListCellRendererComponent(
list,
text,
index,
isSelected,
cellHasFocus
)
}
}
authenticationTypeComboBox.addItem(AuthenticationType.No)
authenticationTypeComboBox.addItem(AuthenticationType.Password)
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
}
private fun initEvents() {
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
removeComponentListener(this)
}
})
authenticationTypeComboBox.addItemListener {
if (it.stateChange == ItemEvent.SELECTED) {
passwordTextField.isEnabled = authenticationTypeComboBox.selectedItem == AuthenticationType.Password
}
}
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.settings
}
override fun getTitle(): String {
return I18n.getString("termora.new-host.general")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
remarkTextArea.setFocusTraversalKeys(
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
)
remarkTextArea.setFocusTraversalKeys(
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
)
remarkTextArea.rows = 8
remarkTextArea.lineWrap = true
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
var rows = 1
val step = 2
val panel = FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
.add(hostTextField).xy(3, rows)
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
.add(portTextField).xy(7, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
.add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
.xyw(3, rows, 5).apply { rows += step }
.build()
return panel
}
}
private inner class SFTPOption : JPanel(BorderLayout()), Option {
val defaultDirectoryField = OutlineTextField(255)
val charsetComboBox = JComboBox<String>()
val passiveComboBox = JComboBox<PassiveMode>()
init {
initView()
initEvents()
}
private fun initView() {
for (e in Charset.availableCharsets()) {
charsetComboBox.addItem(e.key)
}
charsetComboBox.selectedItem = "UTF-8"
passiveComboBox.addItem(PassiveMode.Local)
passiveComboBox.addItem(PassiveMode.Remote)
add(getCenterComponent(), BorderLayout.CENTER)
}
private fun initEvents() {
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.folder
}
override fun getTitle(): String {
return I18n.getString("termora.transport.sftp")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
var rows = 1
val step = 2
val panel = FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
.add(charsetComboBox).xy(3, rows).apply { rows += step }
.add("${FTPI18n.getString("termora.plugins.ftp.passive")}:").xy(1, rows)
.add(passiveComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
.build()
return panel
}
}
enum class PassiveMode {
Local,
Remote,
}
}

View File

@@ -0,0 +1,24 @@
package app.termora.plugins.ftp
import app.termora.I18n
import app.termora.NamedI18n
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.*
object FTPI18n : NamedI18n("i18n/messages") {
private val log = LoggerFactory.getLogger(FTPI18n::class.java)
override fun getLogger(): Logger {
return log
}
override fun getString(key: String): String {
return try {
substitutor.replace(getBundle().getString(key))
} catch (_: MissingResourceException) {
I18n.getString(key)
}
}
}

View File

@@ -0,0 +1,20 @@
package app.termora.plugins.ftp
import app.termora.transfer.s3.S3Path
class FTPPath(fileSystem: FTPFileSystem, root: String?, names: List<String>) : S3Path(fileSystem, root, names) {
override val isBucket: Boolean
get() = false
override val bucketName: String
get() = throw UnsupportedOperationException()
override val objectName: String
get() = throw UnsupportedOperationException()
override fun getCustomType(): String? {
return null
}
}

View File

@@ -0,0 +1,33 @@
package app.termora.plugins.ftp
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,36 @@
package app.termora.plugins.ftp
import app.termora.Disposer
import app.termora.Host
import app.termora.protocol.ProtocolHostPanel
import java.awt.BorderLayout
class FTPProtocolHostPanel : ProtocolHostPanel() {
private val pane = FTPHostOptionsPane()
init {
initView()
initEvents()
}
private fun initView() {
add(pane, BorderLayout.CENTER)
Disposer.register(this, pane)
}
private fun initEvents() {}
override fun getHost(): Host {
return pane.getHost()
}
override fun setHost(host: Host) {
pane.setHost(host)
}
override fun validateFields(): Boolean {
return pane.validateFields()
}
}

View File

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

View File

@@ -0,0 +1,120 @@
package app.termora.plugins.ftp
import app.termora.AuthenticationType
import app.termora.DynamicIcon
import app.termora.Icons
import app.termora.ProxyType
import app.termora.protocol.PathHandler
import app.termora.protocol.PathHandlerRequest
import app.termora.protocol.TransferProtocolProvider
import org.apache.commons.lang3.StringUtils
import org.apache.commons.net.ftp.FTPClient
import org.apache.commons.pool2.BasePooledObjectFactory
import org.apache.commons.pool2.PooledObject
import org.apache.commons.pool2.impl.DefaultPooledObject
import org.apache.commons.pool2.impl.GenericObjectPool
import org.apache.commons.pool2.impl.GenericObjectPoolConfig
import org.slf4j.LoggerFactory
import java.net.InetSocketAddress
import java.net.Proxy
import java.nio.charset.Charset
import java.time.Duration
class FTPProtocolProvider private constructor() : TransferProtocolProvider {
companion object {
private val log = LoggerFactory.getLogger(FTPProtocolProvider::class.java)
val instance = FTPProtocolProvider()
const val PROTOCOL = "FTP"
}
override fun getProtocol(): String {
return PROTOCOL
}
override fun getIcon(width: Int, height: Int): DynamicIcon {
return Icons.ftp
}
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
val host = requester.host
val config = GenericObjectPoolConfig<FTPClient>().apply {
maxTotal = 12
// 与 transfer 最大传输量匹配
maxIdle = 6
minIdle = 1
testOnBorrow = false
testWhileIdle = true
// 检测空闲对象线程每次运行时检测的空闲对象的数量
timeBetweenEvictionRuns = Duration.ofSeconds(30)
// 连接空闲的最小时间,达到此值后空闲链接将会被移除,且保留 minIdle 个空闲连接数
softMinEvictableIdleDuration = Duration.ofSeconds(30)
// 连接的最小空闲时间,达到此值后该空闲连接可能会被移除(还需看是否已达最大空闲连接数)
minEvictableIdleDuration = Duration.ofMinutes(3)
}
val ftpClientPool = GenericObjectPool(object : BasePooledObjectFactory<FTPClient>() {
override fun create(): FTPClient {
val client = FTPClient()
client.charset = Charset.forName(host.options.encoding)
client.controlEncoding = client.charset.name()
client.connect(host.host, host.port)
if (client.isConnected.not()) {
throw IllegalStateException("FTP client is not connected")
}
if (host.proxy.type == ProxyType.HTTP) {
client.proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress(host.proxy.host, host.proxy.port))
} else if (host.proxy.type == ProxyType.SOCKS5) {
client.proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(host.proxy.host, host.proxy.port))
}
val password = if (host.authentication.type == AuthenticationType.Password)
host.authentication.password else StringUtils.EMPTY
if (client.login(host.username, password).not()) {
throw IllegalStateException("Incorrect account or password")
}
if (host.options.extras["passive"] == FTPHostOptionsPane.PassiveMode.Remote.name) {
client.enterRemotePassiveMode()
} else {
client.enterLocalPassiveMode()
}
client.listHiddenFiles = true
return client
}
override fun wrap(obj: FTPClient): PooledObject<FTPClient> {
return DefaultPooledObject(obj)
}
override fun validateObject(p: PooledObject<FTPClient>): Boolean {
val ftp = p.`object`
return ftp.isConnected.not() && ftp.sendNoOp()
}
override fun destroyObject(p: PooledObject<FTPClient>) {
try {
p.`object`.disconnect()
} catch (e: Exception) {
if (log.isWarnEnabled) {
log.warn(e.message, e)
}
}
}
}, config)
val defaultPath = host.options.sftpDefaultDirectory
val fs = FTPFileSystem(ftpClientPool)
return PathHandler(fs, fs.getPath(defaultPath))
}
}

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 = FTPProtocolProviderExtension()
}
override fun getProtocolProvider(): ProtocolProvider {
return FTPProtocolProvider.instance
}
}

View File

@@ -0,0 +1,158 @@
package app.termora.plugins.ftp
import app.termora.transfer.s3.S3FileSystemProvider
import app.termora.transfer.s3.S3Path
import org.apache.commons.io.IOUtils
import org.apache.commons.net.ftp.FTPClient
import org.apache.commons.net.ftp.FTPFile
import org.apache.commons.pool2.impl.GenericObjectPool
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.AccessMode
import java.nio.file.CopyOption
import java.nio.file.NoSuchFileException
import java.nio.file.Path
import java.nio.file.attribute.FileAttribute
import java.nio.file.attribute.PosixFilePermission
import kotlin.io.path.absolutePathString
import kotlin.io.path.exists
class FTPSystemProvider(private val pool: GenericObjectPool<FTPClient>) : S3FileSystemProvider() {
override fun getScheme(): String? {
return "ftp"
}
override fun getOutputStream(path: S3Path): OutputStream {
return createStreamer(path)
}
override fun getInputStream(path: S3Path): InputStream {
val ftp = pool.borrowObject()
val fs = ftp.retrieveFileStream(path.absolutePathString())
return object : InputStream() {
override fun read(): Int {
return fs.read()
}
override fun close() {
IOUtils.closeQuietly(fs)
ftp.completePendingCommand()
pool.returnObject(ftp)
}
}
}
private fun createStreamer(path: S3Path): OutputStream {
val ftp = pool.borrowObject()
val os = ftp.storeFileStream(path.absolutePathString())
return object : OutputStream() {
override fun write(b: Int) {
os.write(b)
}
override fun close() {
IOUtils.closeQuietly(os)
ftp.completePendingCommand()
pool.returnObject(ftp)
}
}
}
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
val paths = mutableListOf<S3Path>()
if (path.exists().not()) {
throw NoSuchFileException(path.absolutePathString())
}
withFtpClient {
val files = it.listFiles(path.absolutePathString())
for (file in files) {
val p = path.resolve(file.name)
p.attributes = p.attributes.copy(
directory = file.isDirectory,
regularFile = file.isFile,
size = file.size,
lastModifiedTime = file.timestamp.timeInMillis,
)
p.attributes.permissions = ftpPermissionsToPosix(file)
paths.add(p)
}
}
return paths
}
private fun ftpPermissionsToPosix(file: FTPFile): Set<PosixFilePermission> {
val perms = mutableSetOf<PosixFilePermission>()
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION))
perms.add(PosixFilePermission.OWNER_READ)
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION))
perms.add(PosixFilePermission.OWNER_WRITE)
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION))
perms.add(PosixFilePermission.OWNER_EXECUTE)
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.READ_PERMISSION))
perms.add(PosixFilePermission.GROUP_READ)
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.WRITE_PERMISSION))
perms.add(PosixFilePermission.GROUP_WRITE)
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.EXECUTE_PERMISSION))
perms.add(PosixFilePermission.GROUP_EXECUTE)
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.READ_PERMISSION))
perms.add(PosixFilePermission.OTHERS_READ)
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.WRITE_PERMISSION))
perms.add(PosixFilePermission.OTHERS_WRITE)
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.EXECUTE_PERMISSION))
perms.add(PosixFilePermission.OTHERS_EXECUTE)
return perms
}
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
withFtpClient { it.mkd(dir.absolutePathString()) }
}
override fun move(source: Path?, target: Path?, vararg options: CopyOption?) {
if (source != null && target != null) {
withFtpClient {
it.rename(source.absolutePathString(), target.absolutePathString())
}
}
}
override fun delete(path: S3Path, isDirectory: Boolean) {
withFtpClient {
if (isDirectory) {
it.rmd(path.absolutePathString())
} else {
it.deleteFile(path.absolutePathString())
}
}
}
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
withFtpClient {
if (it.cwd(path.absolutePathString()) == 250) {
return
}
if (it.listFiles(path.absolutePathString()).isNotEmpty()) {
return
}
}
throw NoSuchFileException(path.absolutePathString())
}
private inline fun <T> withFtpClient(block: (FTPClient) -> T): T {
val client = pool.borrowObject()
return try {
block(client)
} finally {
pool.returnObject(client)
}
}
}

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="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#6C707E"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-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.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#6C707E"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg t="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#CED0D6"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-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.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#CED0D6"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
termora.plugins.ftp.passive=Passive Mode

View File

@@ -0,0 +1 @@
termora.plugins.ftp.passive=被动模式

View File

@@ -0,0 +1 @@
termora.plugins.ftp.passive=被動模式

View File

@@ -0,0 +1,16 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.7"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation("com.maxmind.geoip2:geoip2:4.3.1")
// https://github.com/hstyi/geolite2
implementation("com.github.hstyi:geolite2:v1.0-202507070058")
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -0,0 +1,82 @@
package app.termora.plugins.geo
import app.termora.ApplicationScope
import app.termora.Disposable
import app.termora.geo.GeoLibrary
import com.maxmind.db.CHMCache
import com.maxmind.geoip2.DatabaseReader
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
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 (isInitialized()) return
if (initialized.compareAndSet(false, true)) {
try {
val input = GeoLibrary.getInputStream()
if (input == null) {
throw IllegalStateException("GeoLite2-Country.mmdb not be found")
}
val locale = Locale.getDefault().toString().replace("_", "-")
try {
reader = DatabaseReader.Builder(input)
.locales(listOf(locale, "en"))
.withCache(CHMCache()).build()
} catch (e: Exception) {
throw e
}
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error("Failed to initialize geo database", e)
}
initialized.set(false)
}
}
}
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,43 @@
package app.termora.plugins.geo
import app.termora.EnableManager
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.isSelected = item.isEnabled && enableManager.getFlag(KEY, true)
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,33 @@
package app.termora.plugins.geo
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(SimpleTreeCellRendererExtension::class.java) { GeoSimpleTreeCellRendererExtension.instance }
support.addExtension(HostTreeShowMoreEnableExtension::class.java) { GeoHostTreeShowMoreEnableExtension.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,59 @@
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 com.formdev.flatlaf.util.SystemInfo
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 = if (SystemInfo.isMacOS) "${countryCodeToFlagEmoji(country.isoCode)}${country.name}" else 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 加载中

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