Compare commits
177 Commits
1.0.13
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5050aa37f5 | ||
|
|
53d3d96a06 | ||
|
|
d40b8a4c9c | ||
|
|
728671509c | ||
|
|
b7178a30fb | ||
|
|
939d6a1fd7 | ||
|
|
2986a9cc46 | ||
|
|
f36afaf5d3 | ||
|
|
8cec835583 | ||
|
|
a32838dad6 | ||
|
|
d54671757e | ||
|
|
d1dba56bcd | ||
|
|
919c06779d | ||
|
|
1c90fb4e18 | ||
|
|
c1f1d5185e | ||
|
|
aa4863712d | ||
|
|
247640f2e5 | ||
|
|
5b1f803fa8 | ||
|
|
accf590c17 | ||
|
|
19fbeab817 | ||
|
|
a785ab4680 | ||
|
|
5ee23cb379 | ||
|
|
145d2de001 | ||
|
|
8d3f5fe622 | ||
|
|
9ce4a88041 | ||
|
|
c0ecc9fa7d | ||
|
|
cb33a4468a | ||
|
|
168c4c5c64 | ||
|
|
9916edbd13 | ||
|
|
ab6b6a2127 | ||
|
|
c45f5f4c92 | ||
|
|
92ee2d72f2 | ||
|
|
a4364bcd6a | ||
|
|
d0827c3b0c | ||
|
|
036a04b0b3 | ||
|
|
eee016c643 | ||
|
|
472bf6e81f | ||
|
|
21229e352f | ||
|
|
1138f48a6e | ||
|
|
f044e0480e | ||
|
|
7047f17783 | ||
|
|
9308f15abb | ||
|
|
b2672f11fc | ||
|
|
f92c6586b2 | ||
|
|
69e07a9bd9 | ||
|
|
cdec60fd25 | ||
|
|
7c30933794 | ||
|
|
b892d2fe13 | ||
|
|
91ee463d41 | ||
|
|
169b66334c | ||
|
|
729eb99730 | ||
|
|
b1e62952f5 | ||
|
|
e21e9f9ed9 | ||
|
|
885c0a6337 | ||
|
|
09d837f5b8 | ||
|
|
b1e1f38b50 | ||
|
|
efa9613d26 | ||
|
|
287f6973f0 | ||
|
|
70fc5e3228 | ||
|
|
4bca15dbb0 | ||
|
|
ef2c57bb29 | ||
|
|
1135ecc5a3 | ||
|
|
e1b2e7b4db | ||
|
|
eec9154aeb | ||
|
|
fff2dd89c7 | ||
|
|
54116a4bf5 | ||
|
|
f28e785301 | ||
|
|
39b9bba9cf | ||
|
|
d7120cabe0 | ||
|
|
007318dae3 | ||
|
|
e25bd485ac | ||
|
|
7ba8e177b1 | ||
|
|
17082c5fb8 | ||
|
|
00dfb4ce39 | ||
|
|
cee0c863f8 | ||
|
|
a2a02c0bad | ||
|
|
891688d0ca | ||
|
|
3d47840aa8 | ||
|
|
01d0f9d4bd | ||
|
|
1c8abf9cba | ||
|
|
efd01da6f1 | ||
|
|
c929a794d5 | ||
|
|
85b2f222f4 | ||
|
|
899bd5a356 | ||
|
|
ded16873b0 | ||
|
|
501013ba31 | ||
|
|
76b1a2f4f8 | ||
|
|
e34fea0ecb | ||
|
|
7532546c77 | ||
|
|
187d5be658 | ||
|
|
2bf4d277be | ||
|
|
389c243ada | ||
|
|
bb39178b88 | ||
|
|
e1eab9db06 | ||
|
|
e6a45d25cd | ||
|
|
a64aef24b2 | ||
|
|
c9c8aa8f2a | ||
|
|
d3cfde5238 | ||
|
|
b4e82a4a0e | ||
|
|
40b4848cc8 | ||
|
|
0c4268a194 | ||
|
|
866c823c30 | ||
|
|
6f9cfc3b32 | ||
|
|
3717393ad9 | ||
|
|
10e7681ac2 | ||
|
|
51047e60f4 | ||
|
|
02984bb2a2 | ||
|
|
ea6b2d6a66 | ||
|
|
95bf08b0da | ||
|
|
15131fefd1 | ||
|
|
26a06b1c91 | ||
|
|
3cca89b917 | ||
|
|
d31510eb86 | ||
|
|
feaec3e223 | ||
|
|
97886e019f | ||
|
|
7cb6696bba | ||
|
|
ab017be855 | ||
|
|
6177bbdc68 | ||
|
|
ca484618c7 | ||
|
|
1f68f8a112 | ||
|
|
0cd5670bd3 | ||
|
|
8e9c6bcb68 | ||
|
|
6c1fa0fc53 | ||
|
|
5145cfa8a5 | ||
|
|
87b1a5e315 | ||
|
|
fa59869f2c | ||
|
|
1ae64fe0db | ||
|
|
f8d363836e | ||
|
|
38dccb1d22 | ||
|
|
3e31a89b92 | ||
|
|
d8f892cc02 | ||
|
|
873deb55aa | ||
|
|
c08712d79b | ||
|
|
61bc905727 | ||
|
|
17859be3c5 | ||
|
|
7a24e34695 | ||
|
|
58638eaad8 | ||
|
|
09d2f2d193 | ||
|
|
9121eff8d8 | ||
|
|
8b090b0526 | ||
|
|
15a0d642ff | ||
|
|
dc4333da21 | ||
|
|
184f6d46dc | ||
|
|
68788905fe | ||
|
|
fc46216a3f | ||
|
|
563143645e | ||
|
|
891ccb901b | ||
|
|
928a866fe7 | ||
|
|
ea25b5b46f | ||
|
|
1de10e6129 | ||
|
|
aaf9c2e8d2 | ||
|
|
b8196b5730 | ||
|
|
0a83e8beb4 | ||
|
|
bdf29b27e7 | ||
|
|
96da7eac41 | ||
|
|
71c0751692 | ||
|
|
442f334af2 | ||
|
|
48302a519f | ||
|
|
c00f759f15 | ||
|
|
1736dd909e | ||
|
|
1f01e368dd | ||
|
|
bfba958b7e | ||
|
|
758121b523 | ||
|
|
06e9a89e82 | ||
|
|
0ba6ac3305 | ||
|
|
993f220b8b | ||
|
|
8755c4ad23 | ||
|
|
77cb102dd6 | ||
|
|
89cfb0b451 | ||
|
|
6bdd83f208 | ||
|
|
8f86057dcc | ||
|
|
a7d7ffa2cc | ||
|
|
d51cbeee13 | ||
|
|
deb2a0151e | ||
|
|
e1c4e9312d | ||
|
|
c7233357bd | ||
|
|
eff8d565d0 |
9
.github/workflows/linux-aarch64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
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.6-linux-aarch64-b895.91.tar.gz
|
||||
- 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
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
with:
|
||||
distribution: 'jdkfile'
|
||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||
java-version: '21.0.6'
|
||||
java-version: '21.0.7'
|
||||
architecture: aarch64
|
||||
|
||||
- uses: actions/cache@v4
|
||||
@@ -34,6 +34,11 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||
|
||||
# test build
|
||||
- run: |
|
||||
./gradlew classes -x test --no-daemon
|
||||
./gradlew clean --no-daemon
|
||||
|
||||
# dist
|
||||
- run: |
|
||||
./gradlew dist --no-daemon
|
||||
|
||||
9
.github/workflows/linux-x86-64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
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.6-linux-x64-b895.91.tar.gz
|
||||
- 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
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
with:
|
||||
distribution: 'jdkfile'
|
||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||
java-version: '21.0.6'
|
||||
java-version: '21.0.7'
|
||||
architecture: x64
|
||||
|
||||
- uses: actions/cache@v4
|
||||
@@ -34,6 +34,11 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||
|
||||
# test build
|
||||
- run: |
|
||||
./gradlew classes -x test --no-daemon
|
||||
./gradlew clean --no-daemon
|
||||
|
||||
# dist
|
||||
- run: |
|
||||
./gradlew dist --no-daemon
|
||||
|
||||
9
.github/workflows/osx-aarch64.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
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.6-osx-aarch64-b895.91.tar.gz
|
||||
- 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
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
with:
|
||||
distribution: 'jdkfile'
|
||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||
java-version: '21.0.6'
|
||||
java-version: '21.0.7'
|
||||
architecture: aarch64
|
||||
|
||||
- uses: actions/cache@v4
|
||||
@@ -64,6 +64,11 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||
|
||||
# test build
|
||||
- run: |
|
||||
./gradlew classes -x test --no-daemon
|
||||
./gradlew clean --no-daemon
|
||||
|
||||
# dist
|
||||
- name: Dist
|
||||
env:
|
||||
|
||||
9
.github/workflows/osx-x86-64.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
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.6-osx-x64-b895.91.tar.gz
|
||||
- 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
|
||||
|
||||
# install jdk
|
||||
- name: Installing Java
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
with:
|
||||
distribution: 'jdkfile'
|
||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||
java-version: '21.0.6'
|
||||
java-version: '21.0.7'
|
||||
architecture: x64
|
||||
|
||||
|
||||
@@ -66,6 +66,11 @@ jobs:
|
||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||
|
||||
|
||||
# test build
|
||||
- run: |
|
||||
./gradlew classes -x test --no-daemon
|
||||
./gradlew clean --no-daemon
|
||||
|
||||
# dist
|
||||
- name: Dist
|
||||
env:
|
||||
|
||||
9
.github/workflows/windows-x86-64.yml
vendored
@@ -21,9 +21,9 @@ jobs:
|
||||
|
||||
- name: Installing Java
|
||||
run: |
|
||||
curl -s --output ${{ runner.temp }}\java_package.zip -L https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-windows-x64-b895.91.zip
|
||||
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.6-windows-x64-b895.91" >> $env:GITHUB_ENV
|
||||
echo "JAVA_HOME=${{ runner.temp }}\jbr\jbrsdk-21.0.7-windows-x64-b1034.51" >> $env:GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
@@ -34,6 +34,11 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||
|
||||
# test build
|
||||
- run: |
|
||||
.\gradlew classes -x test --no-daemon
|
||||
.\gradlew clean --no-daemon
|
||||
|
||||
# dist
|
||||
- run: |
|
||||
.\gradlew.bat dist --no-daemon
|
||||
|
||||
2
.github/workflows/winget.yml
vendored
@@ -10,5 +10,5 @@ jobs:
|
||||
if: github.repository == 'TermoraDev/termora'
|
||||
with:
|
||||
identifier: TermoraDev.Termora
|
||||
installers-regex: 'x86-64\.exe$' # Only x86-64.exe files
|
||||
installers-regex: '\.exe$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
126
README.md
@@ -1,52 +1,102 @@
|
||||
<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`
|
||||
- Build for current OS: `./gradlew :dist`
|
||||
|
||||
|
||||
|
||||
## 📄 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.
|
||||
|
||||
115
README.zh_CN.md
@@ -1,47 +1,100 @@
|
||||
# Termora
|
||||
|
||||
**Termora** 是一个终端模拟器和 SSH 客户端,支持 Windows,macOS 和 Linux。
|
||||
**Termora** 是一款跨平台终端模拟器和 SSH 客户端,支持 **Windows、macOS、Linux**。
|
||||
|
||||
<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`
|
||||
- 构建当前系统安装包:`./gradlew :dist`
|
||||
|
||||
|
||||
|
||||
## 📄 授权协议
|
||||
|
||||
Termora 采用双重许可方式,您可以选择:
|
||||
|
||||
- **AGPL-3.0**:自由使用、修改、分发(遵循 [AGPL 条款](https://opensource.org/license/agpl-v3))
|
||||
- **专有许可**:如需闭源或商业用途,请联系作者获取授权
|
||||
|
||||
62
THIRDPARTY
@@ -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
|
||||
@@ -260,4 +236,32 @@ 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
|
||||
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
|
||||
310
build.gradle.kts
@@ -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
|
||||
|
||||
@@ -14,16 +16,19 @@ plugins {
|
||||
java
|
||||
idea
|
||||
application
|
||||
`maven-publish`
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlinx.serialization)
|
||||
}
|
||||
|
||||
|
||||
group = "app.termora"
|
||||
version = "1.0.13"
|
||||
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.getProperty("type") == "deb"
|
||||
|
||||
// macOS 签名信息
|
||||
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
||||
@@ -35,15 +40,16 @@ val macOSNotaryKeychainProfile = System.getenv("TERMORA_MAC_NOTARY_KEYCHAIN_PROF
|
||||
val macOSNotary = macOSSign && macOSNotaryKeychainProfile.isNotBlank()
|
||||
&& System.getenv("TERMORA_MAC_NOTARY").toBoolean()
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies")
|
||||
maven("https://www.jitpack.io")
|
||||
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)
|
||||
@@ -53,79 +59,70 @@ 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)
|
||||
implementation(libs.slf4j.api)
|
||||
implementation(libs.pty4j)
|
||||
implementation(libs.slf4j.tinylog)
|
||||
implementation(libs.tinylog.impl)
|
||||
implementation(libs.commons.codec)
|
||||
implementation(libs.commons.io)
|
||||
implementation(libs.commons.lang3)
|
||||
implementation(libs.commons.csv)
|
||||
implementation(libs.commons.net)
|
||||
implementation(libs.commons.text)
|
||||
implementation(libs.commons.compress)
|
||||
implementation(libs.commons.vfs2) { exclude(group = "*", module = "*") }
|
||||
implementation(libs.kotlinx.coroutines.swing)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
|
||||
implementation(libs.flatlaf) {
|
||||
artifact {
|
||||
if (useNoNativesFlatLaf) {
|
||||
classifier = "no-natives"
|
||||
}
|
||||
}
|
||||
}
|
||||
implementation(libs.flatlaf.extras) {
|
||||
if (useNoNativesFlatLaf) {
|
||||
exclude(group = "com.formdev", module = "flatlaf")
|
||||
}
|
||||
}
|
||||
implementation(libs.flatlaf.swingx) {
|
||||
if (useNoNativesFlatLaf) {
|
||||
exclude(group = "com.formdev", module = "flatlaf")
|
||||
}
|
||||
}
|
||||
api(kotlin("reflect"))
|
||||
api(libs.slf4j.api)
|
||||
api(libs.pty4j)
|
||||
api(libs.slf4j.tinylog)
|
||||
api(libs.tinylog.impl)
|
||||
api(libs.commons.codec)
|
||||
api(libs.commons.io)
|
||||
api(libs.commons.lang3)
|
||||
api(libs.commons.csv)
|
||||
api(libs.commons.net)
|
||||
api(libs.commons.text)
|
||||
api(libs.kotlinx.coroutines.swing)
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.swingx)
|
||||
implementation(libs.jgoodies.forms)
|
||||
implementation(libs.jna)
|
||||
implementation(libs.jna.platform)
|
||||
implementation(libs.versioncompare)
|
||||
implementation(libs.oshi.core)
|
||||
implementation(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
||||
implementation(libs.jfa) { exclude(group = "*", module = "*") }
|
||||
implementation(libs.jbr.api)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.okhttp.logging)
|
||||
implementation(libs.sshd.core)
|
||||
implementation(libs.commonmark)
|
||||
implementation(libs.jgit)
|
||||
implementation(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
||||
implementation(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
||||
implementation(libs.eddsa)
|
||||
implementation(libs.jnafilechooser)
|
||||
implementation(libs.xodus.vfs)
|
||||
implementation(libs.xodus.openAPI)
|
||||
implementation(libs.xodus.environment)
|
||||
implementation(libs.bip39)
|
||||
implementation(libs.colorpicker)
|
||||
implementation(libs.mixpanel)
|
||||
implementation(libs.jSerialComm)
|
||||
implementation(libs.ini4j)
|
||||
implementation(libs.restart4j)
|
||||
api(libs.flatlaf)
|
||||
api(libs.flatlafextras)
|
||||
api(libs.flatlafswingx)
|
||||
|
||||
api(libs.kotlinx.serialization.json)
|
||||
api(libs.swingx)
|
||||
api(libs.jgoodies.forms)
|
||||
api(libs.jna)
|
||||
api(libs.jna.platform)
|
||||
api(libs.versioncompare)
|
||||
api(libs.oshi.core)
|
||||
api(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
||||
api(libs.jfa) { exclude(group = "*", module = "*") }
|
||||
api(libs.jbr.api)
|
||||
api(libs.okhttp)
|
||||
api(libs.okhttp.logging)
|
||||
api(libs.sshd.core)
|
||||
api(libs.commonmark)
|
||||
api(libs.jgit)
|
||||
api(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
||||
api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
||||
api(libs.eddsa)
|
||||
api(libs.jnafilechooser)
|
||||
|
||||
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) {
|
||||
@@ -133,20 +130,49 @@ application {
|
||||
args.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||
args.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||
args.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||
args.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
|
||||
args.add("-Dsun.java2d.metal=true")
|
||||
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"
|
||||
}
|
||||
|
||||
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"
|
||||
url = "https://github.com/TermoraDev/termora"
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name = "AGPL-3.0"
|
||||
url = "https://opensource.org/license/agpl-v3"
|
||||
}
|
||||
}
|
||||
|
||||
developers {
|
||||
developer {
|
||||
name = "hstyi"
|
||||
url = "https://github.com/hstyi"
|
||||
}
|
||||
}
|
||||
|
||||
scm {
|
||||
url = "https://github.com/TermoraDev/termora"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -156,8 +182,10 @@ 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 的本地库提取
|
||||
// 提取出来是为了单独签名,不然无法通过公证
|
||||
@@ -221,6 +249,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/*") }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,6 +341,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*") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,6 +396,7 @@ tasks.register<Exec>("jlink") {
|
||||
"java.logging",
|
||||
"java.management",
|
||||
"java.rmi",
|
||||
"java.sql",
|
||||
"java.security.jgss",
|
||||
"jdk.crypto.ec",
|
||||
"jdk.unsupported",
|
||||
@@ -335,37 +422,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"))
|
||||
@@ -374,6 +462,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(
|
||||
@@ -413,7 +502,11 @@ tasks.register<Exec>("jpackage") {
|
||||
} else if (os.isWindows) {
|
||||
arguments.add("msi")
|
||||
} else if (os.isLinux) {
|
||||
arguments.add("app-image")
|
||||
arguments.add(if (isDeb) "deb" else "app-image")
|
||||
if (isDeb) {
|
||||
arguments.add("--linux-deb-maintainer")
|
||||
arguments.add("support@termora.app")
|
||||
}
|
||||
} else {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
@@ -436,20 +529,22 @@ tasks.register("dist") {
|
||||
// 清空目录
|
||||
exec { commandLine(gradlew, "clean") }
|
||||
|
||||
// 构建自带的插件
|
||||
exec { commandLine(gradlew, ":plugins:migration:build") }
|
||||
|
||||
// 打包并复制依赖
|
||||
exec {
|
||||
commandLine(gradlew, "jar", "copy-dependencies")
|
||||
environment("ENABLE_BUILD" to true)
|
||||
commandLine(gradlew, ":jar", ":copy-dependencies")
|
||||
}
|
||||
|
||||
// 检查依赖的开源协议
|
||||
exec { commandLine(gradlew, "check-license") }
|
||||
exec { commandLine(gradlew, ":check-license") }
|
||||
|
||||
// jlink
|
||||
exec { commandLine(gradlew, "jlink") }
|
||||
exec { commandLine(gradlew, ":jlink") }
|
||||
|
||||
// 打包
|
||||
exec { commandLine(gradlew, "jpackage") }
|
||||
exec { commandLine(gradlew, ":jpackage", "-Dtype=${System.getProperty("type")}") }
|
||||
|
||||
// 根据不同的系统构建不同的二进制包
|
||||
pack()
|
||||
@@ -505,26 +600,32 @@ fun pack() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 zip、7z、msi
|
||||
* 创建 zip、msi
|
||||
*/
|
||||
fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
||||
val dir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").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}",
|
||||
@@ -537,7 +638,7 @@ fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: Str
|
||||
exec {
|
||||
commandLine(
|
||||
"cmd", "/c", "move",
|
||||
"${projectName}-${project.version}.msi",
|
||||
"${projectName}-${appVersion}.msi",
|
||||
"${finalFilenameWithoutExtension}.msi"
|
||||
)
|
||||
workingDir = distributionDir.asFile
|
||||
@@ -553,7 +654,7 @@ 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
|
||||
@@ -614,7 +715,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",
|
||||
@@ -669,6 +782,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
|
||||
@@ -735,6 +849,10 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
idea {
|
||||
module {
|
||||
isDownloadJavadoc = true
|
||||
|
||||
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 61 KiB |
BIN
docs/host-zh_CN.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/host.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
docs/plugins-zh_CN.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/plugins.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 166 KiB |
BIN
docs/readme.png
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 55 KiB |
BIN
docs/sftp.png
|
Before Width: | Height: | Size: 49 KiB |
BIN
docs/tags-zh_CN.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/tags.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
docs/transfer-edit-zh_CN.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
docs/transfer-edit.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/transfer-zh_CN.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
docs/transfer.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
@@ -1,46 +1,53 @@
|
||||
[versions]
|
||||
kotlin = "2.1.20"
|
||||
kotlin = "2.2.0"
|
||||
slf4j = "2.0.17"
|
||||
pty4j = "0.13.3"
|
||||
pty4j = "0.13.6"
|
||||
tinylog = "2.7.0"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
flatlaf = "3.5.4"
|
||||
kotlinx-serialization-json = "1.8.1"
|
||||
flatlaf = "3.6"
|
||||
kotlinx-serialization-json = "1.9.0"
|
||||
commons-codec = "1.18.0"
|
||||
commons-lang3 = "3.17.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"
|
||||
oshi = "6.6.5"
|
||||
oshi = "6.8.1"
|
||||
versioncompare = "1.4.1"
|
||||
jna = "5.17.0"
|
||||
jSystemThemeDetector = "3.9.1"
|
||||
commons-io = "2.18.0"
|
||||
commons-io = "2.19.0"
|
||||
jbr-api = "17.1.10.1"
|
||||
hutool = "5.8.37"
|
||||
jsch = "0.2.25"
|
||||
jsch = "0.2.26"
|
||||
okhttp = "4.12.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.20.6"
|
||||
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-3"
|
||||
h2 = "2.3.232"
|
||||
sqlite = "3.50.2.0"
|
||||
jug = "5.1.0"
|
||||
semver4j = "6.0.0"
|
||||
jsvg = "1.4.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" }
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
@@ -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
@@ -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
|
||||
16
plugins/bg/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.4"
|
||||
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
26
plugins/bg/src/main/kotlin/app/termora/plugins/bg/BGI18n.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package app.termora.plugins.bg
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.database.DatabaseManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Request
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Window
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
internal class BackgroundManager private constructor() : Disposable, GlassPaneAwareExtension,
|
||||
ApplicationRunnerExtension {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(BackgroundManager::class.java)
|
||||
fun getInstance(): BackgroundManager {
|
||||
return ApplicationScope.Companion.forApplicationScope()
|
||||
.getOrCreate(BackgroundManager::class) { BackgroundManager() }
|
||||
}
|
||||
}
|
||||
|
||||
private var bufferedImage: BufferedImage? = null
|
||||
private var imageFilepath = StringUtils.EMPTY
|
||||
private val glassPanes = mutableListOf<WeakReference<JComponent>>()
|
||||
|
||||
|
||||
fun setBackgroundImage(url: String) {
|
||||
clearBackgroundImage()
|
||||
Appearance.backgroundImage = url
|
||||
refreshBackgroundImage()
|
||||
}
|
||||
|
||||
fun getBackgroundImage(): BufferedImage? {
|
||||
val bg = doGetBackgroundImage()
|
||||
if (bg == null) {
|
||||
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||
return null
|
||||
} else {
|
||||
JPopupMenu.setDefaultLightWeightPopupEnabled(true)
|
||||
}
|
||||
} else {
|
||||
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||
JPopupMenu.setDefaultLightWeightPopupEnabled(false)
|
||||
}
|
||||
}
|
||||
return bg
|
||||
}
|
||||
|
||||
private fun doGetBackgroundImage(): BufferedImage? {
|
||||
synchronized(this) {
|
||||
return bufferedImage
|
||||
}
|
||||
}
|
||||
|
||||
fun clearBackgroundImage() {
|
||||
synchronized(this) {
|
||||
bufferedImage = null
|
||||
imageFilepath = StringUtils.EMPTY
|
||||
Appearance.backgroundImage = StringUtils.EMPTY
|
||||
}
|
||||
refreshGlassPanes()
|
||||
}
|
||||
|
||||
private fun refreshBackgroundImage() {
|
||||
val backgroundImage = Appearance.backgroundImage
|
||||
if (backgroundImage.isBlank()) {
|
||||
return
|
||||
}
|
||||
|
||||
var file: File? = null
|
||||
|
||||
// 从网络下载
|
||||
if (backgroundImage.startsWith("http://") || backgroundImage.startsWith("https://")) {
|
||||
file = Application.httpClient.newCall(
|
||||
Request.Builder().get()
|
||||
.url(backgroundImage).build()
|
||||
).execute().use { response ->
|
||||
val tempFile = File(Application.getTemporaryDir(), randomUUID())
|
||||
if (response.isSuccessful.not()) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error("Request {} failed with code {}", backgroundImage, response.code)
|
||||
}
|
||||
return
|
||||
}
|
||||
val body = response.body
|
||||
if (body != null) {
|
||||
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
|
||||
}
|
||||
IOUtils.closeQuietly(body)
|
||||
return@use tempFile
|
||||
}
|
||||
}
|
||||
|
||||
val backgroundImageFile = File(backgroundImage)
|
||||
if (backgroundImageFile.isDirectory) {
|
||||
val files = FileUtils.listFiles(backgroundImageFile, arrayOf("png", "jpg", "jpeg"), false)
|
||||
if (files.isNotEmpty()) {
|
||||
for (i in 0 until files.size) {
|
||||
file = files.randomOrNull()
|
||||
if (file == null) break
|
||||
if (file.absolutePath == imageFilepath) continue
|
||||
}
|
||||
} else {
|
||||
synchronized(this) {
|
||||
imageFilepath = StringUtils.EMPTY
|
||||
bufferedImage = null
|
||||
refreshGlassPanes()
|
||||
}
|
||||
}
|
||||
} else if (backgroundImageFile.isFile) {
|
||||
file = backgroundImageFile
|
||||
}
|
||||
|
||||
if (file == null || imageFilepath == file.absolutePath) {
|
||||
return
|
||||
}
|
||||
|
||||
bufferedImage = file.inputStream().use { ImageIO.read(it) }
|
||||
imageFilepath = file.absolutePath
|
||||
|
||||
refreshGlassPanes()
|
||||
}
|
||||
|
||||
private fun refreshGlassPanes() {
|
||||
SwingUtilities.invokeLater {
|
||||
glassPanes.removeIf {
|
||||
val glassPane = it.get()
|
||||
glassPane?.repaint()
|
||||
glassPane == null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
|
||||
}
|
||||
|
||||
override fun setGlassPane(window: Window, glassPane: JComponent) {
|
||||
glassPanes.add(WeakReference(glassPane))
|
||||
}
|
||||
|
||||
override fun ready() {
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
while (isActive) {
|
||||
runCatching { refreshBackgroundImage() }.onFailure {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error("Refresh failed", it)
|
||||
}
|
||||
}
|
||||
delay(max(Appearance.interval, 30).seconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
23
plugins/bg/src/main/resources/META-INF/plugin.xml
Normal 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>
|
||||
6
plugins/bg/src/main/resources/META-INF/pluginIcon.svg
Normal 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 |
@@ -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 |
2
plugins/bg/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
termora.plugins.bg.interval=Interval
|
||||
termora.plugins.bg.background-image=Background Image
|
||||
@@ -0,0 +1,2 @@
|
||||
termora.plugins.bg.background-image=背景图
|
||||
termora.plugins.bg.interval=切换间隔
|
||||
@@ -0,0 +1,2 @@
|
||||
termora.plugins.bg.background-image=背景圖
|
||||
termora.plugins.bg.interval=切換間隔
|
||||
89
plugins/common.gradle.kts
Normal 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()
|
||||
}
|
||||
}
|
||||
16
plugins/cos/build.gradle.kts
Normal 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")
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
25
plugins/cos/src/main/resources/META-INF/plugin.xml
Normal 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>
|
||||
1
plugins/cos/src/main/resources/META-INF/pluginIcon.svg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
19
plugins/editor/build.gradle.kts
Normal 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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
22
plugins/editor/src/main/resources/META-INF/plugin.xml
Normal 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>
|
||||
@@ -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 |
@@ -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 |
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
15
plugins/ftp/build.gradle.kts
Normal file
@@ -0,0 +1,15 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.1"
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
@@ -1,15 +1,15 @@
|
||||
package app.termora.vfs2.sftp
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import org.apache.commons.vfs2.Capability
|
||||
import org.apache.commons.vfs2.FileName
|
||||
import org.apache.commons.vfs2.FileSystem
|
||||
import org.apache.commons.vfs2.FileSystemOptions
|
||||
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
|
||||
import org.apache.sshd.sftp.client.SftpClientFactory
|
||||
|
||||
class MySftpFileProvider : AbstractOriginatingFileProvider() {
|
||||
class FTPFileProvider private constructor() : AbstractOriginatingFileProvider() {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { FTPFileProvider() }
|
||||
val capabilities = listOf(
|
||||
Capability.CREATE,
|
||||
Capability.DELETE,
|
||||
@@ -27,19 +27,15 @@ class MySftpFileProvider : AbstractOriginatingFileProvider() {
|
||||
}
|
||||
|
||||
override fun getCapabilities(): Collection<Capability> {
|
||||
return MySftpFileProvider.capabilities
|
||||
return FTPFileProvider.capabilities
|
||||
}
|
||||
|
||||
override fun doCreateFileSystem(rootFileName: FileName, fileSystemOptions: FileSystemOptions): FileSystem {
|
||||
val clientSession = MySftpFileSystemConfigBuilder.getInstance()
|
||||
.getClientSession(fileSystemOptions)
|
||||
if (clientSession == null) {
|
||||
throw IllegalArgumentException("client session not found")
|
||||
}
|
||||
return MySftpFileSystem(
|
||||
SftpClientFactory.instance().createSftpFileSystem(clientSession),
|
||||
rootFileName,
|
||||
fileSystemOptions
|
||||
)
|
||||
override fun doCreateFileSystem(
|
||||
rootFileName: FileName,
|
||||
fileSystemOptions: FileSystemOptions
|
||||
): FileSystem? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.PaidPlugin
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class FTPPlugin : PaidPlugin {
|
||||
private val support = ExtensionSupport()
|
||||
|
||||
init {
|
||||
support.addExtension(ProtocolProviderExtension::class.java) { FTPProtocolProviderExtension.Companion.instance }
|
||||
support.addExtension(ProtocolHostPanelExtension::class.java) { FTPProtocolHostPanelExtension.Companion.instance }
|
||||
}
|
||||
|
||||
override fun getAuthor(): String {
|
||||
return "TermoraDev"
|
||||
}
|
||||
|
||||
|
||||
override fun getName(): String {
|
||||
return "FTP"
|
||||
}
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.Host
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
class FTPProtocolHostPanel : ProtocolHostPanel() {
|
||||
override fun getHost(): Host {
|
||||
return Host(
|
||||
name = StringUtils.EMPTY,
|
||||
protocol = FTPProtocolProvider.PROTOCOL
|
||||
)
|
||||
}
|
||||
|
||||
override fun setHost(host: Host) {
|
||||
|
||||
}
|
||||
|
||||
override fun validateFields(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
|
||||
class FTPProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolHostPanelExtension() }
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return FTPProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
return FTPProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.Icons
|
||||
import app.termora.protocol.FileObjectHandler
|
||||
import app.termora.protocol.FileObjectRequest
|
||||
import app.termora.protocol.TransferProtocolProvider
|
||||
import org.apache.commons.vfs2.provider.FileProvider
|
||||
|
||||
class FTPProtocolProvider private constructor() : TransferProtocolProvider {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolProvider() }
|
||||
const val PROTOCOL = "FTP"
|
||||
}
|
||||
|
||||
override fun getProtocol(): String {
|
||||
return PROTOCOL
|
||||
}
|
||||
|
||||
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||
return Icons.ftp
|
||||
}
|
||||
|
||||
override fun getFileProvider(): FileProvider {
|
||||
return FTPFileProvider.instance
|
||||
}
|
||||
|
||||
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolProviderExtension() }
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return FTPProtocolProvider.Companion.instance
|
||||
}
|
||||
}
|
||||
24
plugins/ftp/src/main/resources/META-INF/plugin.xml
Normal 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>
|
||||
1
plugins/ftp/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#6C707E"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#6C707E"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#CED0D6"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#CED0D6"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
16
plugins/geo/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.6"
|
||||
|
||||
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-202506280327")
|
||||
}
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
|
||||
82
plugins/geo/src/main/kotlin/app/termora/plugins/geo/Geo.kt
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
23
plugins/geo/src/main/resources/META-INF/plugin.xml
Normal 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>
|
||||
5
plugins/geo/src/main/resources/META-INF/pluginIcon.svg
Normal 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 |
@@ -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 |
2
plugins/geo/src/main/resources/i18n/messages.properties
Normal 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
|
||||
@@ -0,0 +1,2 @@
|
||||
termora.plugins.geo.first-message=首次使用 <b>Geo</b> 插件会下载 <b>GeoLite2.mmdb</b> 数据库,下载完成后会显示主机地域信息
|
||||
termora.plugins.geo.coming-soon=Geo 加载中
|
||||
@@ -0,0 +1,2 @@
|
||||
termora.plugins.geo.first-message=首次使用 <b>Geo</b> 外掛程式會下載 <b>GeoLite2.mmdb</b> 資料庫,下載完成後會顯示主機地域訊息
|
||||
termora.plugins.geo.coming-soon=Geo 加载中
|
||||
22
plugins/migration/build.gradle.kts
Normal file
@@ -0,0 +1,22 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.2"
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
|
||||
implementation(libs.xodus.vfs)
|
||||
implementation(libs.xodus.openAPI)
|
||||
implementation(libs.xodus.environment)
|
||||
implementation(libs.bip39)
|
||||
implementation(libs.commons.compress)
|
||||
}
|
||||
|
||||
|
||||
ext.set("Termora-Plugin-Entry", "app.termora.plugins.migration.MigrationPlugin")
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
@@ -1,13 +1,12 @@
|
||||
package app.termora
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.highlight.KeywordHighlight
|
||||
import app.termora.keymap.Keymap
|
||||
import app.termora.keymgr.OhKeyPair
|
||||
import app.termora.macro.Macro
|
||||
import app.termora.snippet.Snippet
|
||||
import app.termora.sync.SyncManager
|
||||
import app.termora.sync.SyncType
|
||||
import app.termora.terminal.CursorStyle
|
||||
import jetbrains.exodus.bindings.StringBinding
|
||||
import jetbrains.exodus.env.*
|
||||
@@ -47,7 +46,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
|
||||
fun getDatabase(): Database {
|
||||
return ApplicationScope.forApplicationScope()
|
||||
.getOrCreate(Database::class) { open(Application.getDatabaseFile()) }
|
||||
.getOrCreate(Database::class) { open(MigrationApplicationRunnerExtension.instance.getDatabaseFile()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,18 +288,6 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
val k = StringBinding.stringToEntry(key)
|
||||
val v = StringBinding.stringToEntry(value)
|
||||
store.put(tx, k, v)
|
||||
|
||||
// 数据变动时触发一次同步
|
||||
if (name == HOST_STORE ||
|
||||
name == KEYMAP_STORE ||
|
||||
name == SNIPPET_STORE ||
|
||||
name == KEYWORD_HIGHLIGHT_STORE ||
|
||||
name == MACRO_STORE ||
|
||||
name == KEY_PAIR_STORE ||
|
||||
name == DELETED_DATA_STORE
|
||||
) {
|
||||
SyncManager.getInstance().triggerOnChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private fun delete(tx: Transaction, name: String, key: String) {
|
||||
@@ -356,7 +343,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
}
|
||||
|
||||
|
||||
abstract inner class Property(private val name: String) {
|
||||
abstract inner class Property(val name: String) {
|
||||
private val properties = Collections.synchronizedMap(mutableMapOf<String, String>())
|
||||
|
||||
init {
|
||||
@@ -523,6 +510,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
*/
|
||||
var beep by BooleanPropertyDelegate(true)
|
||||
|
||||
/**
|
||||
* 超链接
|
||||
*/
|
||||
var hyperlink by BooleanPropertyDelegate(true)
|
||||
|
||||
/**
|
||||
* 光标闪烁
|
||||
*/
|
||||
@@ -643,6 +635,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
||||
*/
|
||||
var backgroundRunning by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 标签关闭前确认
|
||||
*/
|
||||
var confirmTabClose by BooleanPropertyDelegate(false)
|
||||
|
||||
/**
|
||||
* 背景图片的地址
|
||||
*/
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.AES.decodeBase64
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
@@ -86,7 +87,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
.layout(
|
||||
FormLayout(
|
||||
"$formMargin, default:grow, 4dlu, pref, $formMargin",
|
||||
"${if (SystemInfo.isWindows) "20dlu" else "0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
||||
"${"0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
||||
)
|
||||
)
|
||||
.add(icon).xyw(2, rows, 4).apply { rows += step }
|
||||
@@ -114,7 +115,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
I18n.getString("termora.doorman.delete-data"),
|
||||
messageType = JOptionPane.WARNING_MESSAGE
|
||||
)
|
||||
Application.browse(Application.getDatabaseFile().toURI())
|
||||
Application.browse(MigrationApplicationRunnerExtension.instance.getDatabaseFile().toURI())
|
||||
}
|
||||
}
|
||||
}).apply { isFocusable = false }).xyw(2, rows, 4, "center, fill")
|
||||
@@ -136,6 +137,9 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
val key = AES.ECB.decrypt(entropy, keyBackup.decodeBase64())
|
||||
Doorman.getInstance().work(key)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
OptionPane.showMessageDialog(
|
||||
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
@@ -219,7 +223,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
)
|
||||
|
||||
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
|
||||
.layout(layout).debug(true)
|
||||
.layout(layout).debug(false)
|
||||
val iterator = textFields.iterator()
|
||||
for (i in 1..5 step 2) {
|
||||
for (j in 1..7 step 2) {
|
||||
@@ -0,0 +1,198 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.account.AccountManager
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.database.OwnerType
|
||||
import app.termora.highlight.KeywordHighlightManager
|
||||
import app.termora.keymap.KeymapManager
|
||||
import app.termora.keymgr.KeyManager
|
||||
import app.termora.macro.MacroManager
|
||||
import app.termora.snippet.SnippetManager
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class MigrationApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(MigrationApplicationRunnerExtension::class.java)
|
||||
val instance by lazy { MigrationApplicationRunnerExtension() }
|
||||
}
|
||||
|
||||
override fun ready() {
|
||||
val file = getDatabaseFile()
|
||||
if (file.exists().not()) return
|
||||
|
||||
// 如果数据库文件存在,那么需要迁移文件
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
|
||||
SwingUtilities.invokeAndWait {
|
||||
try {
|
||||
// 打开数据
|
||||
openDatabase()
|
||||
|
||||
// 尝试解锁
|
||||
openDoor()
|
||||
|
||||
// 询问是否迁移
|
||||
if (askMigrate()) {
|
||||
|
||||
// 迁移
|
||||
migrate()
|
||||
|
||||
// 移动到旧的目录
|
||||
moveOldDirectory()
|
||||
|
||||
// 重启
|
||||
restart()
|
||||
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
} finally {
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
countDownLatch.await()
|
||||
|
||||
}
|
||||
|
||||
private fun openDoor() {
|
||||
if (Doorman.getInstance().isWorking()) {
|
||||
if (DoormanDialog(null).open().not()) {
|
||||
Disposer.dispose(TermoraFrameManager.getInstance())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openDatabase() {
|
||||
try {
|
||||
// 初始化数据库
|
||||
Database.getDatabase()
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
JOptionPane.showMessageDialog(
|
||||
null, "Unable to open database",
|
||||
I18n.getString("termora.title"), JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrate() {
|
||||
val database = Database.getDatabase()
|
||||
val accountManager = AccountManager.getInstance()
|
||||
val databaseManager = DatabaseManager.getInstance()
|
||||
val ownerId = accountManager.getAccountId()
|
||||
val hostManager = HostManager.getInstance()
|
||||
val snippetManager = SnippetManager.getInstance()
|
||||
val macroManager = MacroManager.getInstance()
|
||||
val keymapManager = KeymapManager.getInstance()
|
||||
val keyManager = KeyManager.getInstance()
|
||||
val highlightManager = KeywordHighlightManager.getInstance()
|
||||
val accountOwner = AccountOwner(
|
||||
id = accountManager.getAccountId(),
|
||||
name = accountManager.getEmail(),
|
||||
type = OwnerType.User
|
||||
)
|
||||
|
||||
for (host in database.getHosts()) {
|
||||
if (host.deleted) continue
|
||||
hostManager.addHost(host.copy(ownerId = accountManager.getAccountId(), ownerType = OwnerType.User.name))
|
||||
}
|
||||
|
||||
for (snippet in database.getSnippets()) {
|
||||
if (snippet.deleted) continue
|
||||
snippetManager.addSnippet(snippet)
|
||||
}
|
||||
|
||||
for (macro in database.getMacros()) {
|
||||
macroManager.addMacro(macro)
|
||||
}
|
||||
|
||||
for (keymap in database.getKeymaps()) {
|
||||
keymapManager.addKeymap(keymap)
|
||||
}
|
||||
|
||||
for (keypair in database.getKeyPairs()) {
|
||||
keyManager.addOhKeyPair(keypair, accountOwner)
|
||||
}
|
||||
|
||||
for (e in database.getKeywordHighlights()) {
|
||||
highlightManager.addKeywordHighlight(e, accountOwner)
|
||||
}
|
||||
|
||||
val list = listOf(
|
||||
database.sync,
|
||||
database.properties,
|
||||
database.terminal,
|
||||
database.sftp,
|
||||
database.appearance,
|
||||
)
|
||||
|
||||
for (e in list) {
|
||||
for (k in e.getProperties()) {
|
||||
databaseManager.setSetting(e.name + "." + k.key, k.value)
|
||||
}
|
||||
}
|
||||
|
||||
for (e in database.safetyProperties.getProperties()) {
|
||||
databaseManager.setSetting(database.properties.name + "." + e.key, e.value)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun askMigrate(): Boolean {
|
||||
|
||||
if (MigrationDialog(null).open()) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 移动到旧的目录
|
||||
moveOldDirectory()
|
||||
|
||||
// 重启
|
||||
restart()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun moveOldDirectory() {
|
||||
// 关闭数据库
|
||||
Disposer.dispose(Database.getDatabase())
|
||||
|
||||
val file = getDatabaseFile()
|
||||
FileUtils.moveDirectory(
|
||||
file,
|
||||
FileUtils.getFile(file.parentFile, file.name + "-old-" + System.currentTimeMillis())
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
private fun restart() {
|
||||
|
||||
// 重启
|
||||
TermoraRestarter.getInstance().scheduleRestart(null, ask = false)
|
||||
|
||||
// 退出程序
|
||||
Disposer.dispose(TermoraFrameManager.getInstance())
|
||||
}
|
||||
|
||||
|
||||
fun getDatabaseFile(): File {
|
||||
return FileUtils.getFile(Application.getBaseDataDir(), "storage")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.*
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.JXEditorPane
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.*
|
||||
import javax.swing.event.HyperlinkEvent
|
||||
|
||||
class MigrationDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
|
||||
private var isOpened = false
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
||||
isModal = true
|
||||
isResizable = false
|
||||
controlsVisible = false
|
||||
escapeDispose = false
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
title = StringUtils.EMPTY
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||
}
|
||||
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
|
||||
val loader = TermoraFrame::class.java.classLoader
|
||||
val images = sizes.mapNotNull { e ->
|
||||
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
|
||||
}
|
||||
iconImages = images
|
||||
}
|
||||
|
||||
setLocationRelativeTo(null)
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
var rows = 2
|
||||
val step = 2
|
||||
val formMargin = "7dlu"
|
||||
val icon = JLabel()
|
||||
icon.horizontalAlignment = SwingConstants.CENTER
|
||||
icon.icon = FlatSVGIcon(Icons.newUI.name, 80, 80)
|
||||
|
||||
val editorPane = JXEditorPane()
|
||||
editorPane.contentType = "text/html"
|
||||
editorPane.text = MigrationI18n.getString("termora.plugins.migration.message")
|
||||
editorPane.isEditable = false
|
||||
editorPane.addHyperlinkListener {
|
||||
if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) {
|
||||
Application.browse(it.url.toURI())
|
||||
}
|
||||
}
|
||||
editorPane.background = DynamicColor("window")
|
||||
val scrollPane = JScrollPane(editorPane)
|
||||
scrollPane.border = BorderFactory.createEmptyBorder()
|
||||
scrollPane.preferredSize = Dimension(Int.MAX_VALUE, 225)
|
||||
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowOpened(e: WindowEvent) {
|
||||
removeWindowListener(this)
|
||||
SwingUtilities.invokeLater { scrollPane.verticalScrollBar.value = 0 }
|
||||
}
|
||||
})
|
||||
|
||||
return FormBuilder.create().debug(false)
|
||||
.layout(
|
||||
FormLayout(
|
||||
"$formMargin, default:grow, 4dlu, pref, $formMargin",
|
||||
"${"0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
||||
)
|
||||
)
|
||||
.add(icon).xyw(2, rows, 4).apply { rows += step }
|
||||
.add(scrollPane).xyw(2, rows, 4).apply { rows += step }
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
fun open(): Boolean {
|
||||
isModal = true
|
||||
isVisible = true
|
||||
return isOpened
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
isOpened = true
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
override fun doCancelAction() {
|
||||
isOpened = false
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
override fun createOkAction(): AbstractAction {
|
||||
return OkAction(MigrationI18n.getString("termora.plugins.migration.migrate"))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.NamedI18n
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
object MigrationI18n : NamedI18n("i18n/messages") {
|
||||
private val log = LoggerFactory.getLogger(MigrationI18n::class.java)
|
||||
|
||||
override fun getLogger(): Logger {
|
||||
return log
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import app.termora.ApplicationRunnerExtension
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.Plugin
|
||||
|
||||
class MigrationPlugin : Plugin {
|
||||
private val support = ExtensionSupport()
|
||||
|
||||
init {
|
||||
support.addExtension(ApplicationRunnerExtension::class.java) { MigrationApplicationRunnerExtension.instance }
|
||||
}
|
||||
|
||||
override fun getAuthor(): String {
|
||||
return "TermoraDev"
|
||||
}
|
||||
|
||||
|
||||
override fun getName(): String {
|
||||
return "Migration"
|
||||
}
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package app.termora.plugins.migration
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
import kotlin.time.measureTime
|
||||
|
||||
object PBKDF2 {
|
||||
|
||||
private const val ALGORITHM = "PBKDF2WithHmacSHA512"
|
||||
private val log = LoggerFactory.getLogger(PBKDF2::class.java)
|
||||
|
||||
fun generateSecret(
|
||||
password: CharArray,
|
||||
salt: ByteArray,
|
||||
iterationCount: Int = 150000,
|
||||
keyLength: Int = 256
|
||||
): ByteArray {
|
||||
val bytes: ByteArray
|
||||
val time = measureTime {
|
||||
bytes = SecretKeyFactory.getInstance(ALGORITHM)
|
||||
.generateSecret(PBEKeySpec(password, salt, iterationCount, keyLength))
|
||||
.encoded
|
||||
}
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Secret generated $time")
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
fun hash(slat: ByteArray, password: CharArray, iterationCount: Int, keyLength: Int): ByteArray {
|
||||
val spec = PBEKeySpec(password, slat, iterationCount, keyLength)
|
||||
val secretKeyFactory = SecretKeyFactory.getInstance(ALGORITHM)
|
||||
return secretKeyFactory.generateSecret(spec).encoded
|
||||
}
|
||||
|
||||
}
|
||||