Compare commits
326 Commits
1.0.8
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0020fede1 | ||
|
|
6f1eaab456 | ||
|
|
6173eae772 | ||
|
|
0bb366b1f7 | ||
|
|
9a4d6f7f4d | ||
|
|
a4ae11e301 | ||
|
|
5af0acb619 | ||
|
|
042434b8f8 | ||
|
|
eddc7ef0c6 | ||
|
|
c96ca2d424 | ||
|
|
45be9008fd | ||
|
|
057da4e297 | ||
|
|
e4e41667ff | ||
|
|
95ca0a4af7 | ||
|
|
702dee7983 | ||
|
|
165d544448 | ||
|
|
ecf61bedc4 | ||
|
|
66a81a5da3 | ||
|
|
574c816ebb | ||
|
|
e7cafb74e4 | ||
|
|
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 | ||
|
|
932db49868 | ||
|
|
4d71c6cd05 | ||
|
|
96133e5abf | ||
|
|
f06e5d7dc1 | ||
|
|
d4b96edccf | ||
|
|
e9876d5b91 | ||
|
|
8b9a78a7bd | ||
|
|
6b48f577e9 | ||
|
|
da9b6c21d6 | ||
|
|
f1f889df14 | ||
|
|
ed65853ebe | ||
|
|
5ffdd219d9 | ||
|
|
4f84d6741c | ||
|
|
2568e7fcc8 | ||
|
|
dddbb49084 | ||
|
|
95846ab135 | ||
|
|
b5207e56c1 | ||
|
|
160771e912 | ||
|
|
0fbe180f3f | ||
|
|
41a0409e9e | ||
|
|
79e59143fb | ||
|
|
54e0f621ce | ||
|
|
4c8944d248 | ||
|
|
64bd95d8a8 | ||
|
|
1d88942e8e | ||
|
|
129e1b149a | ||
|
|
01aac98437 | ||
|
|
f9aaf7143f | ||
|
|
28174483f4 | ||
|
|
46412054c4 | ||
|
|
1ab0d26bab | ||
|
|
d90fb9aa35 | ||
|
|
744e64b359 | ||
|
|
2c5442f1f3 | ||
|
|
054c4701d2 | ||
|
|
54044625ea | ||
|
|
ca82704738 | ||
|
|
e98ec3fa8e | ||
|
|
6a4abf7e50 | ||
|
|
e2a6cceafd | ||
|
|
283404b6b9 | ||
|
|
c714f33a44 | ||
|
|
30fe047e5c | ||
|
|
827d814c7b | ||
|
|
ccb2c6daa0 | ||
|
|
1516d6d81e | ||
|
|
09b3655c4e | ||
|
|
614514c87e | ||
|
|
30cba6720d | ||
|
|
dce6551de2 | ||
|
|
95943cdeec | ||
|
|
18a26ee6bf | ||
|
|
f23aae371a | ||
|
|
757bc1c001 | ||
|
|
a19222dc60 | ||
|
|
24677ca4a6 | ||
|
|
0c5b6f8112 | ||
|
|
7c26e3d08a | ||
|
|
9b84fb4ec8 | ||
|
|
d8ec7b6d4a | ||
|
|
769c0d990b | ||
|
|
3f1ae38b61 | ||
|
|
e10fce21a2 | ||
|
|
a00557bb9d | ||
|
|
e478535ae5 | ||
|
|
7756758738 | ||
|
|
e0ea42faee | ||
|
|
e72c6b77b5 | ||
|
|
bcd3aacd6f | ||
|
|
570b0e08ad | ||
|
|
d703850e87 | ||
|
|
4bb1a411e8 | ||
|
|
9884ed19fa | ||
|
|
1ffaed3f36 | ||
|
|
4cb42953ad | ||
|
|
0248992dc3 | ||
|
|
9bab9db875 | ||
|
|
b283a3ea38 | ||
|
|
98ac2928b4 | ||
|
|
a0a6f43c10 | ||
|
|
0c158acbe0 | ||
|
|
9a97b3a304 | ||
|
|
aef44bd0da | ||
|
|
75c65d9ba8 | ||
|
|
93755db77f | ||
|
|
79d0a9a348 | ||
|
|
422e9aac84 | ||
|
|
9915c373b7 | ||
|
|
eba85e6348 | ||
|
|
483a7772f4 | ||
|
|
dcc96358f6 | ||
|
|
b5c30d505b | ||
|
|
1f3ef5f3f0 | ||
|
|
d388bcfc92 | ||
|
|
562c1f98fe | ||
|
|
f3c5009a45 | ||
|
|
09a1d9f51e | ||
|
|
84b48278ad | ||
|
|
ef9caf2578 | ||
|
|
b85bdf840e | ||
|
|
a2d7f3b5bb | ||
|
|
02a96d73c8 | ||
|
|
9fb12c7a71 | ||
|
|
145d8fc802 | ||
|
|
72c9dba806 | ||
|
|
de20bd654c | ||
|
|
35b3a10746 | ||
|
|
05fe6a0eb1 | ||
|
|
0552917c26 | ||
|
|
51c355c113 | ||
|
|
034ee3791d | ||
|
|
adabaf8f2d | ||
|
|
1f392c52a1 | ||
|
|
28fe4c725f | ||
|
|
18fe92cb11 | ||
|
|
c49acf7b51 | ||
|
|
7df317a1b9 | ||
|
|
219e5420f5 | ||
|
|
aefb7c3014 | ||
|
|
f0c7f06ff5 | ||
|
|
604e07b43a | ||
|
|
0000e4610a | ||
|
|
510324d7c4 | ||
|
|
33a359fcbf | ||
|
|
0b84d3271c | ||
|
|
57547c95cb | ||
|
|
503cfa9a4e | ||
|
|
af1f979e31 | ||
|
|
3cd9f92ea9 |
12
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gradle"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 25
|
||||
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-b825.69.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-b825.69.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
|
||||
|
||||
30
.github/workflows/osx-aarch64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install the Apple certificate
|
||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''
|
||||
env:
|
||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||
@@ -33,8 +33,18 @@ jobs:
|
||||
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
- name: Setup the Notary information
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.APPLE_ID != ''"
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
STORE_CREDENTIALS: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||
run: |
|
||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b825.69.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
|
||||
@@ -42,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
|
||||
@@ -54,11 +64,19 @@ 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:
|
||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' }}
|
||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
|
||||
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||
# 只有发布版本时才需要公证
|
||||
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||
run: |
|
||||
./gradlew dist --no-daemon
|
||||
|
||||
@@ -66,4 +84,6 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: termora-osx-aarch64
|
||||
path: build/distributions/*.dmg
|
||||
path: |
|
||||
build/distributions/*.zip
|
||||
build/distributions/*.dmg
|
||||
|
||||
30
.github/workflows/osx-x86-64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install the Apple certificate
|
||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''
|
||||
env:
|
||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||
@@ -33,8 +33,18 @@ jobs:
|
||||
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
- name: Setup the Notary information
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.APPLE_ID != ''"
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
STORE_CREDENTIALS: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||
run: |
|
||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b825.69.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
|
||||
@@ -42,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
|
||||
|
||||
|
||||
@@ -56,11 +66,19 @@ jobs:
|
||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||
|
||||
|
||||
# test build
|
||||
- run: |
|
||||
./gradlew classes -x test --no-daemon
|
||||
./gradlew clean --no-daemon
|
||||
|
||||
# dist
|
||||
- name: Dist
|
||||
env:
|
||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' }}
|
||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
|
||||
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||
# 只有发布版本时才需要公证
|
||||
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||
run: |
|
||||
./gradlew dist --no-daemon
|
||||
|
||||
@@ -68,4 +86,6 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: termora-osx-x86-64
|
||||
path: build/distributions/*.dmg
|
||||
path: |
|
||||
build/distributions/*.zip
|
||||
build/distributions/*.dmg
|
||||
|
||||
24
.github/workflows/windows-x86-64.yml
vendored
@@ -10,11 +10,20 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install zip
|
||||
run: |
|
||||
$system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32"
|
||||
Invoke-WebRequest -Uri "http://stahlworks.com/dev/zip.exe" -OutFile "$system32\zip.exe"
|
||||
Invoke-WebRequest -Uri "http://stahlworks.com/dev/unzip.exe" -OutFile "$system32\unzip.exe"
|
||||
|
||||
- name: Install 7z
|
||||
uses: milliewalky/setup-7-zip@v2
|
||||
|
||||
- name: Installing Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'jetbrains'
|
||||
java-version: '21'
|
||||
run: |
|
||||
curl -s --output ${{ runner.temp }}\java_package.zip -L https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-windows-x64-b1034.51.zip
|
||||
unzip -q ${{ runner.temp }}\java_package.zip -d ${{ runner.temp }}\jbr
|
||||
echo "JAVA_HOME=${{ runner.temp }}\jbr\jbrsdk-21.0.7-windows-x64-b1034.51" >> $env:GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
@@ -25,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
|
||||
@@ -36,4 +50,4 @@ jobs:
|
||||
name: termora-windows-x86-64
|
||||
path: |
|
||||
build/distributions/*.zip
|
||||
build/distributions/*.msi
|
||||
build/distributions/*.exe
|
||||
|
||||
3
.github/workflows/winget.yml
vendored
@@ -7,7 +7,8 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: vedantmgoyal9/winget-releaser@main
|
||||
if: github.repository == 'TermoraDev/termora'
|
||||
with:
|
||||
identifier: TermoraDev.Termora
|
||||
installers-regex: 'x86-64\.msi$' # Only x86-64.msi files
|
||||
installers-regex: '\.exe$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
@@ -6,6 +6,7 @@ certs/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
.vs
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
|
||||
125
README.md
@@ -1,51 +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
|
||||
- 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.
|
||||
|
||||
114
README.zh_CN.md
@@ -1,46 +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 端口转发和跳板机
|
||||
- 终端日志记录
|
||||
- 支持配置同步到 [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))
|
||||
- **专有许可**:如需闭源或商业用途,请联系作者获取授权
|
||||
|
||||
184
THIRDPARTY
@@ -1,240 +1,232 @@
|
||||
annotations 24.0.1
|
||||
annotations
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/java-annotations/blob/master/LICENSE.txt
|
||||
|
||||
bip39-lib-jvm 1.0.8
|
||||
MIT License
|
||||
https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/LICENSE
|
||||
|
||||
colorpicker 2.0.1
|
||||
colorpicker
|
||||
BSD 3-Clause "New" or "Revised" License
|
||||
https://github.com/dheid/colorpicker/blob/main/LICENSE
|
||||
|
||||
commonmark 0.24.0
|
||||
commonmark
|
||||
BSD 2-Clause "Simplified" License
|
||||
https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt
|
||||
|
||||
commons-codec 1.17.1
|
||||
commons-codec
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-codec/blob/master/LICENSE.txt
|
||||
|
||||
commons-compress 1.27.1
|
||||
commons-vfs2
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-compress/blob/master/LICENSE.txt
|
||||
https://github.com/apache/commons-vfs/blob/master/LICENSE.txt
|
||||
|
||||
commons-io 2.18.0
|
||||
commons-io
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-io/blob/master/LICENSE.txt
|
||||
|
||||
commons-lang3 3.17.0
|
||||
commons-lang3
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-lang/blob/master/LICENSE.txt
|
||||
|
||||
commons-net 3.11.1
|
||||
commons-net
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-net/blob/master/LICENSE.txt
|
||||
|
||||
commons-text 1.12.0
|
||||
commons-text
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-text/blob/master/LICENSE.txt
|
||||
|
||||
eddsa 0.3.0
|
||||
commons-csv
|
||||
Apache License 2.0
|
||||
https://github.com/apache/commons-csv/blob/master/LICENSE.txt
|
||||
|
||||
ini4j
|
||||
Apache License 2.0
|
||||
http://www.apache.org/licenses/LICENSE-2.0.txt
|
||||
|
||||
eddsa
|
||||
Creative Commons Zero v1.0 Universal
|
||||
https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt
|
||||
|
||||
flatlaf 3.5.4
|
||||
flatlaf
|
||||
Apache License 2.0
|
||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||
|
||||
flatlaf 3.5.4-no-natives
|
||||
flatlaf-no-natives
|
||||
Apache License 2.0
|
||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||
|
||||
flatlaf-extras 3.5.4
|
||||
flatlaf-extras
|
||||
Apache License 2.0
|
||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||
|
||||
flatlaf-swingx 3.5.4
|
||||
flatlaf-swingx
|
||||
Apache License 2.0
|
||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||
|
||||
JavaEWAH 1.2.3
|
||||
JavaEWAH
|
||||
Apache License 2.0
|
||||
https://github.com/lemire/javaewah/blob/master/LICENSE
|
||||
|
||||
jbr-api 17.1.10.1
|
||||
jbr-api
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/JetBrainsRuntimeApi/blob/main/LICENSE
|
||||
|
||||
jcl-over-slf4j 1.7.36
|
||||
jcl-over-slf4j
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0.txt
|
||||
|
||||
jfa 1.2.0
|
||||
jfa
|
||||
Apache License 2.0
|
||||
https://github.com/0x4a616e/jfa/blob/main/LICENSE
|
||||
|
||||
jgoodies-common 1.8.1
|
||||
jgoodies-common
|
||||
BSD-2-Clause License
|
||||
http://www.opensource.org/licenses/bsd-license.html
|
||||
|
||||
jgoodies-forms 1.9.0
|
||||
jgoodies-forms
|
||||
BSD-2-Clause License
|
||||
http://www.opensource.org/licenses/bsd-license.html
|
||||
|
||||
jna 5.16.0
|
||||
jna
|
||||
Apache License 2.0
|
||||
https://github.com/java-native-access/jna/blob/master/AL2.0
|
||||
|
||||
jna-platform 5.16.0
|
||||
jna-platform
|
||||
Apache License 2.0
|
||||
https://github.com/java-native-access/jna/blob/master/AL2.0
|
||||
|
||||
jnafilechooser-api 1.1.2
|
||||
jnafilechooser-api
|
||||
BSD 3-Clause "New" or "Revised" License
|
||||
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
||||
|
||||
jnafilechooser-win32 1.1.2
|
||||
jnafilechooser-win32
|
||||
BSD 3-Clause "New" or "Revised" License
|
||||
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
||||
|
||||
jsvg 1.4.0
|
||||
jsvg
|
||||
MIT License
|
||||
https://github.com/weisJ/jsvg/blob/master/LICENSE
|
||||
|
||||
jSystemThemeDetector 3.9.1
|
||||
jSystemThemeDetector
|
||||
Apache License 2.0
|
||||
https://github.com/Dansoftowner/jSystemThemeDetector/blob/master/LICENSE
|
||||
|
||||
kotlin-logging 1.7.9
|
||||
kotlin-logging
|
||||
Apache License 2.0
|
||||
https://github.com/oshai/kotlin-logging/blob/master/LICENSE
|
||||
|
||||
kotlin-stdlib 2.1.0
|
||||
kotlin-stdlib
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||
|
||||
kotlin-stdlib-jdk7 1.9.10
|
||||
kotlin-reflect
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||
|
||||
kotlin-stdlib-jdk8 1.9.10
|
||||
kotlin-stdlib-jdk7
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||
|
||||
kotlin-stdlib-jdk8 1.9.10
|
||||
kotlin-stdlib-jdk8
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||
|
||||
kotlinx-coroutines-core-jvm 1.10.1
|
||||
kotlin-stdlib-jdk8
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||
|
||||
restart4j
|
||||
Apache License 2.0
|
||||
https://github.com/hstyi/restart4j/blob/main/LICENSE
|
||||
|
||||
kotlinx-coroutines-core
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
kotlinx-coroutines-swing 1.10.1
|
||||
kotlinx-coroutines-swing
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
kotlinx-serialization-core-jvm 1.7.3
|
||||
kotlinx-serialization-json
|
||||
Apache License 2.0
|
||||
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
||||
|
||||
kotlinx-serialization-json-jvm 1.7.3
|
||||
Apache License 2.0
|
||||
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
||||
|
||||
logging-interceptor 4.12.0
|
||||
logging-interceptor
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
okhttp 4.12.0
|
||||
okhttp
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
okio-jvm 3.6.0
|
||||
okio-jvm
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
org.eclipse.jgit.ssh.apache 7.1.0.202411261347-r
|
||||
org.eclipse.jgit.ssh.apache
|
||||
Eclipse Distribution License
|
||||
https://www.eclipse.org/org/documents/edl-v10.php
|
||||
|
||||
org.eclipse.jgit 7.1.0.202411261347-r
|
||||
org.eclipse.jgit.ssh.apache.agent
|
||||
Eclipse Distribution License
|
||||
https://www.eclipse.org/org/documents/edl-v10.php
|
||||
|
||||
oshi-core 6.6.5
|
||||
org.eclipse.jgit
|
||||
Eclipse Distribution License
|
||||
https://www.eclipse.org/org/documents/edl-v10.php
|
||||
|
||||
oshi-core
|
||||
MIT License
|
||||
https://github.com/oshi/oshi/blob/master/LICENSE
|
||||
|
||||
pty4j 0.13.2
|
||||
pty4j
|
||||
Eclipse Public License 1.0
|
||||
https://github.com/JetBrains/pty4j/blob/master/LICENSE
|
||||
|
||||
slf4j-api 2.0.16
|
||||
slf4j-api
|
||||
MIT License
|
||||
https://github.com/qos-ch/slf4j/blob/master/LICENSE.txt
|
||||
|
||||
slf4j-tinylog 2.7.0
|
||||
slf4j-tinylog
|
||||
Apache License 2.0
|
||||
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||
|
||||
sshd-common 2.14.0
|
||||
sshd-common
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
sshd-core 2.14.0
|
||||
sshd-core
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
sshd-osgi 2.14.0
|
||||
sshd-osgi
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
sshd-sftp 2.14.0
|
||||
sshd-sftp
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
swingx-all 1.6.5-1
|
||||
swingx-all
|
||||
GNU LESSER GENERAL PUBLIC LICENSE v3
|
||||
https://www.gnu.org/licenses/lgpl-3.0
|
||||
|
||||
tinylog-api 2.7.0
|
||||
tinylog-api
|
||||
Apache License 2.0
|
||||
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||
|
||||
tinylog-impl 2.7.0
|
||||
tinylog-impl
|
||||
Apache License 2.0
|
||||
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||
|
||||
versioncompare 1.4.1
|
||||
versioncompare
|
||||
Apache License 2.0
|
||||
https://github.com/G00fY2/version-compare/blob/main/LICENSE
|
||||
|
||||
xodus-compress 2.0.1
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||
|
||||
xodus-environment 2.0.1
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||
|
||||
xodus-openAPI 2.0.1
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||
|
||||
xodus-utils 2.0.1
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||
|
||||
xodus-vfs 2.0.1
|
||||
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
|
||||
|
||||
mixpanel-java 1.5.3
|
||||
mixpanel-java
|
||||
Apache License 2.0
|
||||
https://github.com/mixpanel/mixpanel-java/blob/master/LICENSE
|
||||
|
||||
@@ -242,6 +234,34 @@ json-20231013
|
||||
Public Domain.
|
||||
https://github.com/stleary/JSON-java/blob/master/LICENSE
|
||||
|
||||
jSerialComm 2.11.0
|
||||
jSerialComm
|
||||
Apache License 2.0
|
||||
https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0
|
||||
|
||||
exposed-core
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
|
||||
|
||||
exposed-crypt
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
|
||||
|
||||
exposed-jdbc
|
||||
Apache License 2.0
|
||||
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
|
||||
|
||||
sqlite-jdbc
|
||||
Apache License 2.0
|
||||
https://www.apache.org/licenses/LICENSE-2.0.txt
|
||||
|
||||
java-uuid-generator
|
||||
Apache License 2.0
|
||||
https://github.com/cowtowncoder/java-uuid-generator/blob/master/LICENSE
|
||||
|
||||
semver4j
|
||||
MIT
|
||||
https://github.com/semver4j/semver4j/blob/main/LICENSE
|
||||
|
||||
dom4j
|
||||
Plexus (https://dom4j.github.io)
|
||||
https://github.com/dom4j/dom4j/blob/master/LICENSE
|
||||
788
build.gradle.kts
@@ -3,23 +3,32 @@ import org.gradle.kotlin.dsl.support.uppercaseFirstChar
|
||||
import org.gradle.nativeplatform.platform.internal.ArchitectureInternal
|
||||
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
|
||||
|
||||
plugins {
|
||||
java
|
||||
idea
|
||||
application
|
||||
`maven-publish`
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlinx.serialization)
|
||||
}
|
||||
|
||||
|
||||
group = "app.termora"
|
||||
version = "1.0.8"
|
||||
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
|
||||
@@ -31,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)
|
||||
@@ -49,92 +59,116 @@ 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.net)
|
||||
implementation(libs.commons.text)
|
||||
implementation(libs.commons.compress)
|
||||
implementation(libs.kotlinx.coroutines.swing)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
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.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(libs.flatlaf)
|
||||
api(libs.flatlafextras)
|
||||
api(libs.flatlafswingx)
|
||||
|
||||
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)
|
||||
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)
|
||||
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.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(
|
||||
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
||||
"-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) {
|
||||
args.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||
// macOS NSWindow
|
||||
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()
|
||||
}
|
||||
@@ -142,16 +176,19 @@ tasks.test {
|
||||
tasks.register<Copy>("copy-dependencies") {
|
||||
val dir = layout.buildDirectory.dir("libs")
|
||||
from(configurations.runtimeClasspath).into(dir)
|
||||
val jna = libs.jna.asProvider().get()
|
||||
val pty4j = libs.pty4j.get()
|
||||
val flatlaf = libs.flatlaf.get()
|
||||
val jSerialComm = libs.jSerialComm.get()
|
||||
val restart4j = libs.restart4j.get()
|
||||
val sqlite = libs.sqlite.get()
|
||||
|
||||
// 对 JNA 和 PTY4J 的本地库提取
|
||||
// 提取出来是为了单独签名,不然无法通过公证
|
||||
if (os.isMacOsX && macOSSign) {
|
||||
doLast {
|
||||
val jna = libs.jna.asProvider().get()
|
||||
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
||||
val dylib = dir.get().dir("dylib").asFile
|
||||
val pty4j = libs.pty4j.get()
|
||||
val jSerialComm = libs.jSerialComm.get()
|
||||
|
||||
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
||||
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
||||
val targetDir = File(dylib, jna.name)
|
||||
@@ -177,7 +214,6 @@ tasks.register<Copy>("copy-dependencies") {
|
||||
// 删除所有二进制类库
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
|
||||
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) {
|
||||
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
||||
val targetDir = FileUtils.getFile(dylib, jSerialComm.name, "OSX", archName)
|
||||
FileUtils.forceMkdir(targetDir)
|
||||
// @formatter:off
|
||||
@@ -191,6 +227,40 @@ tasks.register<Copy>("copy-dependencies") {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") }
|
||||
} else if ("${restart4j.name}-${restart4j.version}" == file.nameWithoutExtension) {
|
||||
val targetDir = FileUtils.getFile(dylib, restart4j.name)
|
||||
FileUtils.forceMkdir(targetDir)
|
||||
// @formatter:off
|
||||
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "darwin/${archName}/*", "-d", targetDir.absolutePath) }
|
||||
// @formatter:on
|
||||
// 删除所有二进制类库
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "win32/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "linux/*") }
|
||||
// 设置可执行权限
|
||||
for (e in FileUtils.listFiles(
|
||||
targetDir,
|
||||
FileFilterUtils.trueFileFilter(),
|
||||
FileFilterUtils.falseFileFilter()
|
||||
)) {
|
||||
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/*") }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +273,115 @@ tasks.register<Copy>("copy-dependencies") {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (os.isLinux || os.isWindows) { // 缩减安装包
|
||||
doLast {
|
||||
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
||||
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/darwin-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/sunos-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/openbsd-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/freebsd-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/dragonflybsd-*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
|
||||
if (os.isWindows) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/linux-*") }
|
||||
if (arch.isArm) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-x86*") }
|
||||
} else {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-aarch64/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-x86/*") }
|
||||
}
|
||||
} else if (os.isLinux) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-*") }
|
||||
}
|
||||
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*darwin*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*freebsd*") }
|
||||
if (os.isWindows) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*linux*") }
|
||||
if (arch.isArm) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86-64*") }
|
||||
} else {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/aarch64/*") }
|
||||
}
|
||||
} else if (os.isLinux) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*win*") }
|
||||
}
|
||||
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "Android/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "FreeBSD/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "OpenBSD/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
|
||||
if (os.isWindows) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "Linux/*") }
|
||||
} else if (os.isLinux) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") }
|
||||
}
|
||||
} else if ("${restart4j.name}-${restart4j.version}" == file.nameWithoutExtension) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") }
|
||||
if (os.isWindows) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "linux/*") }
|
||||
if (arch.isArm) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "win32/x86_64/*") }
|
||||
} else {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "win32/aarch64/*") }
|
||||
}
|
||||
} else if (os.isLinux) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "win32/*") }
|
||||
if (arch.isArm) {
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "linux/x86_64/*") }
|
||||
} else {
|
||||
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*") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +392,7 @@ tasks.register<Exec>("jlink") {
|
||||
"java.logging",
|
||||
"java.management",
|
||||
"java.rmi",
|
||||
"java.sql",
|
||||
"java.security.jgss",
|
||||
"jdk.crypto.ec",
|
||||
"jdk.unsupported",
|
||||
@@ -238,33 +418,38 @@ tasks.register<Exec>("jpackage") {
|
||||
|
||||
val buildDir = layout.buildDirectory.get()
|
||||
val options = mutableListOf(
|
||||
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
||||
"-Xmx2g",
|
||||
"-XX:+UseZGC",
|
||||
"-XX:+ZUncommit",
|
||||
"-XX:+ZGenerational",
|
||||
"-XX:ZUncommitDelay=60",
|
||||
"-Xmx2048m",
|
||||
"-XX:+HeapDumpOnOutOfMemoryError",
|
||||
"-Dlogger.console.level=off",
|
||||
"-Dkotlinx.coroutines.debug=off",
|
||||
"-Dapp-version=${project.version}",
|
||||
"-Drelease-date=${DateFormatUtils.format(Date(), "yyyy-MM-dd")}",
|
||||
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
||||
)
|
||||
|
||||
options.add("-Dsun.java2d.metal=true")
|
||||
|
||||
if (os.isMacOsX) {
|
||||
// NSWindow
|
||||
options.add("-Dapple.awt.application.appearance=system")
|
||||
options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||
options.add("--add-opens java.desktop/sun.font=ALL-UNNAMED")
|
||||
options.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||
options.add("--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"))
|
||||
@@ -273,6 +458,7 @@ tasks.register<Exec>("jpackage") {
|
||||
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
|
||||
arguments.addAll(listOf("--vendor", "TermoraDev"))
|
||||
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
||||
arguments.addAll(listOf("--app-content", "$buildDir/plugins"))
|
||||
|
||||
if (os.isWindows) {
|
||||
arguments.addAll(
|
||||
@@ -312,7 +498,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()
|
||||
}
|
||||
@@ -329,103 +519,244 @@ tasks.register<Exec>("jpackage") {
|
||||
|
||||
tasks.register("dist") {
|
||||
doLast {
|
||||
val vendor = Jvm.current().vendor ?: StringUtils.EMPTY
|
||||
@Suppress("UnstableApiUsage")
|
||||
if (!JvmVendorSpec.JETBRAINS.matches(vendor)) {
|
||||
throw GradleException("JVM: $vendor is not supported")
|
||||
}
|
||||
|
||||
val distributionDir = layout.buildDirectory.dir("distributions").get()
|
||||
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
|
||||
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
|
||||
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
|
||||
val macOSFinalFilePath = distributionDir.file("${finalFilenameWithoutExtension}.dmg").asFile.absolutePath
|
||||
|
||||
// 清空目录
|
||||
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
|
||||
if (os.isWindows) { // zip and msi
|
||||
// zip
|
||||
exec {
|
||||
commandLine(
|
||||
"tar", "-vacf",
|
||||
distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath,
|
||||
project.name.uppercaseFirstChar()
|
||||
)
|
||||
workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
|
||||
// 根据不同的系统构建不同的二进制包
|
||||
pack()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("check-license") {
|
||||
doLast {
|
||||
val iterator = File(projectDir, "THIRDPARTY").readLines().iterator()
|
||||
val thirdPartyNames = mutableSetOf<String>()
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
val name = iterator.next()
|
||||
if (name.isBlank()) {
|
||||
continue
|
||||
}
|
||||
|
||||
// msi
|
||||
exec {
|
||||
commandLine(
|
||||
"cmd", "/c", "move",
|
||||
"${project.name.uppercaseFirstChar()}-${project.version}.msi",
|
||||
"${finalFilenameWithoutExtension}.msi"
|
||||
)
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
} else if (os.isLinux) { // tar.gz
|
||||
exec {
|
||||
commandLine(
|
||||
"tar", "-czvf",
|
||||
distributionDir.file("${finalFilenameWithoutExtension}.tar.gz").asFile.absolutePath,
|
||||
project.name.uppercaseFirstChar()
|
||||
)
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
} else if (os.isMacOsX) { // rename
|
||||
exec {
|
||||
commandLine(
|
||||
"mv",
|
||||
distributionDir.file("${project.name.uppercaseFirstChar()}-${project.version}.dmg").asFile.absolutePath,
|
||||
macOSFinalFilePath,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw GradleException("${os.name} is not supported")
|
||||
// ignore license name
|
||||
iterator.next()
|
||||
// ignore license url
|
||||
iterator.next()
|
||||
|
||||
thirdPartyNames.add(name)
|
||||
}
|
||||
|
||||
// AppImage
|
||||
if (os.isLinux) {
|
||||
|
||||
// Download AppImageKit
|
||||
val appimagetool = FileUtils.getFile(projectDir, ".gradle", "appimagetool")
|
||||
if (!appimagetool.exists()) {
|
||||
exec {
|
||||
commandLine(
|
||||
"wget",
|
||||
"-O", appimagetool.absolutePath,
|
||||
"https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-${if (arch.isArm) "aarch64" else "x86_64"}.AppImage"
|
||||
)
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
|
||||
// AppImageKit chmod
|
||||
exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
|
||||
for (dependency in configurations.runtimeClasspath.get().allDependencies) {
|
||||
if (!thirdPartyNames.contains(dependency.name)) {
|
||||
throw GradleException("${dependency.name} No license found")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建包
|
||||
*/
|
||||
fun pack() {
|
||||
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
|
||||
val distributionDir = layout.buildDirectory.dir("distributions").get()
|
||||
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
|
||||
val projectName = project.name.uppercaseFirstChar()
|
||||
|
||||
if (os.isWindows) {
|
||||
packOnWindows(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||
} else if (os.isLinux) {
|
||||
packOnLinux(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||
} else if (os.isMacOsX) {
|
||||
packOnMac(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||
} else {
|
||||
throw GradleException("${os.name} is not supported")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 zip、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 = dir
|
||||
}
|
||||
|
||||
// exe
|
||||
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=exe").toString())
|
||||
exec {
|
||||
commandLine(
|
||||
"iscc",
|
||||
"/DMyAppId=${projectName}",
|
||||
"/DMyAppName=${projectName}",
|
||||
"/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}",
|
||||
"/F${finalFilenameWithoutExtension}",
|
||||
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
|
||||
)
|
||||
}
|
||||
|
||||
// msi
|
||||
exec {
|
||||
commandLine(
|
||||
"cmd", "/c", "move",
|
||||
"${projectName}-${appVersion}.msi",
|
||||
"${finalFilenameWithoutExtension}.msi"
|
||||
)
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对于 macOS 先对 jpackage 构建的 dmg 重命名 -> 签名 -> 公证,另外还会创建一个 zip 包
|
||||
*/
|
||||
fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
||||
val dmgFile = distributionDir.file("${finalFilenameWithoutExtension}.dmg").asFile
|
||||
val zipFile = distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile
|
||||
|
||||
// rename
|
||||
// @formatter:off
|
||||
exec { commandLine("mv", distributionDir.file("${projectName}-${appVersion}.dmg").asFile.absolutePath, dmgFile.absolutePath,) }
|
||||
// @formatter:on
|
||||
|
||||
// sign dmg
|
||||
if (macOSSign) signMacOSLocalFile(dmgFile)
|
||||
|
||||
// 找到 .app
|
||||
val imageFile = layout.buildDirectory.dir("jpackage/images/").get().asFile
|
||||
val appFile = imageFile.listFiles()?.firstOrNull()?.listFiles()?.firstOrNull()
|
||||
?: throw FileNotFoundException("${projectName}.app")
|
||||
|
||||
// zip
|
||||
// @formatter:off
|
||||
exec { commandLine("ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", appFile.absolutePath, zipFile.absolutePath) }
|
||||
// @formatter:on
|
||||
|
||||
// sign zip
|
||||
if (macOSSign) signMacOSLocalFile(zipFile)
|
||||
|
||||
// 公证
|
||||
if (macOSNotary) {
|
||||
val pool = Executors.newCachedThreadPool()
|
||||
val jobs = mutableListOf<Future<*>>()
|
||||
|
||||
// zip
|
||||
pool.submit {
|
||||
// 对 zip 公证
|
||||
notaryMacOSLocalFile(zipFile)
|
||||
// 对 .app 盖章
|
||||
stapleMacOSLocalFile(appFile)
|
||||
// 删除旧的 zip ,旧的 zip 仅仅是为了公证
|
||||
FileUtils.deleteQuietly(zipFile)
|
||||
// 再对盖完章的 app 打成 zip 包
|
||||
// @formatter:off
|
||||
exec { commandLine("ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", appFile.absolutePath, zipFile.absolutePath) }
|
||||
// @formatter:on
|
||||
// 再对 zip 签名
|
||||
signMacOSLocalFile(zipFile)
|
||||
}.apply { jobs.add(this) }
|
||||
|
||||
// dmg
|
||||
pool.submit {
|
||||
// 公证
|
||||
notaryMacOSLocalFile(dmgFile)
|
||||
// 盖章
|
||||
stapleMacOSLocalFile(dmgFile)
|
||||
}.apply { jobs.add(this) }
|
||||
|
||||
// join ...
|
||||
jobs.forEach { it.get() }
|
||||
|
||||
// shutdown
|
||||
pool.shutdown()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 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",
|
||||
distributionDir.file("${finalFilenameWithoutExtension}.tar.gz").asFile.absolutePath,
|
||||
projectName
|
||||
)
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
|
||||
|
||||
// Desktop file
|
||||
val termoraName = project.name.uppercaseFirstChar()
|
||||
val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile
|
||||
desktopFile.writeText(
|
||||
"""[Desktop Entry]
|
||||
// AppImage
|
||||
// Download AppImageKit
|
||||
val appimagetool = FileUtils.getFile(projectDir, ".gradle", "appimagetool")
|
||||
if (!appimagetool.exists()) {
|
||||
exec {
|
||||
commandLine(
|
||||
"wget",
|
||||
"-O", appimagetool.absolutePath,
|
||||
"https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-${if (arch.isArm) "aarch64" else "x86_64"}.AppImage"
|
||||
)
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
|
||||
// AppImageKit chmod
|
||||
exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
|
||||
}
|
||||
|
||||
|
||||
// Desktop file
|
||||
val termoraName = project.name.uppercaseFirstChar()
|
||||
val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile
|
||||
desktopFile.writeText(
|
||||
"""[Desktop Entry]
|
||||
Type=Application
|
||||
Name=${termoraName}
|
||||
Comment=Terminal emulator and SSH client
|
||||
@@ -433,85 +764,24 @@ Icon=/lib/${termoraName}
|
||||
Categories=Development;
|
||||
Terminal=false
|
||||
""".trimIndent()
|
||||
)
|
||||
)
|
||||
|
||||
// AppRun file
|
||||
val appRun = File(desktopFile.parentFile, "AppRun")
|
||||
val sb = StringBuilder()
|
||||
sb.append("#!/bin/sh").appendLine()
|
||||
sb.append("SELF=$(readlink -f \"$0\")").appendLine()
|
||||
sb.append("HERE=\${SELF%/*}").appendLine()
|
||||
sb.append("exec \"\${HERE}/bin/${termoraName}\" \"$@\"")
|
||||
appRun.writeText(sb.toString())
|
||||
appRun.setExecutable(true)
|
||||
// AppRun file
|
||||
val appRun = File(desktopFile.parentFile, "AppRun")
|
||||
val sb = StringBuilder()
|
||||
sb.append("#!/bin/sh").appendLine()
|
||||
sb.append("SELF=$(readlink -f \"$0\")").appendLine()
|
||||
sb.append("HERE=\${SELF%/*}").appendLine()
|
||||
sb.append("export LinuxAppImage=true").appendLine()
|
||||
sb.append("exec \"\${HERE}/bin/${termoraName}\" \"$@\"")
|
||||
appRun.writeText(sb.toString())
|
||||
appRun.setExecutable(true)
|
||||
|
||||
exec {
|
||||
commandLine(appimagetool.absolutePath, termoraName, "${finalFilenameWithoutExtension}.AppImage")
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// sign dmg
|
||||
if (os.isMacOsX && macOSSign) {
|
||||
|
||||
// sign
|
||||
signMacOSLocalFile(File(macOSFinalFilePath))
|
||||
|
||||
// notary
|
||||
if (macOSNotary) {
|
||||
exec {
|
||||
commandLine(
|
||||
"/usr/bin/xcrun", "notarytool",
|
||||
"submit", macOSFinalFilePath,
|
||||
"--keychain-profile", macOSNotaryKeychainProfile,
|
||||
"--wait",
|
||||
)
|
||||
}
|
||||
|
||||
// 绑定公证信息
|
||||
exec {
|
||||
commandLine(
|
||||
"/usr/bin/xcrun",
|
||||
"stapler", "staple", macOSFinalFilePath,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("check-license") {
|
||||
doLast {
|
||||
val thirdParty = mutableMapOf<String, String>()
|
||||
val iterator = File(projectDir, "THIRDPARTY").readLines().iterator()
|
||||
val thirdPartyNames = mutableSetOf<String>()
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
val nameWithVersion = iterator.next()
|
||||
if (nameWithVersion.isBlank()) {
|
||||
continue
|
||||
}
|
||||
|
||||
// ignore license name
|
||||
iterator.next()
|
||||
|
||||
val license = iterator.next()
|
||||
thirdParty[nameWithVersion.replace(StringUtils.SPACE, "-")] = license
|
||||
thirdPartyNames.add(nameWithVersion.split(StringUtils.SPACE).first())
|
||||
}
|
||||
|
||||
for (file in configurations.runtimeClasspath.get()) {
|
||||
val name = file.nameWithoutExtension
|
||||
if (!thirdParty.containsKey(name)) {
|
||||
if (logger.isWarnEnabled) {
|
||||
logger.warn("$name does not exist in third-party")
|
||||
}
|
||||
if (!thirdPartyNames.contains(name)) {
|
||||
throw GradleException("$name No license found")
|
||||
}
|
||||
}
|
||||
}
|
||||
// AppImage
|
||||
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=AppImage").toString())
|
||||
exec {
|
||||
commandLine(appimagetool.absolutePath, termoraName, "${finalFilenameWithoutExtension}.AppImage")
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,15 +804,51 @@ fun signMacOSLocalFile(file: File) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* macOS 对本地文件进行公证
|
||||
*/
|
||||
fun notaryMacOSLocalFile(file: File) {
|
||||
if (os.isMacOsX && macOSNotary) {
|
||||
if (file.exists()) {
|
||||
exec {
|
||||
commandLine(
|
||||
"/usr/bin/xcrun", "notarytool",
|
||||
"submit", file,
|
||||
"--keychain-profile", macOSNotaryKeychainProfile,
|
||||
"--wait",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 盖章
|
||||
*/
|
||||
fun stapleMacOSLocalFile(file: File) {
|
||||
if (os.isMacOsX && macOSNotary) {
|
||||
if (file.exists()) {
|
||||
exec {
|
||||
commandLine(
|
||||
"/usr/bin/xcrun",
|
||||
"stapler", "staple", file,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
kotlin {
|
||||
jvmToolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
@Suppress("UnstableApiUsage")
|
||||
vendor = JvmVendorSpec.JETBRAINS
|
||||
}
|
||||
}
|
||||
|
||||
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,47 +1,53 @@
|
||||
[versions]
|
||||
kotlin = "2.1.0"
|
||||
slf4j = "2.0.16"
|
||||
pty4j = "0.13.2"
|
||||
kotlin = "2.2.0"
|
||||
slf4j = "2.0.17"
|
||||
pty4j = "0.13.6"
|
||||
tinylog = "2.7.0"
|
||||
kotlinx-coroutines = "1.10.1"
|
||||
flatlaf = "3.5.4"
|
||||
trove4j = "1.0.20200330"
|
||||
kotlinx-serialization-json = "1.7.3"
|
||||
commons-codec = "1.17.1"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
flatlaf = "3.6.1-SNAPSHOT"
|
||||
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.12.0"
|
||||
commons-text = "1.13.1"
|
||||
commons-compress = "1.27.1"
|
||||
koin-bom = "4.0.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.16.0"
|
||||
jna = "5.17.0"
|
||||
jSystemThemeDetector = "3.9.1"
|
||||
commons-io = "2.18.0"
|
||||
commons-io = "2.19.0"
|
||||
jbr-api = "17.1.10.1"
|
||||
leveldb = "0.12"
|
||||
guava = "33.3.1-jre"
|
||||
credential-secure-storage = "1.0.3"
|
||||
hutool = "5.8.34"
|
||||
jsch = "0.2.21"
|
||||
okhttp = "4.12.0"
|
||||
bcprov = "1.79"
|
||||
hutool = "5.8.39"
|
||||
jsch = "2.27.2"
|
||||
okhttp = "5.1.0"
|
||||
sshj = "0.39.0"
|
||||
sshd-core = "2.14.0"
|
||||
jgit = "7.1.0.202411261347-r"
|
||||
commonmark = "0.24.0"
|
||||
sshd-core = "2.15.0"
|
||||
jgit = "7.2.0.202503040940-r"
|
||||
commonmark = "0.25.0"
|
||||
jnafilechooser = "1.1.2"
|
||||
xodus = "2.0.1"
|
||||
bip39 = "1.0.8"
|
||||
bip39 = "1.0.9"
|
||||
colorpicker = "2.0.1"
|
||||
rhino = "1.7.15"
|
||||
rhino = "1.8.0"
|
||||
delight-rhino-sandbox = "0.0.17"
|
||||
testcontainers = "1.20.4"
|
||||
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 = "2.0.0"
|
||||
dom4j = "2.2.0"
|
||||
|
||||
[libraries]
|
||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||
@@ -53,16 +59,18 @@ tinylog-impl = { group = "org.tinylog", name = "tinylog-impl", version.ref = "ti
|
||||
commons-codec = { group = "commons-codec", name = "commons-codec", version.ref = "commons-codec" }
|
||||
commons-net = { group = "commons-net", name = "commons-net", version.ref = "commons-net" }
|
||||
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commons-lang3" }
|
||||
commons-csv = { group = "org.apache.commons", name = "commons-csv", version.ref = "commons-csv" }
|
||||
commons-text = { group = "org.apache.commons", name = "commons-text", version.ref = "commons-text" }
|
||||
commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "commons-compress" }
|
||||
commons-vfs2 = { group = "org.apache.commons", name = "commons-vfs2", version.ref = "commons-vfs2" }
|
||||
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" }
|
||||
trove4j = { group = "org.jetbrains.intellij.deps", name = "trove4j", version.ref = "trove4j" }
|
||||
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
|
||||
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" }
|
||||
koin-core = { module = "io.insert-koin:koin-core" }
|
||||
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" }
|
||||
@@ -72,33 +80,39 @@ versioncompare = { module = "io.github.g00fy2:versioncompare", version.ref = "ve
|
||||
jfa = { module = "de.jangassen:jfa", version.ref = "jfa" }
|
||||
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" }
|
||||
leveldb = { module = "org.iq80.leveldb:leveldb", version.ref = "leveldb" }
|
||||
guava = { module = "com.google.guava:guava", version.ref = "guava" }
|
||||
hutool = { module = "cn.hutool:hutool-all", version.ref = "hutool" }
|
||||
credential-secure-storage = { module = "com.microsoft:credential-secure-storage", version.ref = "credential-secure-storage" }
|
||||
jsch = { module = "com.github.mwiede:jsch", version.ref = "jsch" }
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||
bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bcprov" }
|
||||
sshj = { module = "com.hierynomus:sshj", version.ref = "sshj" }
|
||||
sshd-core = { module = "org.apache.sshd:sshd-core", version.ref = "sshd-core" }
|
||||
jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" }
|
||||
commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
|
||||
jgit-sshd = { module = "org.eclipse.jgit:org.eclipse.jgit.ssh.apache", version.ref = "jgit" }
|
||||
jgit-agent = { module = "org.eclipse.jgit:org.eclipse.jgit.ssh.apache.agent", version.ref = "jgit" }
|
||||
xodus-openAPI = { module = "org.jetbrains.xodus:xodus-openAPI", version.ref = "xodus" }
|
||||
xodus-entity-store = { module = "org.jetbrains.xodus:xodus-entity-store", version.ref = "xodus" }
|
||||
xodus-environment = { module = "org.jetbrains.xodus:xodus-environment", version.ref = "xodus" }
|
||||
xodus-crypto = { module = "org.jetbrains.xodus:xodus-crypto", version.ref = "xodus" }
|
||||
xodus-vfs = { module = "org.jetbrains.xodus:xodus-vfs", version.ref = "xodus" }
|
||||
jnafilechooser = { module = "com.github.steos.jnafilechooser:jnafilechooser-api", version.ref = "jnafilechooser" }
|
||||
bip39 = { module = "cash.z.ecc.android:kotlin-bip39-jvm", version.ref = "bip39" }
|
||||
bip39 = { module = "cash.z.ecc.android:kotlin-bip39", version.ref = "bip39" }
|
||||
rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
||||
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
||||
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.5"
|
||||
|
||||
|
||||
|
||||
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,164 @@
|
||||
package app.termora.plugins.bg
|
||||
|
||||
import app.termora.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Request
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Window
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
internal class BackgroundManager private constructor() : Disposable, GlassPaneAwareExtension,
|
||||
ApplicationRunnerExtension {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(BackgroundManager::class.java)
|
||||
fun getInstance(): BackgroundManager {
|
||||
return ApplicationScope.Companion.forApplicationScope()
|
||||
.getOrCreate(BackgroundManager::class) { BackgroundManager() }
|
||||
}
|
||||
}
|
||||
|
||||
private var bufferedImage: BufferedImage? = null
|
||||
private var imageFilepath = StringUtils.EMPTY
|
||||
private val glassPanes = mutableListOf<WeakReference<JComponent>>()
|
||||
|
||||
|
||||
fun setBackgroundImage(url: String) {
|
||||
clearBackgroundImage()
|
||||
Appearance.backgroundImage = url
|
||||
refreshBackgroundImage()
|
||||
}
|
||||
|
||||
fun getBackgroundImage(): BufferedImage? {
|
||||
val bg = doGetBackgroundImage()
|
||||
if (bg == null) {
|
||||
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||
return null
|
||||
} else {
|
||||
JPopupMenu.setDefaultLightWeightPopupEnabled(true)
|
||||
}
|
||||
} else {
|
||||
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||
JPopupMenu.setDefaultLightWeightPopupEnabled(false)
|
||||
}
|
||||
}
|
||||
return bg
|
||||
}
|
||||
|
||||
private fun doGetBackgroundImage(): BufferedImage? {
|
||||
synchronized(this) {
|
||||
return bufferedImage
|
||||
}
|
||||
}
|
||||
|
||||
fun clearBackgroundImage() {
|
||||
synchronized(this) {
|
||||
bufferedImage = null
|
||||
imageFilepath = StringUtils.EMPTY
|
||||
Appearance.backgroundImage = StringUtils.EMPTY
|
||||
}
|
||||
refreshGlassPanes()
|
||||
}
|
||||
|
||||
private fun refreshBackgroundImage() {
|
||||
val backgroundImage = Appearance.backgroundImage
|
||||
if (backgroundImage.isBlank()) {
|
||||
return
|
||||
}
|
||||
|
||||
var file: File? = null
|
||||
|
||||
// 从网络下载
|
||||
if (backgroundImage.startsWith("http://") || backgroundImage.startsWith("https://")) {
|
||||
file = Application.httpClient.newCall(
|
||||
Request.Builder().get()
|
||||
.url(backgroundImage).build()
|
||||
).execute().use { response ->
|
||||
val tempFile = File(Application.getTemporaryDir(), randomUUID())
|
||||
if (response.isSuccessful.not()) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error("Request {} failed with code {}", backgroundImage, response.code)
|
||||
}
|
||||
return
|
||||
}
|
||||
val body = response.body
|
||||
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
|
||||
IOUtils.closeQuietly(body)
|
||||
return@use tempFile
|
||||
}
|
||||
}
|
||||
|
||||
val backgroundImageFile = File(backgroundImage)
|
||||
if (backgroundImageFile.isDirectory) {
|
||||
val files = FileUtils.listFiles(backgroundImageFile, arrayOf("png", "jpg", "jpeg"), false)
|
||||
if (files.isNotEmpty()) {
|
||||
for (i in 0 until files.size) {
|
||||
file = files.randomOrNull()
|
||||
if (file == null) break
|
||||
if (file.absolutePath == imageFilepath) continue
|
||||
}
|
||||
} else {
|
||||
synchronized(this) {
|
||||
imageFilepath = StringUtils.EMPTY
|
||||
bufferedImage = null
|
||||
refreshGlassPanes()
|
||||
}
|
||||
}
|
||||
} else if (backgroundImageFile.isFile) {
|
||||
file = backgroundImageFile
|
||||
}
|
||||
|
||||
if (file == null || imageFilepath == file.absolutePath) {
|
||||
return
|
||||
}
|
||||
|
||||
bufferedImage = file.inputStream().use { ImageIO.read(it) }
|
||||
imageFilepath = file.absolutePath
|
||||
|
||||
refreshGlassPanes()
|
||||
}
|
||||
|
||||
private fun refreshGlassPanes() {
|
||||
SwingUtilities.invokeLater {
|
||||
glassPanes.removeIf {
|
||||
val glassPane = it.get()
|
||||
glassPane?.repaint()
|
||||
glassPane == null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
|
||||
}
|
||||
|
||||
override fun setGlassPane(window: Window, glassPane: JComponent) {
|
||||
glassPanes.add(WeakReference(glassPane))
|
||||
}
|
||||
|
||||
override fun ready() {
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
while (isActive) {
|
||||
runCatching { refreshBackgroundImage() }.onFailure {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error("Refresh failed", it)
|
||||
}
|
||||
}
|
||||
delay(max(Appearance.interval, 30).seconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(":"))
|
||||
implementation("org.apache.commons:commons-pool2:2.12.1")
|
||||
testImplementation(project(":"))
|
||||
}
|
||||
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
@@ -0,0 +1,23 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.transfer.s3.S3FileSystem
|
||||
import app.termora.transfer.s3.S3Path
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.net.ftp.FTPClient
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||
|
||||
class FTPFileSystem(private val pool: GenericObjectPool<FTPClient>) : S3FileSystem(FTPSystemProvider(pool)) {
|
||||
|
||||
override fun create(root: String?, names: List<String>): S3Path {
|
||||
val path = FTPPath(this, root, names)
|
||||
if (names.isEmpty()) {
|
||||
path.attributes = path.attributes.copy(directory = true)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(pool)
|
||||
super.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.keymgr.KeyManager
|
||||
import app.termora.plugin.internal.BasicProxyOption
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.nio.charset.Charset
|
||||
import javax.swing.*
|
||||
|
||||
class FTPHostOptionsPane : OptionsPane() {
|
||||
private val generalOption = GeneralOption()
|
||||
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
|
||||
private val sftpOption = SFTPOption()
|
||||
|
||||
init {
|
||||
addOption(generalOption)
|
||||
addOption(proxyOption)
|
||||
addOption(sftpOption)
|
||||
|
||||
}
|
||||
|
||||
fun getHost(): Host {
|
||||
val name = generalOption.nameTextField.text
|
||||
val protocol = FTPProtocolProvider.PROTOCOL
|
||||
val port = generalOption.portTextField.value as Int
|
||||
var authentication = Authentication.Companion.No
|
||||
var proxy = Proxy.Companion.No
|
||||
val authenticationType = AuthenticationType.Password
|
||||
|
||||
authentication = authentication.copy(
|
||||
type = authenticationType,
|
||||
password = String(generalOption.passwordTextField.password)
|
||||
)
|
||||
|
||||
|
||||
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||
proxy = proxy.copy(
|
||||
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
|
||||
host = proxyOption.proxyHostTextField.text,
|
||||
username = proxyOption.proxyUsernameTextField.text,
|
||||
password = String(proxyOption.proxyPasswordTextField.password),
|
||||
port = proxyOption.proxyPortTextField.value as Int,
|
||||
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val options = Options.Default.copy(
|
||||
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||
encoding = sftpOption.charsetComboBox.selectedItem as String,
|
||||
extras = mutableMapOf("passive" to (sftpOption.passiveComboBox.selectedItem as PassiveMode).name)
|
||||
)
|
||||
|
||||
return Host(
|
||||
name = name,
|
||||
protocol = protocol,
|
||||
port = port,
|
||||
host = generalOption.hostTextField.text,
|
||||
username = generalOption.usernameTextField.text,
|
||||
authentication = authentication,
|
||||
proxy = proxy,
|
||||
sort = System.currentTimeMillis(),
|
||||
remark = generalOption.remarkTextArea.text,
|
||||
options = options,
|
||||
)
|
||||
}
|
||||
|
||||
fun setHost(host: Host) {
|
||||
generalOption.nameTextField.text = host.name
|
||||
generalOption.usernameTextField.text = host.username
|
||||
generalOption.remarkTextArea.text = host.remark
|
||||
generalOption.passwordTextField.text = host.authentication.password
|
||||
generalOption.hostTextField.text = host.host
|
||||
generalOption.portTextField.value = host.port
|
||||
|
||||
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||
proxyOption.proxyUsernameTextField.text = host.proxy.username
|
||||
proxyOption.proxyPortTextField.value = host.proxy.port
|
||||
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
|
||||
|
||||
|
||||
val passive = host.options.extras["passive"] ?: PassiveMode.Local.name
|
||||
sftpOption.charsetComboBox.selectedItem = host.options.encoding
|
||||
sftpOption.passiveComboBox.selectedItem = runCatching { PassiveMode.valueOf(passive) }
|
||||
.getOrNull() ?: PassiveMode.Local
|
||||
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||
}
|
||||
|
||||
fun validateFields(): Boolean {
|
||||
val host = getHost()
|
||||
|
||||
// general
|
||||
if (validateField(generalOption.nameTextField)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if (validateField(generalOption.hostTextField)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(generalOption.usernameTextField.text) || generalOption.passwordTextField.password.isNotEmpty()) {
|
||||
if (validateField(generalOption.usernameTextField)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (validateField(generalOption.passwordTextField)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// proxy
|
||||
if (host.proxy.type != ProxyType.No) {
|
||||
if (validateField(proxyOption.proxyHostTextField)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (host.proxy.authenticationType != AuthenticationType.No) {
|
||||
if (validateField(proxyOption.proxyUsernameTextField)
|
||||
|| validateField(proxyOption.proxyPasswordTextField)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 true 表示有错误
|
||||
*/
|
||||
private fun validateField(textField: JTextField): Boolean {
|
||||
if (textField.isEnabled && (if (textField is JPasswordField) textField.password.isEmpty() else textField.text.isBlank())) {
|
||||
setOutlineError(textField)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun setOutlineError(c: JComponent) {
|
||||
selectOptionJComponent(c)
|
||||
c.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||
c.requestFocusInWindow()
|
||||
}
|
||||
|
||||
|
||||
inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||
val portTextField = PortSpinner(21)
|
||||
val nameTextField = OutlineTextField(128)
|
||||
val usernameTextField = OutlineTextField(128)
|
||||
val hostTextField = OutlineTextField(255)
|
||||
val passwordTextField = OutlinePasswordField(255)
|
||||
val publicKeyComboBox = OutlineComboBox<String>()
|
||||
val remarkTextArea = FixedLengthTextArea(512)
|
||||
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
|
||||
publicKeyComboBox.isEditable = false
|
||||
|
||||
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = StringUtils.EMPTY
|
||||
if (value is String) {
|
||||
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
|
||||
}
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
text,
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = value?.toString() ?: ""
|
||||
when (value) {
|
||||
AuthenticationType.Password -> {
|
||||
text = "Password"
|
||||
}
|
||||
|
||||
AuthenticationType.PublicKey -> {
|
||||
text = "Public Key"
|
||||
}
|
||||
|
||||
AuthenticationType.KeyboardInteractive -> {
|
||||
text = "Keyboard Interactive"
|
||||
}
|
||||
}
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
text,
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||
|
||||
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
|
||||
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentResized(e: ComponentEvent) {
|
||||
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
|
||||
removeComponentListener(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.settings
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.new-host.general")
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
|
||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
remarkTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
remarkTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
|
||||
remarkTextArea.rows = 8
|
||||
remarkTextArea.lineWrap = true
|
||||
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
||||
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||
.add(hostTextField).xy(3, rows)
|
||||
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||
.add(portTextField).xy(7, rows).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
|
||||
.add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||
.xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.build()
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||
val defaultDirectoryField = OutlineTextField(255)
|
||||
val charsetComboBox = JComboBox<String>()
|
||||
val passiveComboBox = JComboBox<PassiveMode>()
|
||||
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
|
||||
for (e in Charset.availableCharsets()) {
|
||||
charsetComboBox.addItem(e.key)
|
||||
}
|
||||
|
||||
charsetComboBox.selectedItem = "UTF-8"
|
||||
|
||||
passiveComboBox.addItem(PassiveMode.Local)
|
||||
passiveComboBox.addItem(PassiveMode.Remote)
|
||||
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.folder
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.transport.sftp")
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow",
|
||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
||||
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${FTPI18n.getString("termora.plugins.ftp.passive")}:").xy(1, rows)
|
||||
.add(passiveComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
|
||||
.build()
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
enum class PassiveMode {
|
||||
Local,
|
||||
Remote,
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.I18n
|
||||
import app.termora.NamedI18n
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
|
||||
object FTPI18n : NamedI18n("i18n/messages") {
|
||||
private val log = LoggerFactory.getLogger(FTPI18n::class.java)
|
||||
|
||||
override fun getLogger(): Logger {
|
||||
return log
|
||||
}
|
||||
|
||||
override fun getString(key: String): String {
|
||||
return try {
|
||||
substitutor.replace(getBundle().getString(key))
|
||||
} catch (_: MissingResourceException) {
|
||||
I18n.getString(key)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.transfer.s3.S3Path
|
||||
|
||||
class FTPPath(fileSystem: FTPFileSystem, root: String?, names: List<String>) : S3Path(fileSystem, root, names) {
|
||||
override val isBucket: Boolean
|
||||
get() = false
|
||||
|
||||
override val bucketName: String
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override val objectName: String
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override fun getCustomType(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.PaidPlugin
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class FTPPlugin : PaidPlugin {
|
||||
private val support = ExtensionSupport()
|
||||
|
||||
init {
|
||||
support.addExtension(ProtocolProviderExtension::class.java) { FTPProtocolProviderExtension.Companion.instance }
|
||||
support.addExtension(ProtocolHostPanelExtension::class.java) { FTPProtocolHostPanelExtension.Companion.instance }
|
||||
}
|
||||
|
||||
override fun getAuthor(): String {
|
||||
return "TermoraDev"
|
||||
}
|
||||
|
||||
|
||||
override fun getName(): String {
|
||||
return "FTP"
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.Disposer
|
||||
import app.termora.Host
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import java.awt.BorderLayout
|
||||
|
||||
class FTPProtocolHostPanel : ProtocolHostPanel() {
|
||||
|
||||
private val pane = FTPHostOptionsPane()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
private fun initView() {
|
||||
add(pane, BorderLayout.CENTER)
|
||||
Disposer.register(this, pane)
|
||||
}
|
||||
|
||||
private fun initEvents() {}
|
||||
|
||||
override fun getHost(): Host {
|
||||
return pane.getHost()
|
||||
}
|
||||
|
||||
override fun setHost(host: Host) {
|
||||
pane.setHost(host)
|
||||
}
|
||||
|
||||
override fun validateFields(): Boolean {
|
||||
return pane.validateFields()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
|
||||
class FTPProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||
companion object {
|
||||
val instance = FTPProtocolHostPanelExtension()
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return FTPProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return FTPProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.AuthenticationType
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.Icons
|
||||
import app.termora.ProxyType
|
||||
import app.termora.protocol.PathHandler
|
||||
import app.termora.protocol.PathHandlerRequest
|
||||
import app.termora.protocol.TransferProtocolProvider
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.net.ftp.FTPClient
|
||||
import org.apache.commons.pool2.BasePooledObjectFactory
|
||||
import org.apache.commons.pool2.PooledObject
|
||||
import org.apache.commons.pool2.impl.DefaultPooledObject
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.nio.charset.Charset
|
||||
import java.time.Duration
|
||||
|
||||
|
||||
class FTPProtocolProvider private constructor() : TransferProtocolProvider {
|
||||
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(FTPProtocolProvider::class.java)
|
||||
|
||||
val instance = FTPProtocolProvider()
|
||||
const val PROTOCOL = "FTP"
|
||||
}
|
||||
|
||||
override fun getProtocol(): String {
|
||||
return PROTOCOL
|
||||
}
|
||||
|
||||
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||
return Icons.ftp
|
||||
}
|
||||
|
||||
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
|
||||
val host = requester.host
|
||||
|
||||
val config = GenericObjectPoolConfig<FTPClient>().apply {
|
||||
maxTotal = 12
|
||||
// 与 transfer 最大传输量匹配
|
||||
maxIdle = 6
|
||||
minIdle = 1
|
||||
testOnBorrow = false
|
||||
testWhileIdle = true
|
||||
// 检测空闲对象线程每次运行时检测的空闲对象的数量
|
||||
timeBetweenEvictionRuns = Duration.ofSeconds(30)
|
||||
// 连接空闲的最小时间,达到此值后空闲链接将会被移除,且保留 minIdle 个空闲连接数
|
||||
softMinEvictableIdleDuration = Duration.ofSeconds(30)
|
||||
// 连接的最小空闲时间,达到此值后该空闲连接可能会被移除(还需看是否已达最大空闲连接数)
|
||||
minEvictableIdleDuration = Duration.ofMinutes(3)
|
||||
}
|
||||
|
||||
val ftpClientPool = GenericObjectPool(object : BasePooledObjectFactory<FTPClient>() {
|
||||
override fun create(): FTPClient {
|
||||
val client = FTPClient()
|
||||
client.charset = Charset.forName(host.options.encoding)
|
||||
client.controlEncoding = client.charset.name()
|
||||
client.connect(host.host, host.port)
|
||||
if (client.isConnected.not()) {
|
||||
throw IllegalStateException("FTP client is not connected")
|
||||
}
|
||||
|
||||
if (host.proxy.type == ProxyType.HTTP) {
|
||||
client.proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress(host.proxy.host, host.proxy.port))
|
||||
} else if (host.proxy.type == ProxyType.SOCKS5) {
|
||||
client.proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(host.proxy.host, host.proxy.port))
|
||||
}
|
||||
|
||||
val password = if (host.authentication.type == AuthenticationType.Password)
|
||||
host.authentication.password else StringUtils.EMPTY
|
||||
if (client.login(host.username, password).not()) {
|
||||
throw IllegalStateException("Incorrect account or password")
|
||||
}
|
||||
|
||||
if (host.options.extras["passive"] == FTPHostOptionsPane.PassiveMode.Remote.name) {
|
||||
client.enterRemotePassiveMode()
|
||||
} else {
|
||||
client.enterLocalPassiveMode()
|
||||
}
|
||||
|
||||
client.listHiddenFiles = true
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
override fun wrap(obj: FTPClient): PooledObject<FTPClient> {
|
||||
return DefaultPooledObject(obj)
|
||||
}
|
||||
|
||||
override fun validateObject(p: PooledObject<FTPClient>): Boolean {
|
||||
val ftp = p.`object`
|
||||
return ftp.isConnected.not() && ftp.sendNoOp()
|
||||
}
|
||||
|
||||
override fun destroyObject(p: PooledObject<FTPClient>) {
|
||||
try {
|
||||
p.`object`.disconnect()
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}, config)
|
||||
|
||||
val defaultPath = host.options.sftpDefaultDirectory
|
||||
val fs = FTPFileSystem(ftpClientPool)
|
||||
return PathHandler(fs, fs.getPath(defaultPath))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||
companion object {
|
||||
val instance = FTPProtocolProviderExtension()
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return FTPProtocolProvider.instance
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.transfer.s3.S3FileSystemProvider
|
||||
import app.termora.transfer.s3.S3Path
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.net.ftp.FTPClient
|
||||
import org.apache.commons.net.ftp.FTPFile
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.AccessMode
|
||||
import java.nio.file.CopyOption
|
||||
import java.nio.file.NoSuchFileException
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.attribute.FileAttribute
|
||||
import java.nio.file.attribute.PosixFilePermission
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.exists
|
||||
|
||||
class FTPSystemProvider(private val pool: GenericObjectPool<FTPClient>) : S3FileSystemProvider() {
|
||||
|
||||
|
||||
override fun getScheme(): String? {
|
||||
return "ftp"
|
||||
}
|
||||
|
||||
override fun getOutputStream(path: S3Path): OutputStream {
|
||||
return createStreamer(path)
|
||||
}
|
||||
|
||||
override fun getInputStream(path: S3Path): InputStream {
|
||||
val ftp = pool.borrowObject()
|
||||
val fs = ftp.retrieveFileStream(path.absolutePathString())
|
||||
return object : InputStream() {
|
||||
override fun read(): Int {
|
||||
return fs.read()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(fs)
|
||||
ftp.completePendingCommand()
|
||||
pool.returnObject(ftp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createStreamer(path: S3Path): OutputStream {
|
||||
val ftp = pool.borrowObject()
|
||||
val os = ftp.storeFileStream(path.absolutePathString())
|
||||
return object : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
os.write(b)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(os)
|
||||
ftp.completePendingCommand()
|
||||
pool.returnObject(ftp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
|
||||
val paths = mutableListOf<S3Path>()
|
||||
if (path.exists().not()) {
|
||||
throw NoSuchFileException(path.absolutePathString())
|
||||
}
|
||||
|
||||
withFtpClient {
|
||||
val files = it.listFiles(path.absolutePathString())
|
||||
for (file in files) {
|
||||
val p = path.resolve(file.name)
|
||||
p.attributes = p.attributes.copy(
|
||||
directory = file.isDirectory,
|
||||
regularFile = file.isFile,
|
||||
size = file.size,
|
||||
lastModifiedTime = file.timestamp.timeInMillis,
|
||||
)
|
||||
p.attributes.permissions = ftpPermissionsToPosix(file)
|
||||
paths.add(p)
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun ftpPermissionsToPosix(file: FTPFile): Set<PosixFilePermission> {
|
||||
val perms = mutableSetOf<PosixFilePermission>()
|
||||
|
||||
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION))
|
||||
perms.add(PosixFilePermission.OWNER_READ)
|
||||
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OWNER_WRITE)
|
||||
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OWNER_EXECUTE)
|
||||
|
||||
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.READ_PERMISSION))
|
||||
perms.add(PosixFilePermission.GROUP_READ)
|
||||
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||
perms.add(PosixFilePermission.GROUP_WRITE)
|
||||
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||
perms.add(PosixFilePermission.GROUP_EXECUTE)
|
||||
|
||||
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.READ_PERMISSION))
|
||||
perms.add(PosixFilePermission.OTHERS_READ)
|
||||
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OTHERS_WRITE)
|
||||
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OTHERS_EXECUTE)
|
||||
|
||||
return perms
|
||||
}
|
||||
|
||||
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
|
||||
withFtpClient { it.mkd(dir.absolutePathString()) }
|
||||
}
|
||||
|
||||
override fun move(source: Path?, target: Path?, vararg options: CopyOption?) {
|
||||
if (source != null && target != null) {
|
||||
withFtpClient {
|
||||
it.rename(source.absolutePathString(), target.absolutePathString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(path: S3Path, isDirectory: Boolean) {
|
||||
withFtpClient {
|
||||
if (isDirectory) {
|
||||
it.rmd(path.absolutePathString())
|
||||
} else {
|
||||
it.deleteFile(path.absolutePathString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
|
||||
withFtpClient {
|
||||
if (it.cwd(path.absolutePathString()) == 250) {
|
||||
return
|
||||
}
|
||||
if (it.listFiles(path.absolutePathString()).isNotEmpty()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
throw NoSuchFileException(path.absolutePathString())
|
||||
}
|
||||
|
||||
private inline fun <T> withFtpClient(block: (FTPClient) -> T): T {
|
||||
val client = pool.borrowObject()
|
||||
return try {
|
||||
block(client)
|
||||
} finally {
|
||||
pool.returnObject(client)
|
||||
}
|
||||
}
|
||||
}
|
||||
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="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#6C707E"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#6C707E"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg t="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#CED0D6"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#CED0D6"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
plugins/ftp/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1 @@
|
||||
termora.plugins.ftp.passive=Passive Mode
|
||||
@@ -0,0 +1 @@
|
||||
termora.plugins.ftp.passive=被动模式
|
||||
@@ -0,0 +1 @@
|
||||
termora.plugins.ftp.passive=被動模式
|
||||
16
plugins/geo/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.7"
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
implementation("com.maxmind.geoip2:geoip2:4.3.1")
|
||||
// https://github.com/hstyi/geolite2
|
||||
implementation("com.github.hstyi:geolite2:v1.0-202507040118")
|
||||
}
|
||||
|
||||
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 加载中
|
||||