Compare commits
301 Commits
1.0.7
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
b332bada95 | ||
|
|
63a12c2ec8 | ||
|
|
743f242805 | ||
|
|
5bead0b27d | ||
|
|
73e3c7016b | ||
|
|
3829dcd0f9 | ||
|
|
b2047044fe | ||
|
|
47d1a13189 | ||
|
|
309909cbd7 | ||
|
|
b5cebb4cea | ||
|
|
b6dd2693cd | ||
|
|
5fdfe98f26 | ||
|
|
0c768aa1ca | ||
|
|
d493e6dc9e | ||
|
|
7e0c7d8891 | ||
|
|
3510c6600d | ||
|
|
32d91150bd | ||
|
|
bbf2d50e3f | ||
|
|
39725f9828 | ||
|
|
1e8c617a85 | ||
|
|
7f8573ec4c | ||
|
|
d8e629917e | ||
|
|
bdc0a15439 | ||
|
|
a25b97614f | ||
|
|
4e12c32566 | ||
|
|
ea9c0f1225 | ||
|
|
ff865f13a2 | ||
|
|
9875200912 | ||
|
|
9f218d004e | ||
|
|
ab727f66f4 | ||
|
|
efbc0302e4 | ||
|
|
ab2367d670 | ||
|
|
045e4f81d6 | ||
|
|
160cfee947 | ||
|
|
0e40b5ecce | ||
|
|
fcaddcee80 | ||
|
|
8d6295fd3b | ||
|
|
d0d51b3e6f | ||
|
|
b8d612f1d5 | ||
|
|
f7c49cde0c | ||
|
|
189f8fb3ba | ||
|
|
2a64bd28a8 | ||
|
|
8a733379a3 | ||
|
|
e5f854dfcd |
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
|
||||||
52
.github/workflows/linux-aarch64.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
name: Linux aarch64
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# download jdk
|
||||||
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-aarch64-b1034.51.tar.gz
|
||||||
|
|
||||||
|
# appimagetool
|
||||||
|
- run: sudo apt install libfuse2
|
||||||
|
|
||||||
|
# install jdk
|
||||||
|
- name: Installing Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'jdkfile'
|
||||||
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
|
java-version: '21.0.7'
|
||||||
|
architecture: aarch64
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
# test build
|
||||||
|
- run: |
|
||||||
|
./gradlew classes -x test --no-daemon
|
||||||
|
./gradlew clean --no-daemon
|
||||||
|
|
||||||
|
# dist
|
||||||
|
- run: |
|
||||||
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-linux-aarch64
|
||||||
|
path: |
|
||||||
|
build/distributions/*.tar.gz
|
||||||
|
build/distributions/*.AppImage
|
||||||
27
.github/workflows/linux-x86-64.yml
vendored
@@ -4,14 +4,17 @@ on: [ push, pull_request ]
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# download jdk
|
# download jdk
|
||||||
- run: wget -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
|
||||||
|
|
||||||
# install jdk
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -19,9 +22,23 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'jdkfile'
|
distribution: 'jdkfile'
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
java-version: '21.0.6'
|
java-version: '21.0.7'
|
||||||
architecture: x64
|
architecture: x64
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
# test build
|
||||||
|
- run: |
|
||||||
|
./gradlew classes -x test --no-daemon
|
||||||
|
./gradlew clean --no-daemon
|
||||||
|
|
||||||
# dist
|
# dist
|
||||||
- run: |
|
- run: |
|
||||||
./gradlew dist --no-daemon
|
./gradlew dist --no-daemon
|
||||||
@@ -30,4 +47,6 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: termora-linux-x86-64
|
name: termora-linux-x86-64
|
||||||
path: build/distributions/*.tar.gz
|
path: |
|
||||||
|
build/distributions/*.tar.gz
|
||||||
|
build/distributions/*.AppImage
|
||||||
|
|||||||
39
.github/workflows/osx-aarch64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install the Apple certificate
|
- name: Install the Apple certificate
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
||||||
env:
|
env:
|
||||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
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 import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
- name: Setup the Notary information
|
||||||
|
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
||||||
|
env:
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
|
STORE_CREDENTIALS: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||||
|
run: |
|
||||||
|
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||||
|
|
||||||
# download jdk
|
# download jdk
|
||||||
- run: wget -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
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -42,14 +52,31 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'jdkfile'
|
distribution: 'jdkfile'
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
java-version: '21.0.6'
|
java-version: '21.0.7'
|
||||||
architecture: aarch64
|
architecture: aarch64
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
# test build
|
||||||
|
- run: |
|
||||||
|
./gradlew classes -x test --no-daemon
|
||||||
|
./gradlew clean --no-daemon
|
||||||
|
|
||||||
# dist
|
# dist
|
||||||
- name: Dist
|
- name: Dist
|
||||||
env:
|
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_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: |
|
run: |
|
||||||
./gradlew dist --no-daemon
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
@@ -57,4 +84,6 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: termora-osx-aarch64
|
name: termora-osx-aarch64
|
||||||
path: build/distributions/*.dmg
|
path: |
|
||||||
|
build/distributions/*.zip
|
||||||
|
build/distributions/*.dmg
|
||||||
|
|||||||
40
.github/workflows/osx-x86-64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install the Apple certificate
|
- name: Install the Apple certificate
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
||||||
env:
|
env:
|
||||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
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 import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
- name: Setup the Notary information
|
||||||
|
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
||||||
|
env:
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
|
STORE_CREDENTIALS: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||||
|
run: |
|
||||||
|
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||||
|
|
||||||
# download jdk
|
# download jdk
|
||||||
- run: wget -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
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -42,15 +52,33 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'jdkfile'
|
distribution: 'jdkfile'
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
java-version: '21.0.6'
|
java-version: '21.0.7'
|
||||||
architecture: x64
|
architecture: x64
|
||||||
|
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
|
||||||
|
# test build
|
||||||
|
- run: |
|
||||||
|
./gradlew classes -x test --no-daemon
|
||||||
|
./gradlew clean --no-daemon
|
||||||
|
|
||||||
# dist
|
# dist
|
||||||
- name: Dist
|
- name: Dist
|
||||||
env:
|
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_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: |
|
run: |
|
||||||
./gradlew dist --no-daemon
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
@@ -58,4 +86,6 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: termora-osx-x86-64
|
name: termora-osx-x86-64
|
||||||
path: build/distributions/*.dmg
|
path: |
|
||||||
|
build/distributions/*.zip
|
||||||
|
build/distributions/*.dmg
|
||||||
|
|||||||
32
.github/workflows/windows-x86-64.yml
vendored
@@ -10,15 +10,39 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
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
|
- name: Installing Java
|
||||||
uses: actions/setup-java@v4
|
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:
|
with:
|
||||||
distribution: 'jetbrains'
|
path: |
|
||||||
java-version: '21'
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
# test build
|
||||||
|
- run: |
|
||||||
|
.\gradlew classes -x test --no-daemon
|
||||||
|
.\gradlew clean --no-daemon
|
||||||
|
|
||||||
# dist
|
# dist
|
||||||
- run: |
|
- run: |
|
||||||
.\gradlew.bat dist --no-daemon
|
.\gradlew.bat dist --no-daemon
|
||||||
|
.\gradlew.bat --stop
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -26,4 +50,4 @@ jobs:
|
|||||||
name: termora-windows-x86-64
|
name: termora-windows-x86-64
|
||||||
path: |
|
path: |
|
||||||
build/distributions/*.zip
|
build/distributions/*.zip
|
||||||
build/distributions/*.msi
|
build/distributions/*.exe
|
||||||
|
|||||||
3
.github/workflows/winget.yml
vendored
@@ -7,7 +7,8 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: vedantmgoyal9/winget-releaser@main
|
- uses: vedantmgoyal9/winget-releaser@main
|
||||||
|
if: github.repository == 'TermoraDev/termora'
|
||||||
with:
|
with:
|
||||||
identifier: TermoraDev.Termora
|
identifier: TermoraDev.Termora
|
||||||
installers-regex: 'x86-64\.msi$' # Only x86-64.msi files
|
installers-regex: '\.exe$'
|
||||||
token: ${{ secrets.WINGET_TOKEN }}
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -6,6 +6,7 @@ certs/
|
|||||||
!gradle/wrapper/gradle-wrapper.jar
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
!**/src/main/**/build/
|
!**/src/main/**/build/
|
||||||
!**/src/test/**/build/
|
!**/src/test/**/build/
|
||||||
|
.vs
|
||||||
|
|
||||||
### IntelliJ IDEA ###
|
### IntelliJ IDEA ###
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
@@ -16,18 +16,20 @@
|
|||||||
|
|
||||||
- SSH and local terminal support
|
- SSH and local terminal support
|
||||||
- Serial port protocol support
|
- Serial port protocol support
|
||||||
- [SFTP](./docs/sftp.png?raw=1) file transfer support
|
- [SFTP](./docs/sftp.png?raw=1) & [Command](./docs/sftp-command.png?raw=1) file transfer support
|
||||||
- Compatible with Windows, macOS, and Linux
|
- Compatible with Windows, macOS, and Linux
|
||||||
- Zmodem protocol support
|
- Zmodem protocol support
|
||||||
- SSH port forwarding & Jump hosts
|
- SSH port forwarding & Jump hosts
|
||||||
|
- Support for X11 and SSH-Agent
|
||||||
- Terminal log
|
- Terminal log
|
||||||
- Configuration synchronization via [Gist](https://gist.github.com)
|
- Configuration synchronization via [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
||||||
- Macro support (record and replay scripts)
|
- Macro support (record and replay scripts)
|
||||||
- Keyword highlighting
|
- Keyword highlighting
|
||||||
- Key management
|
- Key management
|
||||||
- Broadcast commands to multiple sessions
|
- Broadcast commands to multiple sessions
|
||||||
- [Find Everywhere](./docs/findeverywhere.png?raw=1) quick navigation
|
- [Find Everywhere](./docs/findeverywhere.png?raw=1) quick navigation
|
||||||
- Data encryption
|
- Data encryption
|
||||||
|
- Support [plugins](https://www.termora.app/plugins)
|
||||||
- ...
|
- ...
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|||||||
@@ -12,18 +12,20 @@
|
|||||||
|
|
||||||
- 支持 SSH 和本地终端
|
- 支持 SSH 和本地终端
|
||||||
- 支持串口协议
|
- 支持串口协议
|
||||||
- 支持 [SFTP](./docs/sftp-zh_CN.png?raw=1) 文件传输
|
- 支持 [SFTP](./docs/sftp-zh_CN.png?raw=1) & [命令行](./docs/sftp-command.png?raw=1) 文件传输
|
||||||
- 支持 Windows、macOS、Linux 平台
|
- 支持 Windows、macOS、Linux 平台
|
||||||
- 支持 Zmodem 协议
|
- 支持 Zmodem 协议
|
||||||
- 支持 SSH 端口转发和跳板机
|
- 支持 SSH 端口转发和跳板机
|
||||||
|
- 支持 X11 和 SSH-Agent
|
||||||
- 终端日志记录
|
- 终端日志记录
|
||||||
- 支持配置同步到 [Gist](https://gist.github.com)
|
- 支持配置同步到 [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
||||||
- 支持宏(录制脚本并回放)
|
- 支持宏(录制脚本并回放)
|
||||||
- 支持关键词高亮
|
- 支持关键词高亮
|
||||||
- 支持密钥管理器
|
- 支持密钥管理器
|
||||||
- 支持将命令发送到多个会话
|
- 支持将命令发送到多个会话
|
||||||
- 支持 [Find Everywhere](./docs/findeverywhere-zh_CN.png?raw=1) 快速跳转
|
- 支持 [Find Everywhere](./docs/findeverywhere-zh_CN.png?raw=1) 快速跳转
|
||||||
- 支持数据加密
|
- 支持数据加密
|
||||||
|
- 支持[插件](https://www.termora.app/plugins)
|
||||||
- ...
|
- ...
|
||||||
|
|
||||||
## 下载
|
## 下载
|
||||||
|
|||||||
186
THIRDPARTY
@@ -1,240 +1,232 @@
|
|||||||
annotations 24.0.1
|
annotations
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/java-annotations/blob/master/LICENSE.txt
|
https://github.com/JetBrains/java-annotations/blob/master/LICENSE.txt
|
||||||
|
|
||||||
bip39-lib-jvm 1.0.8
|
colorpicker
|
||||||
MIT License
|
|
||||||
https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/LICENSE
|
|
||||||
|
|
||||||
colorpicker 2.0.1
|
|
||||||
BSD 3-Clause "New" or "Revised" License
|
BSD 3-Clause "New" or "Revised" License
|
||||||
https://github.com/dheid/colorpicker/blob/main/LICENSE
|
https://github.com/dheid/colorpicker/blob/main/LICENSE
|
||||||
|
|
||||||
commonmark 0.24.0
|
commonmark
|
||||||
BSD 2-Clause "Simplified" License
|
BSD 2-Clause "Simplified" License
|
||||||
https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt
|
https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt
|
||||||
|
|
||||||
commons-codec 1.17.1
|
commons-codec
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-codec/blob/master/LICENSE.txt
|
https://github.com/apache/commons-codec/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-compress 1.27.1
|
commons-vfs2
|
||||||
Apache License 2.0
|
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
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-io/blob/master/LICENSE.txt
|
https://github.com/apache/commons-io/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-lang3 3.17.0
|
commons-lang3
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-lang/blob/master/LICENSE.txt
|
https://github.com/apache/commons-lang/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-net 3.11.1
|
commons-net
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-net/blob/master/LICENSE.txt
|
https://github.com/apache/commons-net/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-text 1.12.0
|
commons-text
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-text/blob/master/LICENSE.txt
|
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
|
Creative Commons Zero v1.0 Universal
|
||||||
https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt
|
https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt
|
||||||
|
|
||||||
flatlaf 3.5.4
|
flatlaf
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
flatlaf 3.5.4-no-natives
|
flatlaf-no-natives
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
flatlaf-extras 3.5.4
|
flatlaf-extras
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
flatlaf-swingx 3.5.4
|
flatlaf-swingx
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
JavaEWAH 1.2.3
|
JavaEWAH
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/lemire/javaewah/blob/master/LICENSE
|
https://github.com/lemire/javaewah/blob/master/LICENSE
|
||||||
|
|
||||||
jbr-api 17.1.10.1
|
jbr-api
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/JetBrainsRuntimeApi/blob/main/LICENSE
|
https://github.com/JetBrains/JetBrainsRuntimeApi/blob/main/LICENSE
|
||||||
|
|
||||||
jcl-over-slf4j 1.7.36
|
jcl-over-slf4j
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0.txt
|
https://www.apache.org/licenses/LICENSE-2.0.txt
|
||||||
|
|
||||||
jfa 1.2.0
|
jfa
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/0x4a616e/jfa/blob/main/LICENSE
|
https://github.com/0x4a616e/jfa/blob/main/LICENSE
|
||||||
|
|
||||||
jgoodies-common 1.8.1
|
jgoodies-common
|
||||||
BSD-2-Clause License
|
BSD-2-Clause License
|
||||||
http://www.opensource.org/licenses/bsd-license.html
|
http://www.opensource.org/licenses/bsd-license.html
|
||||||
|
|
||||||
jgoodies-forms 1.9.0
|
jgoodies-forms
|
||||||
BSD-2-Clause License
|
BSD-2-Clause License
|
||||||
http://www.opensource.org/licenses/bsd-license.html
|
http://www.opensource.org/licenses/bsd-license.html
|
||||||
|
|
||||||
jna 5.16.0
|
jna
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/java-native-access/jna/blob/master/AL2.0
|
https://github.com/java-native-access/jna/blob/master/AL2.0
|
||||||
|
|
||||||
jna-platform 5.16.0
|
jna-platform
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/java-native-access/jna/blob/master/AL2.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
|
BSD 3-Clause "New" or "Revised" License
|
||||||
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
||||||
|
|
||||||
jnafilechooser-win32 1.1.2
|
jnafilechooser-win32
|
||||||
BSD 3-Clause "New" or "Revised" License
|
BSD 3-Clause "New" or "Revised" License
|
||||||
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
||||||
|
|
||||||
jsvg 1.4.0
|
jsvg
|
||||||
MIT License
|
MIT License
|
||||||
https://github.com/weisJ/jsvg/blob/master/LICENSE
|
https://github.com/weisJ/jsvg/blob/master/LICENSE
|
||||||
|
|
||||||
jSystemThemeDetector 3.9.1
|
jSystemThemeDetector
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/Dansoftowner/jSystemThemeDetector/blob/master/LICENSE
|
https://github.com/Dansoftowner/jSystemThemeDetector/blob/master/LICENSE
|
||||||
|
|
||||||
kotlin-logging 1.7.9
|
kotlin-logging
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/oshai/kotlin-logging/blob/master/LICENSE
|
https://github.com/oshai/kotlin-logging/blob/master/LICENSE
|
||||||
|
|
||||||
kotlin-stdlib 2.1.0
|
kotlin-stdlib
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
kotlin-stdlib-jdk7 1.9.10
|
kotlin-reflect
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
kotlin-stdlib-jdk8 1.9.10
|
kotlin-stdlib-jdk7
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
kotlin-stdlib-jdk8 1.9.10
|
kotlin-stdlib-jdk8
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
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
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
kotlinx-coroutines-swing 1.10.1
|
kotlinx-coroutines-swing
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/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
|
Apache License 2.0
|
||||||
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
||||||
|
|
||||||
kotlinx-serialization-json-jvm 1.7.3
|
logging-interceptor
|
||||||
Apache License 2.0
|
|
||||||
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
|
||||||
|
|
||||||
logging-interceptor 4.12.0
|
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
okhttp 4.12.0
|
okhttp
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
okio-jvm 3.6.0
|
okio-jvm
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/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
|
Eclipse Distribution License
|
||||||
https://www.eclipse.org/org/documents/edl-v10.php
|
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
|
Eclipse Distribution License
|
||||||
https://www.eclipse.org/org/documents/edl-v10.php
|
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
|
MIT License
|
||||||
https://github.com/oshi/oshi/blob/master/LICENSE
|
https://github.com/oshi/oshi/blob/master/LICENSE
|
||||||
|
|
||||||
pty4j 0.13.2
|
pty4j
|
||||||
Eclipse Public License 1.0
|
Eclipse Public License 1.0
|
||||||
https://github.com/JetBrains/pty4j/blob/master/LICENSE
|
https://github.com/JetBrains/pty4j/blob/master/LICENSE
|
||||||
|
|
||||||
slf4j-api 2.0.16
|
slf4j-api
|
||||||
MIT License
|
MIT License
|
||||||
https://github.com/qos-ch/slf4j/blob/master/LICENSE.txt
|
https://github.com/qos-ch/slf4j/blob/master/LICENSE.txt
|
||||||
|
|
||||||
slf4j-tinylog 2.7.0
|
slf4j-tinylog
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||||
|
|
||||||
sshd-common 2.14.0
|
sshd-common
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
sshd-core 2.14.0
|
sshd-core
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
sshd-osgi 2.14.0
|
sshd-osgi
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
sshd-sftp 2.14.0
|
sshd-sftp
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/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
|
GNU LESSER GENERAL PUBLIC LICENSE v3
|
||||||
https://www.gnu.org/licenses/lgpl-3.0
|
https://www.gnu.org/licenses/lgpl-3.0
|
||||||
|
|
||||||
tinylog-api 2.7.0
|
tinylog-api
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||||
|
|
||||||
tinylog-impl 2.7.0
|
tinylog-impl
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||||
|
|
||||||
versioncompare 1.4.1
|
versioncompare
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/G00fY2/version-compare/blob/main/LICENSE
|
https://github.com/G00fY2/version-compare/blob/main/LICENSE
|
||||||
|
|
||||||
xodus-compress 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
|
jediterm
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
|
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
|
||||||
|
|
||||||
mixpanel-java 1.5.3
|
mixpanel-java
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/mixpanel/mixpanel-java/blob/master/LICENSE
|
https://github.com/mixpanel/mixpanel-java/blob/master/LICENSE
|
||||||
|
|
||||||
@@ -242,6 +234,34 @@ json-20231013
|
|||||||
Public Domain.
|
Public Domain.
|
||||||
https://github.com/stleary/JSON-java/blob/master/LICENSE
|
https://github.com/stleary/JSON-java/blob/master/LICENSE
|
||||||
|
|
||||||
jSerialComm 2.11.0
|
jSerialComm
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0
|
https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0
|
||||||
|
|
||||||
|
exposed-core
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
|
||||||
|
|
||||||
|
exposed-crypt
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
|
||||||
|
|
||||||
|
exposed-jdbc
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
|
||||||
|
|
||||||
|
sqlite-jdbc
|
||||||
|
Apache License 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0.txt
|
||||||
|
|
||||||
|
java-uuid-generator
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/cowtowncoder/java-uuid-generator/blob/master/LICENSE
|
||||||
|
|
||||||
|
semver4j
|
||||||
|
MIT
|
||||||
|
https://github.com/semver4j/semver4j/blob/main/LICENSE
|
||||||
|
|
||||||
|
dom4j
|
||||||
|
Plexus (https://dom4j.github.io)
|
||||||
|
https://github.com/dom4j/dom4j/blob/master/LICENSE
|
||||||
752
build.gradle.kts
@@ -3,22 +3,32 @@ import org.gradle.kotlin.dsl.support.uppercaseFirstChar
|
|||||||
import org.gradle.nativeplatform.platform.internal.ArchitectureInternal
|
import org.gradle.nativeplatform.platform.internal.ArchitectureInternal
|
||||||
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||||
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
|
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
|
||||||
|
import org.jetbrains.kotlin.org.apache.commons.io.filefilter.FileFilterUtils
|
||||||
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
||||||
|
import org.jetbrains.kotlin.org.apache.commons.lang3.time.DateFormatUtils
|
||||||
|
import java.io.FileNotFoundException
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.Future
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
|
idea
|
||||||
application
|
application
|
||||||
|
`maven-publish`
|
||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
alias(libs.plugins.kotlinx.serialization)
|
alias(libs.plugins.kotlinx.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
group = "app.termora"
|
group = "app.termora"
|
||||||
version = "1.0.7"
|
version = rootProject.projectDir.resolve("VERSION").readText().trim()
|
||||||
|
|
||||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||||
|
val appVersion = project.version.toString().split("-")[0]
|
||||||
|
val isDeb = os.isLinux && System.getProperty("type") == "deb"
|
||||||
|
|
||||||
// macOS 签名信息
|
// macOS 签名信息
|
||||||
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
||||||
@@ -30,15 +40,16 @@ val macOSNotaryKeychainProfile = System.getenv("TERMORA_MAC_NOTARY_KEYCHAIN_PROF
|
|||||||
val macOSNotary = macOSSign && macOSNotaryKeychainProfile.isNotBlank()
|
val macOSNotary = macOSSign && macOSNotaryKeychainProfile.isNotBlank()
|
||||||
&& System.getenv("TERMORA_MAC_NOTARY").toBoolean()
|
&& System.getenv("TERMORA_MAC_NOTARY").toBoolean()
|
||||||
|
|
||||||
repositories {
|
allprojects {
|
||||||
mavenCentral()
|
repositories {
|
||||||
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies")
|
mavenCentral()
|
||||||
maven("https://www.jitpack.io")
|
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies")
|
||||||
|
maven("https://www.jitpack.io")
|
||||||
|
maven("https://central.sonatype.com/repository/maven-snapshots")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// 由于签名和公证,macOS 不携带 natives
|
|
||||||
val useNoNativesFlatLaf = os.isMacOsX && System.getenv("ENABLE_BUILD").toBoolean()
|
|
||||||
|
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
testImplementation(libs.hutool)
|
testImplementation(libs.hutool)
|
||||||
@@ -48,84 +59,83 @@ dependencies {
|
|||||||
testImplementation(libs.delight.rhino.sandbox)
|
testImplementation(libs.delight.rhino.sandbox)
|
||||||
testImplementation(platform(libs.testcontainers.bom))
|
testImplementation(platform(libs.testcontainers.bom))
|
||||||
testImplementation(libs.testcontainers)
|
testImplementation(libs.testcontainers)
|
||||||
|
testImplementation(libs.h2)
|
||||||
|
testImplementation(libs.exposed.migration)
|
||||||
|
|
||||||
// implementation(platform(libs.koin.bom))
|
// implementation(platform(libs.koin.bom))
|
||||||
// implementation(libs.koin.core)
|
// implementation(libs.koin.core)
|
||||||
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)
|
|
||||||
|
|
||||||
implementation(libs.flatlaf) {
|
api(kotlin("reflect"))
|
||||||
artifact {
|
api(libs.slf4j.api)
|
||||||
if (useNoNativesFlatLaf) {
|
api(libs.pty4j)
|
||||||
classifier = "no-natives"
|
api(libs.slf4j.tinylog)
|
||||||
}
|
api(libs.tinylog.impl)
|
||||||
}
|
api(libs.commons.codec)
|
||||||
}
|
api(libs.commons.io)
|
||||||
implementation(libs.flatlaf.extras) {
|
api(libs.commons.lang3)
|
||||||
if (useNoNativesFlatLaf) {
|
api(libs.commons.csv)
|
||||||
exclude(group = "com.formdev", module = "flatlaf")
|
api(libs.commons.net)
|
||||||
}
|
api(libs.commons.text)
|
||||||
}
|
api(libs.kotlinx.coroutines.swing)
|
||||||
implementation(libs.flatlaf.swingx) {
|
api(libs.kotlinx.coroutines.core)
|
||||||
if (useNoNativesFlatLaf) {
|
|
||||||
exclude(group = "com.formdev", module = "flatlaf")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation(libs.kotlinx.serialization.json)
|
api(libs.flatlaf)
|
||||||
implementation(libs.swingx)
|
api(libs.flatlafextras)
|
||||||
implementation(libs.jgoodies.forms)
|
api(libs.flatlafswingx)
|
||||||
implementation(libs.jna)
|
|
||||||
implementation(libs.jna.platform)
|
api(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.versioncompare)
|
api(libs.swingx)
|
||||||
implementation(libs.oshi.core)
|
api(libs.jgoodies.forms)
|
||||||
implementation(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
api(libs.jna)
|
||||||
implementation(libs.jfa) { exclude(group = "*", module = "*") }
|
api(libs.jna.platform)
|
||||||
implementation(libs.jbr.api)
|
api(libs.versioncompare)
|
||||||
implementation(libs.okhttp)
|
api(libs.oshi.core)
|
||||||
implementation(libs.okhttp.logging)
|
api(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
||||||
implementation(libs.sshd.core)
|
api(libs.jfa) { exclude(group = "*", module = "*") }
|
||||||
implementation(libs.commonmark)
|
api(libs.jbr.api)
|
||||||
implementation(libs.jgit)
|
api(libs.okhttp)
|
||||||
implementation(libs.jgit.sshd)
|
api(libs.okhttp.logging)
|
||||||
implementation(libs.jnafilechooser)
|
api(libs.sshd.core)
|
||||||
implementation(libs.xodus.vfs)
|
api(libs.commonmark)
|
||||||
implementation(libs.xodus.openAPI)
|
api(libs.jgit)
|
||||||
implementation(libs.xodus.environment)
|
api(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
||||||
implementation(libs.bip39)
|
api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
||||||
implementation(libs.colorpicker)
|
api(libs.eddsa)
|
||||||
implementation(libs.mixpanel)
|
api(libs.jnafilechooser)
|
||||||
implementation(libs.jSerialComm)
|
|
||||||
|
api(libs.colorpicker)
|
||||||
|
api(libs.mixpanel)
|
||||||
|
api(libs.jSerialComm)
|
||||||
|
api(libs.ini4j)
|
||||||
|
api(libs.restart4j)
|
||||||
|
api(libs.exposed.core)
|
||||||
|
api(libs.exposed.crypt)
|
||||||
|
api(libs.exposed.jdbc)
|
||||||
|
api(libs.sqlite)
|
||||||
|
api(libs.jug)
|
||||||
|
api(libs.semver4j)
|
||||||
|
api(libs.jsvg)
|
||||||
|
api(libs.dom4j) { exclude(group = "*", module = "*") }
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
val args = mutableListOf(
|
val args = mutableListOf(
|
||||||
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
"-Xmx2048m",
|
||||||
"-Xmx2g",
|
"-Drelease-date=${DateFormatUtils.format(Date(), "yyyy-MM-dd")}",
|
||||||
"-XX:+UseZGC",
|
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
|
||||||
"-XX:+ZUncommit",
|
|
||||||
"-XX:+ZGenerational",
|
|
||||||
"-XX:ZUncommitDelay=60",
|
|
||||||
"-XX:SoftMaxHeapSize=64m"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
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("-Dsun.java2d.metal=true")
|
||||||
args.add("-Dapple.awt.application.appearance=system")
|
args.add("-Dapple.awt.application.appearance=system")
|
||||||
}
|
}
|
||||||
|
|
||||||
args.add("-Dapp-version=${project.version}")
|
args.add("-DTERMORA_PLUGIN_DIRECTORY=${layout.buildDirectory.get().asFile.absolutePath}${File.separator}plugins")
|
||||||
|
|
||||||
if (os.isLinux) {
|
if (os.isLinux) {
|
||||||
args.add("-Dsun.java2d.opengl=true")
|
args.add("-Dsun.java2d.opengl=true")
|
||||||
@@ -135,6 +145,38 @@ application {
|
|||||||
mainClass = "app.termora.MainKt"
|
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 {
|
tasks.test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
@@ -142,16 +184,19 @@ tasks.test {
|
|||||||
tasks.register<Copy>("copy-dependencies") {
|
tasks.register<Copy>("copy-dependencies") {
|
||||||
val dir = layout.buildDirectory.dir("libs")
|
val dir = layout.buildDirectory.dir("libs")
|
||||||
from(configurations.runtimeClasspath).into(dir)
|
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 的本地库提取
|
// 对 JNA 和 PTY4J 的本地库提取
|
||||||
// 提取出来是为了单独签名,不然无法通过公证
|
// 提取出来是为了单独签名,不然无法通过公证
|
||||||
if (os.isMacOsX && macOSSign) {
|
if (os.isMacOsX && macOSSign) {
|
||||||
doLast {
|
doLast {
|
||||||
val jna = libs.jna.asProvider().get()
|
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
||||||
val dylib = dir.get().dir("dylib").asFile
|
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()) {
|
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
||||||
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
||||||
val targetDir = File(dylib, jna.name)
|
val targetDir = File(dylib, jna.name)
|
||||||
@@ -177,7 +222,6 @@ tasks.register<Copy>("copy-dependencies") {
|
|||||||
// 删除所有二进制类库
|
// 删除所有二进制类库
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
|
||||||
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) {
|
} 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)
|
val targetDir = FileUtils.getFile(dylib, jSerialComm.name, "OSX", archName)
|
||||||
FileUtils.forceMkdir(targetDir)
|
FileUtils.forceMkdir(targetDir)
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
@@ -191,6 +235,40 @@ tasks.register<Copy>("copy-dependencies") {
|
|||||||
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") }
|
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 +281,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 +400,7 @@ tasks.register<Exec>("jlink") {
|
|||||||
"java.logging",
|
"java.logging",
|
||||||
"java.management",
|
"java.management",
|
||||||
"java.rmi",
|
"java.rmi",
|
||||||
|
"java.sql",
|
||||||
"java.security.jgss",
|
"java.security.jgss",
|
||||||
"jdk.crypto.ec",
|
"jdk.crypto.ec",
|
||||||
"jdk.unsupported",
|
"jdk.unsupported",
|
||||||
@@ -238,34 +426,38 @@ tasks.register<Exec>("jpackage") {
|
|||||||
|
|
||||||
val buildDir = layout.buildDirectory.get()
|
val buildDir = layout.buildDirectory.get()
|
||||||
val options = mutableListOf(
|
val options = mutableListOf(
|
||||||
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
"-Xmx2048m",
|
||||||
"-Xmx2g",
|
|
||||||
"-XX:+UseZGC",
|
|
||||||
"-XX:+ZUncommit",
|
|
||||||
"-XX:+ZGenerational",
|
|
||||||
"-XX:ZUncommitDelay=60",
|
|
||||||
"-XX:SoftMaxHeapSize=64m",
|
|
||||||
"-XX:+HeapDumpOnOutOfMemoryError",
|
"-XX:+HeapDumpOnOutOfMemoryError",
|
||||||
"-Dlogger.console.level=off",
|
"-Dlogger.console.level=off",
|
||||||
"-Dkotlinx.coroutines.debug=off",
|
"-Dkotlinx.coroutines.debug=off",
|
||||||
"-Dapp-version=${project.version}",
|
"-Dapp-version=${project.version}",
|
||||||
|
"-Drelease-date=${DateFormatUtils.format(Date(), "yyyy-MM-dd")}",
|
||||||
|
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
||||||
)
|
)
|
||||||
|
|
||||||
options.add("-Dsun.java2d.metal=true")
|
options.add("-Dsun.java2d.metal=true")
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
|
// NSWindow
|
||||||
options.add("-Dapple.awt.application.appearance=system")
|
options.add("-Dapple.awt.application.appearance=system")
|
||||||
|
options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||||
|
options.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||||
|
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||||
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||||
|
options.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (os.isLinux) {
|
if (os.isLinux) {
|
||||||
options.add("-Dsun.java2d.opengl=true")
|
options.add("-Dsun.java2d.opengl=true")
|
||||||
|
if (isDeb) {
|
||||||
|
options.add("-Djpackage.app-layout=deb")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage")
|
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage")
|
||||||
arguments.addAll(listOf("--runtime-image", "${buildDir}/jlink"))
|
arguments.addAll(listOf("--runtime-image", "${buildDir}/jlink"))
|
||||||
arguments.addAll(listOf("--name", project.name.uppercaseFirstChar()))
|
arguments.addAll(listOf("--name", project.name.uppercaseFirstChar()))
|
||||||
arguments.addAll(listOf("--app-version", "${project.version}"))
|
arguments.addAll(listOf("--app-version", appVersion))
|
||||||
arguments.addAll(listOf("--main-jar", tasks.jar.get().archiveFileName.get()))
|
arguments.addAll(listOf("--main-jar", tasks.jar.get().archiveFileName.get()))
|
||||||
arguments.addAll(listOf("--main-class", application.mainClass.get()))
|
arguments.addAll(listOf("--main-class", application.mainClass.get()))
|
||||||
arguments.addAll(listOf("--input", "$buildDir/libs"))
|
arguments.addAll(listOf("--input", "$buildDir/libs"))
|
||||||
@@ -274,7 +466,18 @@ tasks.register<Exec>("jpackage") {
|
|||||||
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
|
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
|
||||||
arguments.addAll(listOf("--vendor", "TermoraDev"))
|
arguments.addAll(listOf("--vendor", "TermoraDev"))
|
||||||
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
||||||
arguments.addAll(listOf("--description", "A terminal emulator and SSH client."))
|
arguments.addAll(listOf("--app-content", "$buildDir/plugins"))
|
||||||
|
|
||||||
|
if (os.isWindows) {
|
||||||
|
arguments.addAll(
|
||||||
|
listOf(
|
||||||
|
"--description",
|
||||||
|
"${project.name.uppercaseFirstChar()}: A terminal emulator and SSH client"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
arguments.addAll(listOf("--description", "A terminal emulator and SSH client."))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
@@ -292,6 +495,10 @@ tasks.register<Exec>("jpackage") {
|
|||||||
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
|
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (os.isLinux) {
|
||||||
|
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.png"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
arguments.add("--type")
|
arguments.add("--type")
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
@@ -299,7 +506,11 @@ tasks.register<Exec>("jpackage") {
|
|||||||
} else if (os.isWindows) {
|
} else if (os.isWindows) {
|
||||||
arguments.add("msi")
|
arguments.add("msi")
|
||||||
} else if (os.isLinux) {
|
} 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 {
|
} else {
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
@@ -316,142 +527,272 @@ tasks.register<Exec>("jpackage") {
|
|||||||
|
|
||||||
tasks.register("dist") {
|
tasks.register("dist") {
|
||||||
doLast {
|
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 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, "clean") }
|
||||||
|
|
||||||
|
// 构建自带的插件
|
||||||
|
exec { commandLine(gradlew, ":plugins:migration:build") }
|
||||||
|
|
||||||
// 打包并复制依赖
|
// 打包并复制依赖
|
||||||
exec {
|
exec {
|
||||||
commandLine(gradlew, "jar", "copy-dependencies")
|
commandLine(gradlew, ":jar", ":copy-dependencies")
|
||||||
environment("ENABLE_BUILD" to true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查依赖的开源协议
|
// 检查依赖的开源协议
|
||||||
exec { commandLine(gradlew, "check-license") }
|
exec { commandLine(gradlew, ":check-license") }
|
||||||
|
|
||||||
// jlink
|
// jlink
|
||||||
exec { commandLine(gradlew, "jlink") }
|
exec { commandLine(gradlew, ":jlink") }
|
||||||
|
|
||||||
// 打包
|
// 打包
|
||||||
exec { commandLine(gradlew, "jpackage") }
|
exec { commandLine(gradlew, ":jpackage", "-Dtype=${System.getProperty("type")}") }
|
||||||
|
|
||||||
// pack
|
// 根据不同的系统构建不同的二进制包
|
||||||
if (os.isWindows) { // zip and msi
|
pack()
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 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") {
|
tasks.register("check-license") {
|
||||||
doLast {
|
doLast {
|
||||||
val thirdParty = mutableMapOf<String, String>()
|
|
||||||
val iterator = File(projectDir, "THIRDPARTY").readLines().iterator()
|
val iterator = File(projectDir, "THIRDPARTY").readLines().iterator()
|
||||||
val thirdPartyNames = mutableSetOf<String>()
|
val thirdPartyNames = mutableSetOf<String>()
|
||||||
|
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
val nameWithVersion = iterator.next()
|
val name = iterator.next()
|
||||||
if (nameWithVersion.isBlank()) {
|
if (name.isBlank()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore license name
|
// ignore license name
|
||||||
iterator.next()
|
iterator.next()
|
||||||
|
// ignore license url
|
||||||
|
iterator.next()
|
||||||
|
|
||||||
val license = iterator.next()
|
thirdPartyNames.add(name)
|
||||||
thirdParty[nameWithVersion.replace(StringUtils.SPACE, "-")] = license
|
|
||||||
thirdPartyNames.add(nameWithVersion.split(StringUtils.SPACE).first())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (file in configurations.runtimeClasspath.get()) {
|
for (dependency in configurations.runtimeClasspath.get().allDependencies) {
|
||||||
val name = file.nameWithoutExtension
|
if (!thirdPartyNames.contains(dependency.name)) {
|
||||||
if (!thirdParty.containsKey(name)) {
|
throw GradleException("${dependency.name} No license found")
|
||||||
if (logger.isWarnEnabled) {
|
|
||||||
logger.warn("$name does not exist in third-party")
|
|
||||||
}
|
|
||||||
if (!thirdPartyNames.contains(name)) {
|
|
||||||
throw GradleException("$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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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("export LinuxAppImage=true").appendLine()
|
||||||
|
sb.append("exec \"\${HERE}/bin/${termoraName}\" \"$@\"")
|
||||||
|
appRun.writeText(sb.toString())
|
||||||
|
appRun.setExecutable(true)
|
||||||
|
|
||||||
|
// AppImage
|
||||||
|
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=AppImage").toString())
|
||||||
|
exec {
|
||||||
|
commandLine(appimagetool.absolutePath, termoraName, "${finalFilenameWithoutExtension}.AppImage")
|
||||||
|
workingDir = distributionDir.asFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* macOS 对本地文件进行签名
|
* macOS 对本地文件进行签名
|
||||||
*/
|
*/
|
||||||
@@ -471,11 +812,54 @@ 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 {
|
kotlin {
|
||||||
jvmToolchain {
|
jvmToolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(21)
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
@Suppress("UnstableApiUsage")
|
}
|
||||||
vendor = JvmVendorSpec.JETBRAINS
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
idea {
|
||||||
|
module {
|
||||||
|
isDownloadJavadoc = true
|
||||||
|
isDownloadSources = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BIN
docs/sftp-command.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
@@ -1,47 +1,53 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.1.0"
|
kotlin = "2.2.0"
|
||||||
slf4j = "2.0.16"
|
slf4j = "2.0.17"
|
||||||
pty4j = "0.13.2"
|
pty4j = "0.13.6"
|
||||||
tinylog = "2.7.0"
|
tinylog = "2.7.0"
|
||||||
kotlinx-coroutines = "1.10.1"
|
kotlinx-coroutines = "1.10.2"
|
||||||
flatlaf = "3.5.4"
|
flatlaf = "3.6"
|
||||||
trove4j = "1.0.20200330"
|
kotlinx-serialization-json = "1.8.1"
|
||||||
kotlinx-serialization-json = "1.7.3"
|
commons-codec = "1.18.0"
|
||||||
commons-codec = "1.17.1"
|
|
||||||
commons-lang3 = "3.17.0"
|
commons-lang3 = "3.17.0"
|
||||||
|
commons-csv = "1.14.0"
|
||||||
commons-net = "3.11.1"
|
commons-net = "3.11.1"
|
||||||
commons-text = "1.12.0"
|
commons-text = "1.13.1"
|
||||||
commons-compress = "1.27.1"
|
commons-compress = "1.27.1"
|
||||||
koin-bom = "4.0.0"
|
commons-vfs2 = "2.10.0"
|
||||||
swingx = "1.6.5-1"
|
swingx = "1.6.5-1"
|
||||||
jgoodies-forms = "1.9.0"
|
jgoodies-forms = "1.9.0"
|
||||||
jfa = "1.2.0"
|
jfa = "1.2.0"
|
||||||
oshi = "6.6.5"
|
oshi = "6.8.1"
|
||||||
versioncompare = "1.4.1"
|
versioncompare = "1.4.1"
|
||||||
jna = "5.16.0"
|
jna = "5.17.0"
|
||||||
jSystemThemeDetector = "3.9.1"
|
jSystemThemeDetector = "3.9.1"
|
||||||
commons-io = "2.18.0"
|
commons-io = "2.19.0"
|
||||||
jbr-api = "17.1.10.1"
|
jbr-api = "17.1.10.1"
|
||||||
leveldb = "0.12"
|
hutool = "5.8.37"
|
||||||
guava = "33.3.1-jre"
|
jsch = "0.2.26"
|
||||||
credential-secure-storage = "1.0.3"
|
|
||||||
hutool = "5.8.34"
|
|
||||||
jsch = "0.2.21"
|
|
||||||
okhttp = "4.12.0"
|
okhttp = "4.12.0"
|
||||||
bcprov = "1.79"
|
|
||||||
sshj = "0.39.0"
|
sshj = "0.39.0"
|
||||||
sshd-core = "2.14.0"
|
sshd-core = "2.15.0"
|
||||||
jgit = "7.1.0.202411261347-r"
|
jgit = "7.2.0.202503040940-r"
|
||||||
commonmark = "0.24.0"
|
commonmark = "0.25.0"
|
||||||
jnafilechooser = "1.1.2"
|
jnafilechooser = "1.1.2"
|
||||||
xodus = "2.0.1"
|
xodus = "2.0.1"
|
||||||
bip39 = "1.0.8"
|
bip39 = "1.0.9"
|
||||||
colorpicker = "2.0.1"
|
colorpicker = "2.0.1"
|
||||||
rhino = "1.7.15"
|
rhino = "1.8.0"
|
||||||
delight-rhino-sandbox = "0.0.17"
|
delight-rhino-sandbox = "0.0.17"
|
||||||
testcontainers = "1.20.4"
|
testcontainers = "1.21.2"
|
||||||
mixpanel = "1.5.3"
|
mixpanel = "1.5.3"
|
||||||
jSerialComm="2.11.0"
|
jSerialComm = "2.11.0"
|
||||||
|
ini4j = "0.5.5-2"
|
||||||
|
restart4j = "0.0.1"
|
||||||
|
eddsa = "0.3.0"
|
||||||
|
exposed = "1.0.0-beta-2"
|
||||||
|
h2 = "2.3.232"
|
||||||
|
sqlite = "3.50.1.0"
|
||||||
|
jug = "5.1.0"
|
||||||
|
semver4j = "5.8.0"
|
||||||
|
jsvg = "1.4.0"
|
||||||
|
dom4j = "2.1.4"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||||
@@ -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-codec = { group = "commons-codec", name = "commons-codec", version.ref = "commons-codec" }
|
||||||
commons-net = { group = "commons-net", name = "commons-net", version.ref = "commons-net" }
|
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-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-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-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" }
|
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 = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
|
||||||
flatlaf-extras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" }
|
flatlafextras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" }
|
||||||
trove4j = { group = "org.jetbrains.intellij.deps", name = "trove4j", version.ref = "trove4j" }
|
flatlafswingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
|
||||||
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
|
|
||||||
testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" }
|
testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" }
|
||||||
testcontainers = { module = "org.testcontainers:testcontainers" }
|
testcontainers = { module = "org.testcontainers:testcontainers" }
|
||||||
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" }
|
swingx = { module = "org.swinglabs.swingx:swingx-all", version.ref = "swingx" }
|
||||||
jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" }
|
jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" }
|
||||||
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
||||||
@@ -72,33 +80,39 @@ versioncompare = { module = "io.github.g00fy2:versioncompare", version.ref = "ve
|
|||||||
jfa = { module = "de.jangassen:jfa", version.ref = "jfa" }
|
jfa = { module = "de.jangassen:jfa", version.ref = "jfa" }
|
||||||
oshi-core = { module = "com.github.oshi:oshi-core", version.ref = "oshi" }
|
oshi-core = { module = "com.github.oshi:oshi-core", version.ref = "oshi" }
|
||||||
commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
|
commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
|
||||||
|
restart4j = { module = "com.github.hstyi:restart4j", version.ref = "restart4j" }
|
||||||
jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" }
|
jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" }
|
||||||
flatlaf-swingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
|
|
||||||
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" }
|
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" }
|
jsch = { module = "com.github.mwiede:jsch", version.ref = "jsch" }
|
||||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", 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" }
|
sshj = { module = "com.hierynomus:sshj", version.ref = "sshj" }
|
||||||
sshd-core = { module = "org.apache.sshd:sshd-core", version.ref = "sshd-core" }
|
sshd-core = { module = "org.apache.sshd:sshd-core", version.ref = "sshd-core" }
|
||||||
jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" }
|
jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" }
|
||||||
commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
|
commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
|
||||||
jgit-sshd = { module = "org.eclipse.jgit:org.eclipse.jgit.ssh.apache", version.ref = "jgit" }
|
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-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-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" }
|
xodus-vfs = { module = "org.jetbrains.xodus:xodus-vfs", version.ref = "xodus" }
|
||||||
jnafilechooser = { module = "com.github.steos.jnafilechooser:jnafilechooser-api", version.ref = "jnafilechooser" }
|
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" }
|
rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
||||||
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
||||||
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
|
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
|
||||||
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
|
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
|
||||||
jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "jSerialComm" }
|
jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "jSerialComm" }
|
||||||
|
eddsa = { module = "net.i2p.crypto:eddsa", version.ref = "eddsa" }
|
||||||
|
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
|
||||||
|
exposed-crypt = { module = "org.jetbrains.exposed:exposed-crypt", version.ref = "exposed" }
|
||||||
|
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
||||||
|
exposed-migration = { module = "org.jetbrains.exposed:exposed-migration", version.ref = "exposed" }
|
||||||
|
h2 = { module = "com.h2database:h2", version.ref = "h2" }
|
||||||
|
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
|
||||||
|
jug = { module = "com.fasterxml.uuid:java-uuid-generator", version.ref = "jug" }
|
||||||
|
jsvg = { module = "com.github.weisj:jsvg", version.ref = "jsvg" }
|
||||||
|
dom4j = { module = "org.dom4j:dom4j", version.ref = "dom4j" }
|
||||||
|
semver4j = { module = "org.semver4j:semver4j", version.ref = "semver4j" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.10.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
10
plugins/LICENSE
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Copyright (c) 2025-present hstyi
|
||||||
|
|
||||||
|
The files in this catalogue are for public access only. Specific descriptions are given below:
|
||||||
|
|
||||||
|
- You may view and study the contents of these files;
|
||||||
|
- You may NOT use them for any commercial purpose;
|
||||||
|
- You may NOT modify, copy, distribute, republish, or use them to create derivative works;
|
||||||
|
- Written permission must be obtained from the author for any use beyond personal viewing.
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
75
plugins/THIRDPARTY
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
minio
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/minio/minio-java/blob/master/LICENSE
|
||||||
|
|
||||||
|
aliyun-sdk-oss
|
||||||
|
Apache License 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
|
||||||
|
jaxb-api
|
||||||
|
BSD 3-Clause "New" or "Revised" License
|
||||||
|
https://github.com/jakartaee/jaxb-api/blob/master/LICENSE.md
|
||||||
|
|
||||||
|
activation
|
||||||
|
COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.1
|
||||||
|
https://github.com/javaee/activation/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
jaxb-runtime
|
||||||
|
BSD 3-Clause "New" or "Revised" License
|
||||||
|
https://github.com/eclipse-ee4j/jaxb-ri/blob/master/LICENSE.md
|
||||||
|
|
||||||
|
esdk-obs-java-bundle
|
||||||
|
HUAWEI LICENSE
|
||||||
|
https://github.com/huaweicloud/huaweicloud-sdk-java-obs/blob/master/LICENSE
|
||||||
|
|
||||||
|
xodus-compress
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
xodus-environment
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
xodus-openAPI
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
xodus-utils
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
xodus-vfs
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
kotlin-bip39
|
||||||
|
MIT License
|
||||||
|
https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/LICENSE
|
||||||
|
|
||||||
|
commons-compress
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/apache/commons-compress/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
cos_api
|
||||||
|
MIT License
|
||||||
|
https://github.com/tencentyun/cos-java-sdk-v5/blob/master/LICENSE
|
||||||
|
|
||||||
|
AutoComplete
|
||||||
|
BSD-3-Clause license
|
||||||
|
https://github.com/bobbylight/AutoComplete/blob/master/LICENSE.md
|
||||||
|
|
||||||
|
RSTALanguageSupport
|
||||||
|
BSD-3-Clause license
|
||||||
|
https://github.com/bobbylight/RSTALanguageSupport/blob/master/README.md
|
||||||
|
|
||||||
|
RSyntaxTextArea
|
||||||
|
BSD-3-Clause license
|
||||||
|
https://github.com/bobbylight/RSyntaxTextArea/blob/master/LICENSE.md
|
||||||
|
|
||||||
|
MaxMind GeoIP2 API
|
||||||
|
Apache License, Version 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
|
||||||
|
GeoLite2 (https://www.maxmind.com)
|
||||||
|
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
|
||||||
|
https://creativecommons.org/licenses/by-sa/4.0/
|
||||||
16
plugins/bg/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
project.version = "0.0.4"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
compileOnly(project(":"))
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.EnableManager
|
||||||
|
import app.termora.database.DatabaseManager
|
||||||
|
|
||||||
|
object Appearance {
|
||||||
|
private val enableManager get() = EnableManager.getInstance()
|
||||||
|
private val appearance get() = DatabaseManager.getInstance().appearance
|
||||||
|
|
||||||
|
var backgroundImage: String
|
||||||
|
get() = enableManager.getFlag("Plugins.bg.backgroundImage", appearance.backgroundImage)
|
||||||
|
set(value) {
|
||||||
|
enableManager.setFlag("Plugins.bg.backgroundImage", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
var interval: Int
|
||||||
|
get() = enableManager.getFlag("Plugins.bg.interval", 360)
|
||||||
|
set(value) {
|
||||||
|
enableManager.setFlag("Plugins.bg.interval", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.GlassPaneExtension
|
||||||
|
import app.termora.WindowScope
|
||||||
|
import com.formdev.flatlaf.FlatLaf
|
||||||
|
import java.awt.AlphaComposite
|
||||||
|
import java.awt.Graphics2D
|
||||||
|
import javax.swing.JComponent
|
||||||
|
|
||||||
|
class BGGlassPaneExtension private constructor() : GlassPaneExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = BGGlassPaneExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun paint(scope: WindowScope, c: JComponent, g2d: Graphics2D) {
|
||||||
|
|
||||||
|
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
|
||||||
|
g2d.composite = AlphaComposite.getInstance(
|
||||||
|
AlphaComposite.SRC_OVER,
|
||||||
|
if (FlatLaf.isLafDark()) 0.2f else 0.1f
|
||||||
|
)
|
||||||
|
g2d.drawImage(img, 0, 0, c.width, c.height, null)
|
||||||
|
g2d.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
26
plugins/bg/src/main/kotlin/app/termora/plugins/bg/BGI18n.kt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.AbstractI18n
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object BGI18n : AbstractI18n() {
|
||||||
|
private val log = LoggerFactory.getLogger(BGI18n::class.java)
|
||||||
|
private val myBundle by lazy {
|
||||||
|
val bundle = ResourceBundle.getBundle("i18n/messages", Locale.getDefault(), BGI18n::class.java.classLoader)
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("I18n: {}", bundle.baseBundleName ?: "null")
|
||||||
|
}
|
||||||
|
return@lazy bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getBundle(): ResourceBundle {
|
||||||
|
return myBundle
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLogger(): Logger {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.ApplicationRunnerExtension
|
||||||
|
import app.termora.GlassPaneAwareExtension
|
||||||
|
import app.termora.GlassPaneExtension
|
||||||
|
import app.termora.SettingsOptionExtension
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionSupport
|
||||||
|
import app.termora.plugin.Plugin
|
||||||
|
|
||||||
|
class BGPlugin : Plugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(GlassPaneExtension::class.java) { BGGlassPaneExtension.instance }
|
||||||
|
support.addExtension(SettingsOptionExtension::class.java) { BackgroundSettingsOptionExtension.instance }
|
||||||
|
support.addExtension(ApplicationRunnerExtension::class.java) { BackgroundManager.getInstance() }
|
||||||
|
support.addExtension(GlassPaneAwareExtension::class.java) { BackgroundManager.getInstance() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "Customize Background"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.database.DatabaseManager
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import java.io.File
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.JPopupMenu
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
internal class BackgroundManager private constructor() : Disposable, GlassPaneAwareExtension,
|
||||||
|
ApplicationRunnerExtension {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(BackgroundManager::class.java)
|
||||||
|
fun getInstance(): BackgroundManager {
|
||||||
|
return ApplicationScope.Companion.forApplicationScope()
|
||||||
|
.getOrCreate(BackgroundManager::class) { BackgroundManager() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var bufferedImage: BufferedImage? = null
|
||||||
|
private var imageFilepath = StringUtils.EMPTY
|
||||||
|
private val glassPanes = mutableListOf<WeakReference<JComponent>>()
|
||||||
|
|
||||||
|
|
||||||
|
fun setBackgroundImage(url: String) {
|
||||||
|
clearBackgroundImage()
|
||||||
|
Appearance.backgroundImage = url
|
||||||
|
refreshBackgroundImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBackgroundImage(): BufferedImage? {
|
||||||
|
val bg = doGetBackgroundImage()
|
||||||
|
if (bg == null) {
|
||||||
|
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
JPopupMenu.setDefaultLightWeightPopupEnabled(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||||
|
JPopupMenu.setDefaultLightWeightPopupEnabled(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bg
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doGetBackgroundImage(): BufferedImage? {
|
||||||
|
synchronized(this) {
|
||||||
|
return bufferedImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearBackgroundImage() {
|
||||||
|
synchronized(this) {
|
||||||
|
bufferedImage = null
|
||||||
|
imageFilepath = StringUtils.EMPTY
|
||||||
|
Appearance.backgroundImage = StringUtils.EMPTY
|
||||||
|
}
|
||||||
|
refreshGlassPanes()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshBackgroundImage() {
|
||||||
|
val backgroundImage = Appearance.backgroundImage
|
||||||
|
if (backgroundImage.isBlank()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var file: File? = null
|
||||||
|
|
||||||
|
// 从网络下载
|
||||||
|
if (backgroundImage.startsWith("http://") || backgroundImage.startsWith("https://")) {
|
||||||
|
file = Application.httpClient.newCall(
|
||||||
|
Request.Builder().get()
|
||||||
|
.url(backgroundImage).build()
|
||||||
|
).execute().use { response ->
|
||||||
|
val tempFile = File(Application.getTemporaryDir(), randomUUID())
|
||||||
|
if (response.isSuccessful.not()) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error("Request {} failed with code {}", backgroundImage, response.code)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val body = response.body
|
||||||
|
if (body != null) {
|
||||||
|
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
|
||||||
|
}
|
||||||
|
IOUtils.closeQuietly(body)
|
||||||
|
return@use tempFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val backgroundImageFile = File(backgroundImage)
|
||||||
|
if (backgroundImageFile.isDirectory) {
|
||||||
|
val files = FileUtils.listFiles(backgroundImageFile, arrayOf("png", "jpg", "jpeg"), false)
|
||||||
|
if (files.isNotEmpty()) {
|
||||||
|
for (i in 0 until files.size) {
|
||||||
|
file = files.randomOrNull()
|
||||||
|
if (file == null) break
|
||||||
|
if (file.absolutePath == imageFilepath) continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
synchronized(this) {
|
||||||
|
imageFilepath = StringUtils.EMPTY
|
||||||
|
bufferedImage = null
|
||||||
|
refreshGlassPanes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (backgroundImageFile.isFile) {
|
||||||
|
file = backgroundImageFile
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file == null || imageFilepath == file.absolutePath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferedImage = file.inputStream().use { ImageIO.read(it) }
|
||||||
|
imageFilepath = file.absolutePath
|
||||||
|
|
||||||
|
refreshGlassPanes()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshGlassPanes() {
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
glassPanes.removeIf {
|
||||||
|
val glassPane = it.get()
|
||||||
|
glassPane?.repaint()
|
||||||
|
glassPane == null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setGlassPane(window: Window, glassPane: JComponent) {
|
||||||
|
glassPanes.add(WeakReference(glassPane))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ready() {
|
||||||
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
|
while (isActive) {
|
||||||
|
runCatching { refreshBackgroundImage() }.onFailure {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error("Refresh failed", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(max(Appearance.interval, 30).seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.OptionsPane.Companion.FORM_MARGIN
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatButton
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
|
import javax.swing.*
|
||||||
|
import javax.swing.event.DocumentEvent
|
||||||
|
|
||||||
|
class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(BackgroundOption::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
|
val backgroundImageTextField = OutlineTextField()
|
||||||
|
val intervalSpinner = NumberSpinner(360, minimum = 30, maximum = 86400)
|
||||||
|
|
||||||
|
private val backgroundButton = JButton(Icons.folder)
|
||||||
|
private val backgroundClearButton = FlatButton()
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
|
||||||
|
backgroundImageTextField.isEditable = false
|
||||||
|
backgroundImageTextField.trailingComponent = backgroundButton
|
||||||
|
backgroundImageTextField.text = Appearance.backgroundImage
|
||||||
|
backgroundImageTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
|
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
backgroundClearButton.isFocusable = false
|
||||||
|
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
|
||||||
|
backgroundClearButton.icon = Icons.delete
|
||||||
|
backgroundClearButton.buttonType = FlatButton.ButtonType.toolBarButton
|
||||||
|
|
||||||
|
intervalSpinner.value = Appearance.interval
|
||||||
|
|
||||||
|
add(getFormPanel(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
backgroundButton.addActionListener {
|
||||||
|
val chooser = FileChooser()
|
||||||
|
chooser.osxAllowedFileTypes = listOf("png", "jpg", "jpeg")
|
||||||
|
chooser.allowsMultiSelection = false
|
||||||
|
chooser.win32Filters.add(Pair("Image files", listOf("png", "jpg", "jpeg")))
|
||||||
|
chooser.fileSelectionMode = JFileChooser.FILES_AND_DIRECTORIES
|
||||||
|
chooser.showOpenDialog(owner).thenAccept {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
onSelectedBackgroundImage(it.first())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backgroundClearButton.addActionListener {
|
||||||
|
BackgroundManager.getInstance().clearBackgroundImage()
|
||||||
|
backgroundImageTextField.text = StringUtils.EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
|
intervalSpinner.addChangeListener {
|
||||||
|
val value = intervalSpinner.value
|
||||||
|
if (value is Int) {
|
||||||
|
Appearance.interval = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSelectedBackgroundImage(file: File) {
|
||||||
|
try {
|
||||||
|
if (file.isFile) {
|
||||||
|
val destFile = FileUtils.getFile(Application.getBaseDataDir(), "background", file.name)
|
||||||
|
FileUtils.forceMkdirParent(destFile)
|
||||||
|
FileUtils.deleteQuietly(destFile)
|
||||||
|
FileUtils.copyFile(file, destFile, StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
BackgroundManager.getInstance().setBackgroundImage(destFile.absolutePath)
|
||||||
|
} else if (file.isDirectory) {
|
||||||
|
BackgroundManager.getInstance().setBackgroundImage(file.absolutePath)
|
||||||
|
}
|
||||||
|
backgroundImageTextField.text = file.absolutePath
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
ExceptionUtils.getRootCauseMessage(e),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.imageGray
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return BGI18n.getString("termora.plugins.bg.background-image")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getFormPanel(): JPanel {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default",
|
||||||
|
"pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val builder = FormBuilder.create().layout(layout)
|
||||||
|
val bgClearBox = Box.createHorizontalBox()
|
||||||
|
bgClearBox.add(backgroundClearButton)
|
||||||
|
|
||||||
|
builder.add("${BGI18n.getString("termora.plugins.bg.background-image")}:").xy(1, rows)
|
||||||
|
.add(backgroundImageTextField).xy(3, rows)
|
||||||
|
.add(bgClearBox).xy(5, rows)
|
||||||
|
.apply { rows += step }
|
||||||
|
|
||||||
|
builder.add("${BGI18n.getString("termora.plugins.bg.interval")}:").xy(1, rows)
|
||||||
|
.add(intervalSpinner).xy(3, rows)
|
||||||
|
.apply { rows += step }
|
||||||
|
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.OptionsPane
|
||||||
|
import app.termora.SettingsOptionExtension
|
||||||
|
|
||||||
|
class BackgroundSettingsOptionExtension private constructor(): SettingsOptionExtension {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { BackgroundSettingsOptionExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSettingsOption(): OptionsPane.Option {
|
||||||
|
return BackgroundOption()
|
||||||
|
}
|
||||||
|
}
|
||||||
23
plugins/bg/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>bg</id>
|
||||||
|
|
||||||
|
<name>Customize Background</name>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.bg.BGPlugin</entry>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Customize application background</description>
|
||||||
|
<description language="zh_CN">自定义应用程序背景</description>
|
||||||
|
<description language="zh_TW">自訂應用程式背景</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
6
plugins/bg/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#3574F0"/>
|
||||||
|
<path d="M2.5 9.33566L4.1822 7.66899C4.56052 7.29415 5.16625 7.28159 5.55979 7.64043L11.9861 13.5" stroke="#3574F0"/>
|
||||||
|
<circle cx="10" cy="6" r="1.5" stroke="#3574F0"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 472 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#548AF7"/>
|
||||||
|
<path d="M2.5 9.33566L4.1822 7.66899C4.56052 7.29415 5.16625 7.28159 5.55979 7.64043L11.9861 13.5" stroke="#548AF7"/>
|
||||||
|
<circle cx="10" cy="6" r="1.5" stroke="#548AF7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 472 B |
2
plugins/bg/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
termora.plugins.bg.interval=Interval
|
||||||
|
termora.plugins.bg.background-image=Background Image
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
termora.plugins.bg.background-image=背景图
|
||||||
|
termora.plugins.bg.interval=切换间隔
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
termora.plugins.bg.background-image=背景圖
|
||||||
|
termora.plugins.bg.interval=切換間隔
|
||||||
89
plugins/common.gradle.kts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||||
|
|
||||||
|
|
||||||
|
tasks.withType<Jar> {
|
||||||
|
|
||||||
|
manifest {
|
||||||
|
attributes(
|
||||||
|
"Implementation-Title" to project.name,
|
||||||
|
"Implementation-Version" to project.version,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
from("${rootProject.projectDir}/plugins/LICENSE") {
|
||||||
|
into("META-INF")
|
||||||
|
}
|
||||||
|
|
||||||
|
from("${rootProject.projectDir}/plugins/THIRDPARTY") {
|
||||||
|
into("META-INF")
|
||||||
|
}
|
||||||
|
|
||||||
|
// archiveBaseName.set("${project.name}-${rootProject.version}")
|
||||||
|
destinationDirectory.set(file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named<Copy>("processResources") {
|
||||||
|
filesMatching("META-INF/plugin.xml") {
|
||||||
|
expand(
|
||||||
|
"projectName" to project.name,
|
||||||
|
"projectVersion" to project.version,
|
||||||
|
"rootProjectVersion" to rootProject.version,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Copy>("copy-dependencies") {
|
||||||
|
from(configurations.getByName("runtimeClasspath").filterNot {
|
||||||
|
it.name.startsWith("kotlin-stdlib") || it.name.startsWith("annotations")
|
||||||
|
})
|
||||||
|
into("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("build") {
|
||||||
|
dependsOn("copy-dependencies")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("run-plugin") {
|
||||||
|
dependsOn("build")
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
|
|
||||||
|
val runtimeCompileOnly by configurations.creating { extendsFrom(configurations.getByName("compileOnly")) }
|
||||||
|
val mainClass = "app.termora.MainKt"
|
||||||
|
val executable = System.getProperty("java.home") + "/bin/java"
|
||||||
|
val classpath = (configurations.getByName("compileClasspath") + configurations.getByName("runtimeClasspath")
|
||||||
|
+ runtimeCompileOnly).joinToString(if (os.isWindows) ";" else ":")
|
||||||
|
val commands = mutableListOf<String>(executable)
|
||||||
|
commands.add("-Dapp-version=${rootProject.version}")
|
||||||
|
commands.add("--add-exports java.base/sun.nio.ch=ALL-UNNAMED")
|
||||||
|
if (os.isMacOsX) {
|
||||||
|
// NSWindow
|
||||||
|
commands.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||||
|
commands.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||||
|
commands.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||||
|
commands.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||||
|
commands.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
|
||||||
|
commands.add("-Dapple.awt.application.appearance=system")
|
||||||
|
}
|
||||||
|
commands.addAll(listOf("-cp", classpath, mainClass))
|
||||||
|
|
||||||
|
exec {
|
||||||
|
commandLine = commands
|
||||||
|
environment(
|
||||||
|
"TERMORA_PLUGIN_DIRECTORY" to file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/"),
|
||||||
|
"TERMORA_BASE_DATA_DIR" to "${layout.buildDirectory.get().asFile.absolutePath}/data",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Test>().configureEach {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("clean") {
|
||||||
|
doLast {
|
||||||
|
file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}").deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
16
plugins/cos/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
project.version = "0.0.2"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
implementation("com.qcloud:cos_api:5.6.245")
|
||||||
|
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,19 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
|
||||||
|
class COSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { COSProtocolHostPanelExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return COSProtocolProvider.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||||
|
return COSProtocolHostPanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.DynamicIcon
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.protocol.PathHandler
|
||||||
|
import app.termora.protocol.PathHandlerRequest
|
||||||
|
import app.termora.protocol.TransferProtocolProvider
|
||||||
|
import com.qcloud.cos.ClientConfig
|
||||||
|
import com.qcloud.cos.auth.BasicCOSCredentials
|
||||||
|
import com.qcloud.cos.model.Bucket
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
|
||||||
|
|
||||||
|
class COSProtocolProvider private constructor() : TransferProtocolProvider {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { COSProtocolProvider() }
|
||||||
|
const val PROTOCOL = "COS"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocol(): String {
|
||||||
|
return PROTOCOL
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||||
|
return Icons.tencent
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
|
||||||
|
val host = requester.host
|
||||||
|
val secretId = host.username
|
||||||
|
val secretKey = host.authentication.password
|
||||||
|
val cred = BasicCOSCredentials(secretId, secretKey)
|
||||||
|
val clientConfig = ClientConfig()
|
||||||
|
|
||||||
|
clientConfig.isPrintShutdownStackTrace = false
|
||||||
|
val cosClient = COSClientHandler.createCOSClient(cred, StringUtils.EMPTY, host.proxy)
|
||||||
|
val buckets: List<Bucket>
|
||||||
|
|
||||||
|
try {
|
||||||
|
buckets = cosClient.listBuckets()
|
||||||
|
} finally {
|
||||||
|
cosClient.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultPath = host.options.sftpDefaultDirectory
|
||||||
|
val fs = COSFileSystem(COSClientHandler(cred, host.proxy, buckets))
|
||||||
|
return PathHandler(fs, fs.getPath(defaultPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
class COSProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { COSProtocolProviderExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return COSProtocolProvider.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
25
plugins/cos/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>cos</id>
|
||||||
|
|
||||||
|
<name>Tencent COS</name>
|
||||||
|
|
||||||
|
|
||||||
|
<paid/>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.cos.COSPlugin</entry>
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Connecting to Tencent COS</description>
|
||||||
|
<description language="zh_CN">支持连接到腾讯云对象存储</description>
|
||||||
|
<description language="zh_TW">支援連接到騰訊雲物件存儲</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
1
plugins/cos/src/main/resources/META-INF/pluginIcon.svg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
19
plugins/editor/build.gradle.kts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
project.version = "0.0.6"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
compileOnly(project(":"))
|
||||||
|
implementation("com.fifesoft:rsyntaxtextarea:3.6.0")
|
||||||
|
implementation("com.fifesoft:languagesupport:3.4.0")
|
||||||
|
implementation("com.fifesoft:autocomplete:3.3.2")
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package app.termora.plugins.editor
|
||||||
|
|
||||||
|
import app.termora.DialogWrapper
|
||||||
|
import app.termora.Disposable
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.OptionPane
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.UIManager
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
import kotlin.io.path.name
|
||||||
|
|
||||||
|
|
||||||
|
class EditorDialog(file: Path, owner: Window, private val myDisposable: Disposable) : DialogWrapper(null) {
|
||||||
|
|
||||||
|
private val filename = file.name
|
||||||
|
private val filepath = File(file.absolutePathString())
|
||||||
|
private val editorPanel = EditorPanel(this, filepath)
|
||||||
|
private val disposed = AtomicBoolean()
|
||||||
|
|
||||||
|
init {
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||||
|
isModal = false
|
||||||
|
controlsVisible = true
|
||||||
|
isResizable = true
|
||||||
|
title = filename
|
||||||
|
iconImages = owner.iconImages
|
||||||
|
escapeDispose = false
|
||||||
|
defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
||||||
|
|
||||||
|
initEvents()
|
||||||
|
|
||||||
|
setLocationRelativeTo(owner)
|
||||||
|
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowClosing(e: WindowEvent?) {
|
||||||
|
if (disposed.compareAndSet(false, true)) {
|
||||||
|
doCancelAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Disposer.register(myDisposable, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
if (disposed.compareAndSet(false, true)) {
|
||||||
|
doCancelAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Disposer.register(disposable, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
if (disposed.compareAndSet(false, true)) {
|
||||||
|
Disposer.dispose(myDisposable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doCancelAction() {
|
||||||
|
if (editorPanel.changes()) {
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
this,
|
||||||
|
"文件尚未保存,你确定要退出吗?",
|
||||||
|
optionType = JOptionPane.OK_CANCEL_OPTION,
|
||||||
|
) != JOptionPane.OK_OPTION
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.doCancelAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
return editorPanel
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSouthPanel(): JComponent? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
package app.termora.plugins.editor
|
||||||
|
|
||||||
|
import app.termora.DocumentAdaptor
|
||||||
|
import app.termora.DynamicColor
|
||||||
|
import app.termora.EnableManager
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.database.DatabaseManager
|
||||||
|
import com.formdev.flatlaf.FlatLaf
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatTextField
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.apache.commons.io.FilenameUtils
|
||||||
|
import org.dom4j.io.OutputFormat
|
||||||
|
import org.dom4j.io.SAXReader
|
||||||
|
import org.dom4j.io.XMLWriter
|
||||||
|
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea
|
||||||
|
import org.fife.ui.rsyntaxtextarea.SyntaxConstants
|
||||||
|
import org.fife.ui.rsyntaxtextarea.Theme
|
||||||
|
import org.fife.ui.rtextarea.RTextScrollPane
|
||||||
|
import org.fife.ui.rtextarea.SearchContext
|
||||||
|
import org.fife.ui.rtextarea.SearchEngine
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Insets
|
||||||
|
import java.awt.event.ActionEvent
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
|
import java.io.File
|
||||||
|
import java.io.StringReader
|
||||||
|
import java.io.StringWriter
|
||||||
|
import javax.swing.*
|
||||||
|
import javax.swing.SwingConstants.VERTICAL
|
||||||
|
import javax.swing.event.DocumentEvent
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class EditorPanel(private val window: JDialog, private val file: File) : JPanel(BorderLayout()) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(EditorPanel::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var text = file.readText(Charsets.UTF_8)
|
||||||
|
private val layeredPane = LayeredPane()
|
||||||
|
|
||||||
|
private val textArea = RSyntaxTextArea()
|
||||||
|
private val scrollPane = RTextScrollPane(textArea)
|
||||||
|
private val findPanel = FlatToolBar().apply { isFloatable = false }
|
||||||
|
private val toolbar = FlatToolBar().apply { isFloatable = false }
|
||||||
|
private val searchTextField = FlatTextField()
|
||||||
|
private val closeFindPanelBtn = JButton(Icons.close)
|
||||||
|
private val nextBtn = JButton(Icons.down)
|
||||||
|
private val prevBtn = JButton(Icons.up)
|
||||||
|
private val context = SearchContext()
|
||||||
|
private val softWrapBtn = JToggleButton(Icons.softWrap)
|
||||||
|
private val scrollUpBtn = JButton(Icons.scrollUp)
|
||||||
|
private val scrollEndBtn = JButton(Icons.scrollDown)
|
||||||
|
private val prettyBtn = JButton(Icons.reformatCode)
|
||||||
|
|
||||||
|
private val enableManager get() = EnableManager.getInstance()
|
||||||
|
private val prettyJson = Json {
|
||||||
|
prettyPrint = true
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
textArea.font = textArea.font.deriveFont(DatabaseManager.getInstance().terminal.fontSize.toFloat())
|
||||||
|
textArea.text = text
|
||||||
|
textArea.antiAliasingEnabled = true
|
||||||
|
softWrapBtn.isSelected = enableManager.getFlag("Plugins.editor.softWrap", false)
|
||||||
|
|
||||||
|
val theme = if (FlatLaf.isLafDark())
|
||||||
|
Theme.load(javaClass.getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/dark.xml"))
|
||||||
|
else
|
||||||
|
Theme.load(javaClass.getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/idea.xml"))
|
||||||
|
|
||||||
|
theme.apply(textArea)
|
||||||
|
|
||||||
|
val extension = FilenameUtils.getExtension(file.name)?.lowercase()
|
||||||
|
textArea.syntaxEditingStyle = when (extension) {
|
||||||
|
"java" -> SyntaxConstants.SYNTAX_STYLE_JAVA
|
||||||
|
"kt" -> SyntaxConstants.SYNTAX_STYLE_KOTLIN
|
||||||
|
"properties" -> SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE
|
||||||
|
"cpp", "c++" -> SyntaxConstants.SYNTAX_STYLE_CPLUSPLUS
|
||||||
|
"c" -> SyntaxConstants.SYNTAX_STYLE_C
|
||||||
|
"cs" -> SyntaxConstants.SYNTAX_STYLE_CSHARP
|
||||||
|
"css" -> SyntaxConstants.SYNTAX_STYLE_CSS
|
||||||
|
"html", "htm", "htmlx" -> SyntaxConstants.SYNTAX_STYLE_HTML
|
||||||
|
"js" -> SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT
|
||||||
|
"ts" -> SyntaxConstants.SYNTAX_STYLE_TYPESCRIPT
|
||||||
|
"xml", "svg" -> SyntaxConstants.SYNTAX_STYLE_XML
|
||||||
|
"yaml", "yml" -> SyntaxConstants.SYNTAX_STYLE_YAML
|
||||||
|
"sh", "shell" -> SyntaxConstants.SYNTAX_STYLE_UNIX_SHELL
|
||||||
|
"sql" -> SyntaxConstants.SYNTAX_STYLE_SQL
|
||||||
|
"bat" -> SyntaxConstants.SYNTAX_STYLE_WINDOWS_BATCH
|
||||||
|
"py" -> SyntaxConstants.SYNTAX_STYLE_PYTHON
|
||||||
|
"php" -> SyntaxConstants.SYNTAX_STYLE_PHP
|
||||||
|
"lua" -> SyntaxConstants.SYNTAX_STYLE_LUA
|
||||||
|
"less" -> SyntaxConstants.SYNTAX_STYLE_LESS
|
||||||
|
"jsp" -> SyntaxConstants.SYNTAX_STYLE_JSP
|
||||||
|
"json" -> SyntaxConstants.SYNTAX_STYLE_JSON
|
||||||
|
"ini" -> SyntaxConstants.SYNTAX_STYLE_INI
|
||||||
|
"hosts" -> SyntaxConstants.SYNTAX_STYLE_HOSTS
|
||||||
|
"go" -> SyntaxConstants.SYNTAX_STYLE_GO
|
||||||
|
"dtd" -> SyntaxConstants.SYNTAX_STYLE_DTD
|
||||||
|
"dart" -> SyntaxConstants.SYNTAX_STYLE_DART
|
||||||
|
"csv" -> SyntaxConstants.SYNTAX_STYLE_CSV
|
||||||
|
"md" -> SyntaxConstants.SYNTAX_STYLE_MARKDOWN
|
||||||
|
else -> SyntaxConstants.SYNTAX_STYLE_NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有 JSON 才可以格式化
|
||||||
|
prettyBtn.isVisible = textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_JSON ||
|
||||||
|
textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_XML
|
||||||
|
|
||||||
|
textArea.discardAllEdits()
|
||||||
|
|
||||||
|
scrollPane.border = BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor)
|
||||||
|
|
||||||
|
findPanel.isVisible = false
|
||||||
|
findPanel.isOpaque = true
|
||||||
|
findPanel.background = DynamicColor("window")
|
||||||
|
|
||||||
|
searchTextField.background = findPanel.background
|
||||||
|
searchTextField.padding = Insets(0, 4, 0, 0)
|
||||||
|
searchTextField.border = BorderFactory.createEmptyBorder()
|
||||||
|
|
||||||
|
findPanel.add(searchTextField)
|
||||||
|
findPanel.add(prevBtn)
|
||||||
|
findPanel.add(nextBtn)
|
||||||
|
findPanel.add(closeFindPanelBtn)
|
||||||
|
findPanel.border = BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createMatteBorder(0, 1, 1, 0, DynamicColor.BorderColor),
|
||||||
|
BorderFactory.createEmptyBorder(2, 2, 2, 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
toolbar.orientation = VERTICAL
|
||||||
|
toolbar.add(scrollUpBtn)
|
||||||
|
toolbar.add(prettyBtn)
|
||||||
|
toolbar.add(softWrapBtn)
|
||||||
|
toolbar.add(scrollEndBtn)
|
||||||
|
|
||||||
|
val viewPanel = JPanel(BorderLayout())
|
||||||
|
viewPanel.add(scrollPane, BorderLayout.CENTER)
|
||||||
|
viewPanel.add(toolbar, BorderLayout.EAST)
|
||||||
|
viewPanel.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||||
|
|
||||||
|
layeredPane.add(findPanel, JLayeredPane.MODAL_LAYER as Any)
|
||||||
|
layeredPane.add(viewPanel, JLayeredPane.DEFAULT_LAYER as Any)
|
||||||
|
|
||||||
|
add(layeredPane, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
window.addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowOpened(e: WindowEvent?) {
|
||||||
|
scrollPane.verticalScrollBar.value = 0
|
||||||
|
window.removeWindowListener(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
softWrapBtn.addActionListener {
|
||||||
|
enableManager.getFlag("Plugins.editor.softWrap", softWrapBtn.isSelected)
|
||||||
|
textArea.lineWrap = softWrapBtn.isSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollUpBtn.addActionListener { scrollPane.verticalScrollBar.value = 0 }
|
||||||
|
scrollEndBtn.addActionListener { scrollPane.verticalScrollBar.value = scrollPane.verticalScrollBar.maximum }
|
||||||
|
|
||||||
|
textArea.inputMap.put(
|
||||||
|
KeyStroke.getKeyStroke(KeyEvent.VK_S, toolkit.menuShortcutKeyMaskEx),
|
||||||
|
"Save"
|
||||||
|
)
|
||||||
|
textArea.inputMap.put(
|
||||||
|
KeyStroke.getKeyStroke(KeyEvent.VK_F, toolkit.menuShortcutKeyMaskEx),
|
||||||
|
"Find"
|
||||||
|
)
|
||||||
|
textArea.inputMap.put(
|
||||||
|
KeyStroke.getKeyStroke(KeyEvent.VK_F, toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK),
|
||||||
|
"Format"
|
||||||
|
)
|
||||||
|
|
||||||
|
searchTextField.inputMap.put(
|
||||||
|
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
|
||||||
|
"Esc"
|
||||||
|
)
|
||||||
|
|
||||||
|
searchTextField.actionMap.put("Esc", object : AbstractAction("Esc") {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
textArea.clearMarkAllHighlights()
|
||||||
|
textArea.requestFocusInWindow()
|
||||||
|
findPanel.isVisible = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
closeFindPanelBtn.addActionListener { searchTextField.actionMap.get("Esc").actionPerformed(it) }
|
||||||
|
|
||||||
|
textArea.actionMap.put("Save", object : AbstractAction("Save") {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
file.writeText(textArea.text, Charsets.UTF_8)
|
||||||
|
text = textArea.text
|
||||||
|
window.title = file.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
textArea.actionMap.put("Format", object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
format()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
textArea.actionMap.put("Find", object : AbstractAction("Find") {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
findPanel.isVisible = true
|
||||||
|
searchTextField.selectAll()
|
||||||
|
searchTextField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
textArea.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
|
window.title = if (textArea.text.hashCode() != text.hashCode()) {
|
||||||
|
"${file.name} *"
|
||||||
|
} else {
|
||||||
|
file.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
searchTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
|
search()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
searchTextField.addActionListener { nextBtn.doClick(0) }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
prettyBtn.addActionListener(textArea.actionMap.get("Format"))
|
||||||
|
|
||||||
|
prevBtn.addActionListener { search(false) }
|
||||||
|
nextBtn.addActionListener { search(true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun format() {
|
||||||
|
val vertical = scrollPane.verticalScrollBar.value
|
||||||
|
val horizontal = scrollPane.horizontalScrollBar.value
|
||||||
|
val caretPosition = textArea.caretPosition
|
||||||
|
|
||||||
|
val c = if (textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_JSON) {
|
||||||
|
runCatching {
|
||||||
|
val json = prettyJson.parseToJsonElement(textArea.text)
|
||||||
|
textArea.text = prettyJson.encodeToString(json)
|
||||||
|
}.onFailure {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(it.message, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_XML) {
|
||||||
|
runCatching {
|
||||||
|
val document = SAXReader().read(StringReader(textArea.text))
|
||||||
|
val sw = StringWriter()
|
||||||
|
val writer = XMLWriter(sw, OutputFormat.createPrettyPrint())
|
||||||
|
writer.write(document)
|
||||||
|
textArea.text = sw.toString()
|
||||||
|
}.onFailure {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(it.message, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
} ?: return
|
||||||
|
|
||||||
|
c.onSuccess {
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
scrollPane.verticalScrollBar.value = min(
|
||||||
|
vertical,
|
||||||
|
scrollPane.verticalScrollBar.maximum
|
||||||
|
)
|
||||||
|
scrollPane.horizontalScrollBar.value = min(
|
||||||
|
horizontal,
|
||||||
|
scrollPane.horizontalScrollBar.maximum
|
||||||
|
)
|
||||||
|
if (caretPosition >= 0 && caretPosition < textArea.document.length) {
|
||||||
|
textArea.caretPosition = caretPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun search(searchForward: Boolean = true) {
|
||||||
|
textArea.clearMarkAllHighlights()
|
||||||
|
|
||||||
|
|
||||||
|
val text: String = searchTextField.getText()
|
||||||
|
if (text.isEmpty()) return
|
||||||
|
context.searchFor = text
|
||||||
|
context.searchForward = searchForward
|
||||||
|
context.wholeWord = false
|
||||||
|
val result = SearchEngine.find(textArea, context)
|
||||||
|
|
||||||
|
prevBtn.isEnabled = result.markedCount > 0
|
||||||
|
nextBtn.isEnabled = result.markedCount > 0
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changes() = text != textArea.text
|
||||||
|
|
||||||
|
private inner class LayeredPane : JLayeredPane() {
|
||||||
|
override fun doLayout() {
|
||||||
|
synchronized(treeLock) {
|
||||||
|
for (c in components) {
|
||||||
|
if (c == findPanel) {
|
||||||
|
val height = max(findPanel.preferredSize.height, findPanel.height)
|
||||||
|
val x = width / 2
|
||||||
|
c.setBounds(x, 1, width - x, height)
|
||||||
|
} else {
|
||||||
|
c.setBounds(0, 0, width, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package app.termora.plugins.editor
|
||||||
|
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionSupport
|
||||||
|
import app.termora.plugin.Plugin
|
||||||
|
import app.termora.transfer.TransportEditFileExtension
|
||||||
|
|
||||||
|
class EditorPlugin : Plugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(TransportEditFileExtension::class.java) { MyTransportEditFileExtension.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "SFTP File Editor"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.termora.plugins.editor
|
||||||
|
|
||||||
|
import app.termora.Disposable
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.transfer.TransportEditFileExtension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.nio.file.Path
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
class MyTransportEditFileExtension private constructor() : TransportEditFileExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = MyTransportEditFileExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun edit(owner: Window, path: Path): Disposable {
|
||||||
|
val disposable = Disposer.newDisposable()
|
||||||
|
SwingUtilities.invokeLater { EditorDialog(path, owner, disposable).isVisible = true }
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
|
}
|
||||||
22
plugins/editor/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>editor</id>
|
||||||
|
|
||||||
|
<name>SFTP File Editor</name>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.editor.EditorPlugin</entry>
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Edit SFTP files using the built-in editor</description>
|
||||||
|
<description language="zh_CN">使用内置编辑器编辑 SFTP 文件</description>
|
||||||
|
<description language="zh_TW">使用內建編輯器編輯 SFTP 文件</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V7.47774C14.2142 6.80872 13.0333 6.84543 12.2909 7.58786L7 12.8787V14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" />
|
||||||
|
<path d="M8.09379 5H13C13.5523 5 14 5.44772 14 6V7.02381C14.3594 7.07711 14.7072 7.22842 15 7.47774V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H7V13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823L8.09379 5Z" fill="#6C707E"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4122 8.29497C14.0217 7.90444 13.3885 7.90444 12.998 8.29497L11.6466 9.64633L8 13.2929V16H10.7071L15.7051 11.0021C16.0956 10.6116 16.0956 9.97839 15.7051 9.58786L14.4122 8.29497ZM14 11.2929L14.998 10.295L13.7051 9.00208L12.7071 10L14 11.2929ZM12 10.7072L13.2929 12L10.2929 15H9V13.7072L12 10.7072Z" fill="#6C707E"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V7.47774C14.2142 6.80872 13.0333 6.84543 12.2909 7.58786L7 12.8787V14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" />
|
||||||
|
<path d="M8.09379 5H13C13.5523 5 14 5.44772 14 6V7.02381C14.3594 7.07711 14.7072 7.22842 15 7.47774V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H7V13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823L8.09379 5Z" fill="#CED0D6"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4122 8.29497C14.0217 7.90444 13.3885 7.90444 12.998 8.29497L11.6466 9.64633L8 13.2929V16H10.7071L15.7051 11.0021C16.0956 10.6116 16.0956 9.97839 15.7051 9.58786L14.4122 8.29497ZM14 11.2929L14.998 10.295L13.7051 9.00208L12.7071 10L14 11.2929ZM12 10.7072L13.2929 12L10.2929 15H9V13.7072L12 10.7072Z" fill="#CED0D6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,107 @@
|
|||||||
|
package app.termora.plugins.editor;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.*;
|
||||||
|
import javax.swing.*;
|
||||||
|
|
||||||
|
import org.fife.ui.rtextarea.*;
|
||||||
|
import org.fife.ui.rsyntaxtextarea.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple example showing how to do search and replace in a RSyntaxTextArea.
|
||||||
|
* The toolbar isn't very user-friendly, but this is just to show you how to use
|
||||||
|
* the API.<p>
|
||||||
|
*
|
||||||
|
* This example uses RSyntaxTextArea 2.5.6.
|
||||||
|
*/
|
||||||
|
public class FindAndReplaceDemo extends JFrame implements ActionListener {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private RSyntaxTextArea textArea;
|
||||||
|
private JTextField searchField;
|
||||||
|
private JCheckBox regexCB;
|
||||||
|
private JCheckBox matchCaseCB;
|
||||||
|
|
||||||
|
public FindAndReplaceDemo() {
|
||||||
|
|
||||||
|
JPanel cp = new JPanel(new BorderLayout());
|
||||||
|
|
||||||
|
textArea = new RSyntaxTextArea(20, 60);
|
||||||
|
textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
|
||||||
|
textArea.setCodeFoldingEnabled(true);
|
||||||
|
RTextScrollPane sp = new RTextScrollPane(textArea);
|
||||||
|
cp.add(sp);
|
||||||
|
|
||||||
|
// Create a toolbar with searching options.
|
||||||
|
JToolBar toolBar = new JToolBar();
|
||||||
|
searchField = new JTextField(30);
|
||||||
|
toolBar.add(searchField);
|
||||||
|
final JButton nextButton = new JButton("Find Next");
|
||||||
|
nextButton.setActionCommand("FindNext");
|
||||||
|
nextButton.addActionListener(this);
|
||||||
|
toolBar.add(nextButton);
|
||||||
|
searchField.addActionListener(new ActionListener() {
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
nextButton.doClick(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
JButton prevButton = new JButton("Find Previous");
|
||||||
|
prevButton.setActionCommand("FindPrev");
|
||||||
|
prevButton.addActionListener(this);
|
||||||
|
toolBar.add(prevButton);
|
||||||
|
regexCB = new JCheckBox("Regex");
|
||||||
|
toolBar.add(regexCB);
|
||||||
|
matchCaseCB = new JCheckBox("Match Case");
|
||||||
|
toolBar.add(matchCaseCB);
|
||||||
|
cp.add(toolBar, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
setContentPane(cp);
|
||||||
|
setTitle("Find and Replace Demo");
|
||||||
|
setDefaultCloseOperation(EXIT_ON_CLOSE);
|
||||||
|
pack();
|
||||||
|
setLocationRelativeTo(null);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
|
||||||
|
// "FindNext" => search forward, "FindPrev" => search backward
|
||||||
|
String command = e.getActionCommand();
|
||||||
|
boolean forward = "FindNext".equals(command);
|
||||||
|
|
||||||
|
// Create an object defining our search parameters.
|
||||||
|
SearchContext context = new SearchContext();
|
||||||
|
String text = searchField.getText();
|
||||||
|
if (text.length() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.setSearchFor(text);
|
||||||
|
context.setMatchCase(matchCaseCB.isSelected());
|
||||||
|
context.setRegularExpression(regexCB.isSelected());
|
||||||
|
context.setSearchForward(forward);
|
||||||
|
context.setWholeWord(false);
|
||||||
|
|
||||||
|
boolean found = SearchEngine.find(textArea, context).wasFound();
|
||||||
|
if (!found) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Text not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// Start all Swing applications on the EDT.
|
||||||
|
SwingUtilities.invokeLater(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
String laf = UIManager.getSystemLookAndFeelClassName();
|
||||||
|
UIManager.setLookAndFeel(laf);
|
||||||
|
} catch (Exception e) { /* never happens */ }
|
||||||
|
FindAndReplaceDemo demo = new FindAndReplaceDemo();
|
||||||
|
demo.setVisible(true);
|
||||||
|
demo.textArea.requestFocusInWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
15
plugins/ftp/build.gradle.kts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
project.version = "0.0.1"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
compileOnly(project(":"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import org.apache.commons.vfs2.Capability
|
||||||
|
import org.apache.commons.vfs2.FileName
|
||||||
|
import org.apache.commons.vfs2.FileSystem
|
||||||
|
import org.apache.commons.vfs2.FileSystemOptions
|
||||||
|
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
|
||||||
|
|
||||||
|
class FTPFileProvider private constructor() : AbstractOriginatingFileProvider() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { FTPFileProvider() }
|
||||||
|
val capabilities = listOf(
|
||||||
|
Capability.CREATE,
|
||||||
|
Capability.DELETE,
|
||||||
|
Capability.RENAME,
|
||||||
|
Capability.GET_TYPE,
|
||||||
|
Capability.LIST_CHILDREN,
|
||||||
|
Capability.READ_CONTENT,
|
||||||
|
Capability.URI,
|
||||||
|
Capability.WRITE_CONTENT,
|
||||||
|
Capability.GET_LAST_MODIFIED,
|
||||||
|
Capability.SET_LAST_MODIFIED_FILE,
|
||||||
|
Capability.RANDOM_ACCESS_READ,
|
||||||
|
Capability.APPEND_CONTENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCapabilities(): Collection<Capability> {
|
||||||
|
return FTPFileProvider.capabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doCreateFileSystem(
|
||||||
|
rootFileName: FileName,
|
||||||
|
fileSystemOptions: FileSystemOptions
|
||||||
|
): FileSystem? {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.DynamicIcon
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionSupport
|
||||||
|
import app.termora.plugin.PaidPlugin
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
class FTPPlugin : PaidPlugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(ProtocolProviderExtension::class.java) { FTPProtocolProviderExtension.Companion.instance }
|
||||||
|
support.addExtension(ProtocolHostPanelExtension::class.java) { FTPProtocolHostPanelExtension.Companion.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "FTP"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.Host
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
|
||||||
|
class FTPProtocolHostPanel : ProtocolHostPanel() {
|
||||||
|
override fun getHost(): Host {
|
||||||
|
return Host(
|
||||||
|
name = StringUtils.EMPTY,
|
||||||
|
protocol = FTPProtocolProvider.PROTOCOL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setHost(host: Host) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun validateFields(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
|
||||||
|
class FTPProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { FTPProtocolHostPanelExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return FTPProtocolProvider.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||||
|
return FTPProtocolHostPanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.DynamicIcon
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.protocol.FileObjectHandler
|
||||||
|
import app.termora.protocol.FileObjectRequest
|
||||||
|
import app.termora.protocol.TransferProtocolProvider
|
||||||
|
import org.apache.commons.vfs2.provider.FileProvider
|
||||||
|
|
||||||
|
class FTPProtocolProvider private constructor() : TransferProtocolProvider {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { FTPProtocolProvider() }
|
||||||
|
const val PROTOCOL = "FTP"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocol(): String {
|
||||||
|
return PROTOCOL
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||||
|
return Icons.ftp
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFileProvider(): FileProvider {
|
||||||
|
return FTPFileProvider.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { FTPProtocolProviderExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return FTPProtocolProvider.Companion.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
24
plugins/ftp/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>ftp</id>
|
||||||
|
|
||||||
|
<name>FTP</name>
|
||||||
|
|
||||||
|
<paid/>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.ftp.FTPPlugin</entry>
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Connecting to FTP</description>
|
||||||
|
<description language="zh_CN">支持连接到到 FTP</description>
|
||||||
|
<description language="zh_TW">支援連接到 FTP</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
1
plugins/ftp/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#6C707E"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#6C707E"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#CED0D6"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#CED0D6"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
16
plugins/geo/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
project.version = "0.0.5"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
compileOnly(project(":"))
|
||||||
|
implementation("com.maxmind.geoip2:geoip2:4.3.1")
|
||||||
|
// https://github.com/hstyi/geolite2
|
||||||
|
implementation("com.github.hstyi:geolite2:v1.0-202506280327")
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
|
|
||||||
82
plugins/geo/src/main/kotlin/app/termora/plugins/geo/Geo.kt
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package app.termora.plugins.geo
|
||||||
|
|
||||||
|
import app.termora.ApplicationScope
|
||||||
|
import app.termora.Disposable
|
||||||
|
import app.termora.geo.GeoLibrary
|
||||||
|
import com.maxmind.db.CHMCache
|
||||||
|
import com.maxmind.geoip2.DatabaseReader
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
||||||
|
|
||||||
|
internal class Geo private constructor() : Disposable {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(Geo::class.java)
|
||||||
|
|
||||||
|
fun getInstance(): Geo {
|
||||||
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(Geo::class) { Geo() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val initialized = AtomicBoolean(false)
|
||||||
|
private var reader: DatabaseReader? = null
|
||||||
|
|
||||||
|
private fun initialize() {
|
||||||
|
if (isInitialized()) return
|
||||||
|
|
||||||
|
if (initialized.compareAndSet(false, true)) {
|
||||||
|
try {
|
||||||
|
val input = GeoLibrary.getInputStream()
|
||||||
|
if (input == null) {
|
||||||
|
throw IllegalStateException("GeoLite2-Country.mmdb not be found")
|
||||||
|
}
|
||||||
|
val locale = Locale.getDefault().toString().replace("_", "-")
|
||||||
|
try {
|
||||||
|
reader = DatabaseReader.Builder(input)
|
||||||
|
.locales(listOf(locale, "en"))
|
||||||
|
.withCache(CHMCache()).build()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error("Failed to initialize geo database", e)
|
||||||
|
}
|
||||||
|
initialized.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun country(ip: String): Country? {
|
||||||
|
try {
|
||||||
|
initialize()
|
||||||
|
|
||||||
|
val reader = reader ?: return null
|
||||||
|
val response = reader.tryCountry(InetAddress.getByName(ip)).getOrNull() ?: return null
|
||||||
|
val isoCode = response.country.isoCode
|
||||||
|
var name = response.country.name
|
||||||
|
// 控制名称不要太长,如果太长则使用缩写。例如:United States
|
||||||
|
if (name != null && name.length > 6) name = isoCode
|
||||||
|
return Country(isoCode, name ?: isoCode)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.error("Failed to initialize geo database", e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isInitialized(): Boolean = initialized.get()
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
IOUtils.closeQuietly(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Country(val isoCode: String, val name: String)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package app.termora.plugins.geo
|
||||||
|
|
||||||
|
import app.termora.EnableManager
|
||||||
|
import app.termora.SwingUtils
|
||||||
|
import app.termora.TermoraFrameManager
|
||||||
|
import app.termora.tree.HostTreeShowMoreEnableExtension
|
||||||
|
import app.termora.tree.NewHostTree
|
||||||
|
import javax.swing.JCheckBoxMenuItem
|
||||||
|
import javax.swing.JTree
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
internal class GeoHostTreeShowMoreEnableExtension private constructor() : HostTreeShowMoreEnableExtension {
|
||||||
|
companion object {
|
||||||
|
private const val KEY = "Plugins.Geo.ShowMore.Enable"
|
||||||
|
|
||||||
|
val instance = GeoHostTreeShowMoreEnableExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val enableManager get() = EnableManager.getInstance()
|
||||||
|
|
||||||
|
override fun createJCheckBoxMenuItem(tree: JTree): JCheckBoxMenuItem {
|
||||||
|
val item = JCheckBoxMenuItem("Geo")
|
||||||
|
item.isSelected = item.isEnabled && enableManager.getFlag(KEY, true)
|
||||||
|
item.addActionListener {
|
||||||
|
enableManager.setFlag(KEY, item.isSelected)
|
||||||
|
updateComponentTreeUI()
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateComponentTreeUI() {
|
||||||
|
// reload all tree
|
||||||
|
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
||||||
|
for (tree in SwingUtils.getDescendantsOfClass(NewHostTree::class.java, frame)) {
|
||||||
|
SwingUtilities.updateComponentTreeUI(tree)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isShowMore(): Boolean {
|
||||||
|
return enableManager.getFlag(KEY, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package app.termora.plugins.geo
|
||||||
|
|
||||||
|
import app.termora.AbstractI18n
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object GeoI18n : AbstractI18n() {
|
||||||
|
private val log = LoggerFactory.getLogger(GeoI18n::class.java)
|
||||||
|
private val myBundle by lazy {
|
||||||
|
val bundle = ResourceBundle.getBundle("i18n/messages", Locale.getDefault(), GeoI18n::class.java.classLoader)
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("I18n: {}", bundle.baseBundleName ?: "null")
|
||||||
|
}
|
||||||
|
return@lazy bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getBundle(): ResourceBundle {
|
||||||
|
return myBundle
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLogger(): Logger {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package app.termora.plugins.geo
|
||||||
|
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionSupport
|
||||||
|
import app.termora.plugin.Plugin
|
||||||
|
import app.termora.tree.HostTreeShowMoreEnableExtension
|
||||||
|
import app.termora.tree.SimpleTreeCellRendererExtension
|
||||||
|
|
||||||
|
class GeoPlugin : Plugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(SimpleTreeCellRendererExtension::class.java) { GeoSimpleTreeCellRendererExtension.instance }
|
||||||
|
support.addExtension(HostTreeShowMoreEnableExtension::class.java) { GeoHostTreeShowMoreEnableExtension.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "Geo"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package app.termora.plugins.geo
|
||||||
|
|
||||||
|
import app.termora.ColorHash
|
||||||
|
import app.termora.tree.HostTreeNode
|
||||||
|
import app.termora.tree.MarkerSimpleTreeCellAnnotation
|
||||||
|
import app.termora.tree.SimpleTreeCellAnnotation
|
||||||
|
import app.termora.tree.SimpleTreeCellRendererExtension
|
||||||
|
import java.awt.Color
|
||||||
|
import javax.swing.JTree
|
||||||
|
|
||||||
|
class GeoSimpleTreeCellRendererExtension private constructor() : SimpleTreeCellRendererExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = GeoSimpleTreeCellRendererExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val geo get() = Geo.getInstance()
|
||||||
|
|
||||||
|
override fun createAnnotations(
|
||||||
|
tree: JTree,
|
||||||
|
value: Any?,
|
||||||
|
sel: Boolean,
|
||||||
|
expanded: Boolean,
|
||||||
|
leaf: Boolean,
|
||||||
|
row: Int,
|
||||||
|
hasFocus: Boolean
|
||||||
|
): List<SimpleTreeCellAnnotation> {
|
||||||
|
|
||||||
|
val node = value as? HostTreeNode ?: return emptyList()
|
||||||
|
if (node.isFolder) return emptyList()
|
||||||
|
val protocol = node.data.protocol
|
||||||
|
if ((protocol == "SSH" || protocol == "RDP").not()) return emptyList()
|
||||||
|
|
||||||
|
if (GeoHostTreeShowMoreEnableExtension.instance.isShowMore().not()) return emptyList()
|
||||||
|
val country = geo.country(node.data.host) ?: return emptyList()
|
||||||
|
|
||||||
|
val text = "${countryCodeToFlagEmoji(country.isoCode)}${country.name}"
|
||||||
|
return listOf(
|
||||||
|
MarkerSimpleTreeCellAnnotation(
|
||||||
|
text,
|
||||||
|
foreground = Color.white,
|
||||||
|
background = ColorHash.hash(country.isoCode),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun countryCodeToFlagEmoji(code: String): String {
|
||||||
|
if (code.length < 2) return "❓"
|
||||||
|
val upper = code.take(2).uppercase()
|
||||||
|
val first = Character.codePointAt(upper, 0) - 'A'.code + 0x1F1E6
|
||||||
|
val second = Character.codePointAt(upper, 1) - 'A'.code + 0x1F1E6
|
||||||
|
return String(Character.toChars(first)) + String(Character.toChars(second))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
plugins/geo/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>geo</id>
|
||||||
|
|
||||||
|
<name>Geo</name>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.geo.GeoPlugin</entry>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Display the geographical location of the host</description>
|
||||||
|
<description language="zh_CN">显示主机的地理位置</description>
|
||||||
|
<description language="zh_TW">顯示主機的地理位置</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
5
plugins/geo/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="8" cy="8" r="6.5" stroke="#6C707E"/>
|
||||||
|
<path d="M10.5 8C10.5 9.38071 9.38071 10.5 8 10.5C6.61929 10.5 5.5 9.38071 5.5 8C5.5 6.61929 6.61929 5.5 8 5.5C9.38071 5.5 10.5 6.61929 10.5 8Z" stroke="#3574F0"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 453 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="8" cy="8" r="6.5" stroke="#CED0D6"/>
|
||||||
|
<path d="M10.5 8C10.5 9.38071 9.38071 10.5 8 10.5C6.61929 10.5 5.5 9.38071 5.5 8C5.5 6.61929 6.61929 5.5 8 5.5C9.38071 5.5 10.5 6.61929 10.5 8Z" stroke="#548AF7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 453 B |
2
plugins/geo/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
termora.plugins.geo.first-message=The first time you use the <b>Geo</b> plugin, it will download the <b>GeoLite2.mmdb</b> database. <br/>Once the download is complete, it will display the host region information.
|
||||||
|
termora.plugins.geo.coming-soon=Geo loading
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
termora.plugins.geo.first-message=首次使用 <b>Geo</b> 插件会下载 <b>GeoLite2.mmdb</b> 数据库,下载完成后会显示主机地域信息
|
||||||
|
termora.plugins.geo.coming-soon=Geo 加载中
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
termora.plugins.geo.first-message=首次使用 <b>Geo</b> 外掛程式會下載 <b>GeoLite2.mmdb</b> 資料庫,下載完成後會顯示主機地域訊息
|
||||||
|
termora.plugins.geo.coming-soon=Geo 加载中
|
||||||
22
plugins/migration/build.gradle.kts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
project.version = "0.0.2"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
compileOnly(project(":"))
|
||||||
|
|
||||||
|
implementation(libs.xodus.vfs)
|
||||||
|
implementation(libs.xodus.openAPI)
|
||||||
|
implementation(libs.xodus.environment)
|
||||||
|
implementation(libs.bip39)
|
||||||
|
implementation(libs.commons.compress)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ext.set("Termora-Plugin-Entry", "app.termora.plugins.migration.MigrationPlugin")
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
@@ -1,25 +1,22 @@
|
|||||||
package app.termora
|
package app.termora.plugins.migration
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.highlight.KeywordHighlight
|
import app.termora.highlight.KeywordHighlight
|
||||||
import app.termora.keymap.Keymap
|
import app.termora.keymap.Keymap
|
||||||
import app.termora.keymgr.OhKeyPair
|
import app.termora.keymgr.OhKeyPair
|
||||||
import app.termora.macro.Macro
|
import app.termora.macro.Macro
|
||||||
import app.termora.sync.SyncType
|
import app.termora.snippet.Snippet
|
||||||
import app.termora.terminal.CursorStyle
|
import app.termora.terminal.CursorStyle
|
||||||
import jetbrains.exodus.bindings.StringBinding
|
import jetbrains.exodus.bindings.StringBinding
|
||||||
import jetbrains.exodus.env.*
|
import jetbrains.exodus.env.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.component1
|
|
||||||
import kotlin.collections.component2
|
|
||||||
import kotlin.collections.set
|
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
@@ -28,9 +25,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val KEYMAP_STORE = "Keymap"
|
private const val KEYMAP_STORE = "Keymap"
|
||||||
private const val HOST_STORE = "Host"
|
private const val HOST_STORE = "Host"
|
||||||
|
private const val SNIPPET_STORE = "Snippet"
|
||||||
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
|
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
|
||||||
private const val MACRO_STORE = "Macro"
|
private const val MACRO_STORE = "Macro"
|
||||||
private const val KEY_PAIR_STORE = "KeyPair"
|
private const val KEY_PAIR_STORE = "KeyPair"
|
||||||
|
private const val DELETED_DATA_STORE = "DeletedData"
|
||||||
private val log = LoggerFactory.getLogger(Database::class.java)
|
private val log = LoggerFactory.getLogger(Database::class.java)
|
||||||
|
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
|
|
||||||
fun getDatabase(): Database {
|
fun getDatabase(): Database {
|
||||||
return ApplicationScope.forApplicationScope()
|
return ApplicationScope.forApplicationScope()
|
||||||
.getOrCreate(Database::class) { open(Application.getDatabaseFile()) }
|
.getOrCreate(Database::class) { open(MigrationApplicationRunnerExtension.instance.getDatabaseFile()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +54,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
val safetyProperties by lazy { SafetyProperties("Setting.SafetyProperties") }
|
val safetyProperties by lazy { SafetyProperties("Setting.SafetyProperties") }
|
||||||
val terminal by lazy { Terminal() }
|
val terminal by lazy { Terminal() }
|
||||||
val appearance by lazy { Appearance() }
|
val appearance by lazy { Appearance() }
|
||||||
|
val sftp by lazy { SFTP() }
|
||||||
val sync by lazy { Sync() }
|
val sync by lazy { Sync() }
|
||||||
|
|
||||||
private val doorman get() = Doorman.getInstance()
|
private val doorman get() = Doorman.getInstance()
|
||||||
@@ -106,17 +106,6 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAllHost() {
|
|
||||||
env.executeInTransaction { tx ->
|
|
||||||
val store = env.openStore(HOST_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
|
||||||
store.openCursor(tx).use {
|
|
||||||
while (it.next) {
|
|
||||||
it.deleteCurrent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAllKeyPair() {
|
fun removeAllKeyPair() {
|
||||||
env.executeInTransaction { tx ->
|
env.executeInTransaction { tx ->
|
||||||
val store = env.openStore(KEY_PAIR_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
val store = env.openStore(KEY_PAIR_STORE, StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING, tx)
|
||||||
@@ -157,11 +146,67 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
env.executeInTransaction {
|
env.executeInTransaction {
|
||||||
delete(it, HOST_STORE, id)
|
delete(it, HOST_STORE, id)
|
||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("Removed Host: $id")
|
log.debug("Removed host: $id")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addDeletedData(deletedData: DeletedData) {
|
||||||
|
val text = ohMyJson.encodeToString(deletedData)
|
||||||
|
env.executeInTransaction {
|
||||||
|
put(it, DELETED_DATA_STORE, deletedData.id, text)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Added DeletedData: ${deletedData.id} , $text")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDeletedData(): Collection<DeletedData> {
|
||||||
|
return env.computeInTransaction { tx ->
|
||||||
|
openCursor<DeletedData?>(tx, DELETED_DATA_STORE) { _, value ->
|
||||||
|
try {
|
||||||
|
ohMyJson.decodeFromString(value)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.values.filterNotNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addSnippet(snippet: Snippet) {
|
||||||
|
var text = ohMyJson.encodeToString(snippet)
|
||||||
|
if (doorman.isWorking()) {
|
||||||
|
text = doorman.encrypt(text)
|
||||||
|
}
|
||||||
|
env.executeInTransaction {
|
||||||
|
put(it, SNIPPET_STORE, snippet.id, text)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Added Snippet: ${snippet.id} , ${snippet.name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeSnippet(id: String) {
|
||||||
|
env.executeInTransaction {
|
||||||
|
delete(it, SNIPPET_STORE, id)
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Removed snippet: $id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSnippets(): Collection<Snippet> {
|
||||||
|
val isWorking = doorman.isWorking()
|
||||||
|
return env.computeInTransaction { tx ->
|
||||||
|
openCursor<Snippet>(tx, SNIPPET_STORE) { _, value ->
|
||||||
|
if (isWorking)
|
||||||
|
ohMyJson.decodeFromString(doorman.decrypt(value))
|
||||||
|
else
|
||||||
|
ohMyJson.decodeFromString(value)
|
||||||
|
}.values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getKeywordHighlights(): Collection<KeywordHighlight> {
|
fun getKeywordHighlights(): Collection<KeywordHighlight> {
|
||||||
return env.computeInTransaction { tx ->
|
return env.computeInTransaction { tx ->
|
||||||
openCursor<KeywordHighlight>(tx, KEYWORD_HIGHLIGHT_STORE) { _, value ->
|
openCursor<KeywordHighlight>(tx, KEYWORD_HIGHLIGHT_STORE) { _, value ->
|
||||||
@@ -298,12 +343,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
abstract inner class Property(private val name: String) {
|
abstract inner class Property(val name: String) {
|
||||||
private val properties = Collections.synchronizedMap(mutableMapOf<String, String>())
|
private val properties = Collections.synchronizedMap(mutableMapOf<String, String>())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@Suppress("OPT_IN_USAGE")
|
swingCoroutineScope.launch(Dispatchers.IO) { properties.putAll(getProperties()) }
|
||||||
GlobalScope.launch(Dispatchers.IO) { properties.putAll(getProperties()) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun getString(key: String): String? {
|
protected open fun getString(key: String): String? {
|
||||||
@@ -375,6 +419,13 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected inner class DoublePropertyDelegate(defaultValue: Double) :
|
||||||
|
PropertyDelegate<Double>(defaultValue) {
|
||||||
|
override fun convertValue(value: String): Double {
|
||||||
|
return value.toDoubleOrNull() ?: initializer.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
protected inner class LongPropertyDelegate(defaultValue: Long) :
|
protected inner class LongPropertyDelegate(defaultValue: Long) :
|
||||||
PropertyDelegate<Long>(defaultValue) {
|
PropertyDelegate<Long>(defaultValue) {
|
||||||
@@ -401,10 +452,10 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
protected inner class CursorStylePropertyDelegate(defaultValue: CursorStyle) :
|
protected inner class CursorStylePropertyDelegate(defaultValue: CursorStyle) :
|
||||||
PropertyDelegate<CursorStyle>(defaultValue) {
|
PropertyDelegate<CursorStyle>(defaultValue) {
|
||||||
override fun convertValue(value: String): CursorStyle {
|
override fun convertValue(value: String): CursorStyle {
|
||||||
try {
|
return try {
|
||||||
return CursorStyle.valueOf(value)
|
CursorStyle.valueOf(value)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
return initializer.invoke()
|
initializer.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -459,6 +510,16 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
*/
|
*/
|
||||||
var beep by BooleanPropertyDelegate(true)
|
var beep by BooleanPropertyDelegate(true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 超链接
|
||||||
|
*/
|
||||||
|
var hyperlink by BooleanPropertyDelegate(true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 光标闪烁
|
||||||
|
*/
|
||||||
|
var cursorBlink by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选中复制
|
* 选中复制
|
||||||
*/
|
*/
|
||||||
@@ -473,6 +534,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
* 终端断开连接时自动关闭Tab
|
* 终端断开连接时自动关闭Tab
|
||||||
*/
|
*/
|
||||||
var autoCloseTabWhenDisconnected by BooleanPropertyDelegate(false)
|
var autoCloseTabWhenDisconnected by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否显示悬浮工具栏
|
||||||
|
*/
|
||||||
|
var floatingToolbar by BooleanPropertyDelegate(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -564,6 +630,21 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
var darkTheme by StringPropertyDelegate("Dark")
|
var darkTheme by StringPropertyDelegate("Dark")
|
||||||
var lightTheme by StringPropertyDelegate("Light")
|
var lightTheme by StringPropertyDelegate("Light")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 允许后台运行,也就是托盘
|
||||||
|
*/
|
||||||
|
var backgroundRunning by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签关闭前确认
|
||||||
|
*/
|
||||||
|
var confirmTabClose by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 背景图片的地址
|
||||||
|
*/
|
||||||
|
var backgroundImage by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 语言
|
* 语言
|
||||||
*/
|
*/
|
||||||
@@ -571,6 +652,46 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
I18n.containsLanguage(Locale.getDefault()) ?: Locale.US.toString()
|
I18n.containsLanguage(Locale.getDefault()) ?: Locale.US.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 透明度
|
||||||
|
*/
|
||||||
|
var opacity by DoublePropertyDelegate(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SFTP
|
||||||
|
*/
|
||||||
|
inner class SFTP : Property("Setting.SFTP") {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑命令
|
||||||
|
*/
|
||||||
|
var editCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sftp command
|
||||||
|
*/
|
||||||
|
var sftpCommand by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* defaultDirectory
|
||||||
|
*/
|
||||||
|
var defaultDirectory by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否固定在标签栏
|
||||||
|
*/
|
||||||
|
var pinTab by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否保留原始文件时间
|
||||||
|
*/
|
||||||
|
var preserveModificationTime by BooleanPropertyDelegate(false)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -587,6 +708,7 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
*/
|
*/
|
||||||
var rangeHosts by BooleanPropertyDelegate(true)
|
var rangeHosts by BooleanPropertyDelegate(true)
|
||||||
var rangeKeyPairs by BooleanPropertyDelegate(true)
|
var rangeKeyPairs by BooleanPropertyDelegate(true)
|
||||||
|
var rangeSnippets by BooleanPropertyDelegate(true)
|
||||||
var rangeKeywordHighlights by BooleanPropertyDelegate(true)
|
var rangeKeywordHighlights by BooleanPropertyDelegate(true)
|
||||||
var rangeMacros by BooleanPropertyDelegate(true)
|
var rangeMacros by BooleanPropertyDelegate(true)
|
||||||
var rangeKeymap by BooleanPropertyDelegate(true)
|
var rangeKeymap by BooleanPropertyDelegate(true)
|
||||||
@@ -610,6 +732,11 @@ class Database private constructor(private val env: Environment) : Disposable {
|
|||||||
* 最后同步时间
|
* 最后同步时间
|
||||||
*/
|
*/
|
||||||
var lastSyncTime by LongPropertyDelegate(0L)
|
var lastSyncTime by LongPropertyDelegate(0L)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步策略,为空就是默认手动
|
||||||
|
*/
|
||||||
|
var policy by StringPropertyDelegate(StringUtils.EMPTY)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora.plugins.migration
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
import app.termora.AES.decodeBase64
|
import app.termora.AES.decodeBase64
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
@@ -86,7 +87,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
|||||||
.layout(
|
.layout(
|
||||||
FormLayout(
|
FormLayout(
|
||||||
"$formMargin, default:grow, 4dlu, pref, $formMargin",
|
"$formMargin, default:grow, 4dlu, pref, $formMargin",
|
||||||
"${if (SystemInfo.isWindows) "20dlu" else "0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
"${"0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.add(icon).xyw(2, rows, 4).apply { rows += step }
|
.add(icon).xyw(2, rows, 4).apply { rows += step }
|
||||||
@@ -114,7 +115,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
|||||||
I18n.getString("termora.doorman.delete-data"),
|
I18n.getString("termora.doorman.delete-data"),
|
||||||
messageType = JOptionPane.WARNING_MESSAGE
|
messageType = JOptionPane.WARNING_MESSAGE
|
||||||
)
|
)
|
||||||
Application.browse(Application.getDatabaseFile().toURI())
|
Application.browse(MigrationApplicationRunnerExtension.instance.getDatabaseFile().toURI())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).apply { isFocusable = false }).xyw(2, rows, 4, "center, fill")
|
}).apply { isFocusable = false }).xyw(2, rows, 4, "center, fill")
|
||||||
@@ -136,6 +137,9 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
|||||||
val key = AES.ECB.decrypt(entropy, keyBackup.decodeBase64())
|
val key = AES.ECB.decrypt(entropy, keyBackup.decodeBase64())
|
||||||
Doorman.getInstance().work(key)
|
Doorman.getInstance().work(key)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
OptionPane.showMessageDialog(
|
OptionPane.showMessageDialog(
|
||||||
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
||||||
messageType = JOptionPane.ERROR_MESSAGE
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
@@ -219,7 +223,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
|
val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin")
|
||||||
.layout(layout).debug(true)
|
.layout(layout).debug(false)
|
||||||
val iterator = textFields.iterator()
|
val iterator = textFields.iterator()
|
||||||
for (i in 1..5 step 2) {
|
for (i in 1..5 step 2) {
|
||||||
for (j in 1..7 step 2) {
|
for (j in 1..7 step 2) {
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package app.termora.plugins.migration
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.account.AccountManager
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
|
import app.termora.database.DatabaseManager
|
||||||
|
import app.termora.database.OwnerType
|
||||||
|
import app.termora.highlight.KeywordHighlightManager
|
||||||
|
import app.termora.keymap.KeymapManager
|
||||||
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.macro.MacroManager
|
||||||
|
import app.termora.snippet.SnippetManager
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
class MigrationApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(MigrationApplicationRunnerExtension::class.java)
|
||||||
|
val instance by lazy { MigrationApplicationRunnerExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ready() {
|
||||||
|
val file = getDatabaseFile()
|
||||||
|
if (file.exists().not()) return
|
||||||
|
|
||||||
|
// 如果数据库文件存在,那么需要迁移文件
|
||||||
|
val countDownLatch = CountDownLatch(1)
|
||||||
|
|
||||||
|
SwingUtilities.invokeAndWait {
|
||||||
|
try {
|
||||||
|
// 打开数据
|
||||||
|
openDatabase()
|
||||||
|
|
||||||
|
// 尝试解锁
|
||||||
|
openDoor()
|
||||||
|
|
||||||
|
// 询问是否迁移
|
||||||
|
if (askMigrate()) {
|
||||||
|
|
||||||
|
// 迁移
|
||||||
|
migrate()
|
||||||
|
|
||||||
|
// 移动到旧的目录
|
||||||
|
moveOldDirectory()
|
||||||
|
|
||||||
|
// 重启
|
||||||
|
restart()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
countDownLatch.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
countDownLatch.await()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openDoor() {
|
||||||
|
if (Doorman.getInstance().isWorking()) {
|
||||||
|
if (DoormanDialog(null).open().not()) {
|
||||||
|
Disposer.dispose(TermoraFrameManager.getInstance())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openDatabase() {
|
||||||
|
try {
|
||||||
|
// 初始化数据库
|
||||||
|
Database.getDatabase()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
JOptionPane.showMessageDialog(
|
||||||
|
null, "Unable to open database",
|
||||||
|
I18n.getString("termora.title"), JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrate() {
|
||||||
|
val database = Database.getDatabase()
|
||||||
|
val accountManager = AccountManager.getInstance()
|
||||||
|
val databaseManager = DatabaseManager.getInstance()
|
||||||
|
val ownerId = accountManager.getAccountId()
|
||||||
|
val hostManager = HostManager.getInstance()
|
||||||
|
val snippetManager = SnippetManager.getInstance()
|
||||||
|
val macroManager = MacroManager.getInstance()
|
||||||
|
val keymapManager = KeymapManager.getInstance()
|
||||||
|
val keyManager = KeyManager.getInstance()
|
||||||
|
val highlightManager = KeywordHighlightManager.getInstance()
|
||||||
|
val accountOwner = AccountOwner(
|
||||||
|
id = accountManager.getAccountId(),
|
||||||
|
name = accountManager.getEmail(),
|
||||||
|
type = OwnerType.User
|
||||||
|
)
|
||||||
|
|
||||||
|
for (host in database.getHosts()) {
|
||||||
|
if (host.deleted) continue
|
||||||
|
hostManager.addHost(host.copy(ownerId = accountManager.getAccountId(), ownerType = OwnerType.User.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (snippet in database.getSnippets()) {
|
||||||
|
if (snippet.deleted) continue
|
||||||
|
snippetManager.addSnippet(snippet)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (macro in database.getMacros()) {
|
||||||
|
macroManager.addMacro(macro)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (keymap in database.getKeymaps()) {
|
||||||
|
keymapManager.addKeymap(keymap)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (keypair in database.getKeyPairs()) {
|
||||||
|
keyManager.addOhKeyPair(keypair, accountOwner)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (e in database.getKeywordHighlights()) {
|
||||||
|
highlightManager.addKeywordHighlight(e, accountOwner)
|
||||||
|
}
|
||||||
|
|
||||||
|
val list = listOf(
|
||||||
|
database.sync,
|
||||||
|
database.properties,
|
||||||
|
database.terminal,
|
||||||
|
database.sftp,
|
||||||
|
database.appearance,
|
||||||
|
)
|
||||||
|
|
||||||
|
for (e in list) {
|
||||||
|
for (k in e.getProperties()) {
|
||||||
|
databaseManager.setSetting(e.name + "." + k.key, k.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (e in database.safetyProperties.getProperties()) {
|
||||||
|
databaseManager.setSetting(database.properties.name + "." + e.key, e.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun askMigrate(): Boolean {
|
||||||
|
|
||||||
|
if (MigrationDialog(null).open()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动到旧的目录
|
||||||
|
moveOldDirectory()
|
||||||
|
|
||||||
|
// 重启
|
||||||
|
restart()
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun moveOldDirectory() {
|
||||||
|
// 关闭数据库
|
||||||
|
Disposer.dispose(Database.getDatabase())
|
||||||
|
|
||||||
|
val file = getDatabaseFile()
|
||||||
|
FileUtils.moveDirectory(
|
||||||
|
file,
|
||||||
|
FileUtils.getFile(file.parentFile, file.name + "-old-" + System.currentTimeMillis())
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restart() {
|
||||||
|
|
||||||
|
// 重启
|
||||||
|
TermoraRestarter.getInstance().scheduleRestart(null, ask = false)
|
||||||
|
|
||||||
|
// 退出程序
|
||||||
|
Disposer.dispose(TermoraFrameManager.getInstance())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getDatabaseFile(): File {
|
||||||
|
return FileUtils.getFile(Application.getBaseDataDir(), "storage")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package app.termora.plugins.migration
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.jdesktop.swingx.JXEditorPane
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
import javax.swing.*
|
||||||
|
import javax.swing.event.HyperlinkEvent
|
||||||
|
|
||||||
|
class MigrationDialog(owner: Window?) : DialogWrapper(owner) {
|
||||||
|
|
||||||
|
private var isOpened = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
||||||
|
isModal = true
|
||||||
|
isResizable = false
|
||||||
|
controlsVisible = false
|
||||||
|
escapeDispose = false
|
||||||
|
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
title = StringUtils.EMPTY
|
||||||
|
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||||
|
val sizes = listOf(16, 20, 24, 28, 32, 48, 64)
|
||||||
|
val loader = TermoraFrame::class.java.classLoader
|
||||||
|
val images = sizes.mapNotNull { e ->
|
||||||
|
loader.getResourceAsStream("icons/termora_${e}x${e}.png")?.use { ImageIO.read(it) }
|
||||||
|
}
|
||||||
|
iconImages = images
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocationRelativeTo(null)
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
var rows = 2
|
||||||
|
val step = 2
|
||||||
|
val formMargin = "7dlu"
|
||||||
|
val icon = JLabel()
|
||||||
|
icon.horizontalAlignment = SwingConstants.CENTER
|
||||||
|
icon.icon = FlatSVGIcon(Icons.newUI.name, 80, 80)
|
||||||
|
|
||||||
|
val editorPane = JXEditorPane()
|
||||||
|
editorPane.contentType = "text/html"
|
||||||
|
editorPane.text = MigrationI18n.getString("termora.plugins.migration.message")
|
||||||
|
editorPane.isEditable = false
|
||||||
|
editorPane.addHyperlinkListener {
|
||||||
|
if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) {
|
||||||
|
Application.browse(it.url.toURI())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editorPane.background = DynamicColor("window")
|
||||||
|
val scrollPane = JScrollPane(editorPane)
|
||||||
|
scrollPane.border = BorderFactory.createEmptyBorder()
|
||||||
|
scrollPane.preferredSize = Dimension(Int.MAX_VALUE, 225)
|
||||||
|
|
||||||
|
addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowOpened(e: WindowEvent) {
|
||||||
|
removeWindowListener(this)
|
||||||
|
SwingUtilities.invokeLater { scrollPane.verticalScrollBar.value = 0 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return FormBuilder.create().debug(false)
|
||||||
|
.layout(
|
||||||
|
FormLayout(
|
||||||
|
"$formMargin, default:grow, 4dlu, pref, $formMargin",
|
||||||
|
"${"0dlu"}, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.add(icon).xyw(2, rows, 4).apply { rows += step }
|
||||||
|
.add(scrollPane).xyw(2, rows, 4).apply { rows += step }
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun open(): Boolean {
|
||||||
|
isModal = true
|
||||||
|
isVisible = true
|
||||||
|
return isOpened
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doOKAction() {
|
||||||
|
isOpened = true
|
||||||
|
super.doOKAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doCancelAction() {
|
||||||
|
isOpened = false
|
||||||
|
super.doCancelAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createOkAction(): AbstractAction {
|
||||||
|
return OkAction(MigrationI18n.getString("termora.plugins.migration.migrate"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package app.termora.plugins.migration
|
||||||
|
|
||||||
|
import app.termora.NamedI18n
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
object MigrationI18n : NamedI18n("i18n/messages") {
|
||||||
|
private val log = LoggerFactory.getLogger(MigrationI18n::class.java)
|
||||||
|
|
||||||
|
override fun getLogger(): Logger {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package app.termora.plugins.migration
|
||||||
|
|
||||||
|
import app.termora.ApplicationRunnerExtension
|
||||||
|
import app.termora.DynamicIcon
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionSupport
|
||||||
|
import app.termora.plugin.Plugin
|
||||||
|
|
||||||
|
class MigrationPlugin : Plugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(ApplicationRunnerExtension::class.java) { MigrationApplicationRunnerExtension.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "Migration"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package app.termora.plugins.migration
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import javax.crypto.SecretKeyFactory
|
||||||
|
import javax.crypto.spec.PBEKeySpec
|
||||||
|
import kotlin.time.measureTime
|
||||||
|
|
||||||
|
object PBKDF2 {
|
||||||
|
|
||||||
|
private const val ALGORITHM = "PBKDF2WithHmacSHA512"
|
||||||
|
private val log = LoggerFactory.getLogger(PBKDF2::class.java)
|
||||||
|
|
||||||
|
fun generateSecret(
|
||||||
|
password: CharArray,
|
||||||
|
salt: ByteArray,
|
||||||
|
iterationCount: Int = 150000,
|
||||||
|
keyLength: Int = 256
|
||||||
|
): ByteArray {
|
||||||
|
val bytes: ByteArray
|
||||||
|
val time = measureTime {
|
||||||
|
bytes = SecretKeyFactory.getInstance(ALGORITHM)
|
||||||
|
.generateSecret(PBEKeySpec(password, salt, iterationCount, keyLength))
|
||||||
|
.encoded
|
||||||
|
}
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.debug("Secret generated $time")
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hash(slat: ByteArray, password: CharArray, iterationCount: Int, keyLength: Int): ByteArray {
|
||||||
|
val spec = PBEKeySpec(password, slat, iterationCount, keyLength)
|
||||||
|
val secretKeyFactory = SecretKeyFactory.getInstance(ALGORITHM)
|
||||||
|
return secretKeyFactory.generateSecret(spec).encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora.plugins.migration
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
import app.termora.AES.decodeBase64
|
import app.termora.AES.decodeBase64
|
||||||
import app.termora.AES.encodeBase64String
|
import app.termora.AES.encodeBase64String
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package app.termora.plugins.migration
|
||||||
|
enum class SyncType {
|
||||||
|
GitLab,
|
||||||
|
GitHub,
|
||||||
|
Gitee,
|
||||||
|
WebDAV,
|
||||||
|
}
|
||||||
24
plugins/migration/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>migration</id>
|
||||||
|
|
||||||
|
<name>Migration</name>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<!-- since: >=xxx , or >xxx -->
|
||||||
|
<!-- until: <=xxx , or <xxx -->
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.migration.MigrationPlugin</entry>
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Migrate version 1.x configuration files to 2.x</description>
|
||||||
|
<description language="zh_CN">将 1.x 版本的配置文件迁移到 2.x</description>
|
||||||
|
<description language="zh_TW">將 1.x 版本的設定檔移轉到 2.x</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
19
plugins/migration/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_659_75852)">
|
||||||
|
<path d="M7.49998 0.523674C8.50002 5.49999 10.5 7.49999 15.554 8.5C10.5 9.49999 8.5 11.5 7.50005 16.4763C6.50002 11.5 4.50002 9.49999 -0.553986 8.49998C4.5 7.49999 6.5 5.49999 7.49998 0.523674Z" fill="url(#paint0_linear_659_75852)"/>
|
||||||
|
<path d="M12.9933 4.90705C14.0451 4.90705 14.8979 4.05433 14.8979 3.00245C14.8979 1.95056 14.0451 1.09784 12.9933 1.09784C11.9414 1.09784 11.0886 1.95056 11.0886 3.00245C11.0886 4.05433 11.9414 4.90705 12.9933 4.90705Z" fill="url(#paint1_linear_659_75852)"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_659_75852" x1="7.50002" y1="0.523674" x2="7.50002" y2="16.4763" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#3573F0"/>
|
||||||
|
<stop offset="1" stop-color="#EA33EC"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint1_linear_659_75852" x1="7.50002" y1="0.523674" x2="7.50002" y2="16.4763" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#3573F0"/>
|
||||||
|
<stop offset="1" stop-color="#EA33EC"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="clip0_659_75852">
|
||||||
|
<rect width="16" height="16" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
termora.plugins.migration.message=<html> \
|
||||||
|
<h1 align="center">2.0 is ready.</h1> \
|
||||||
|
<br/> \
|
||||||
|
<h3>1. The storage structure has been updated. Existing data needs to be migrated. Just click <font color="#3573F0">“Migrate”</font> to complete the process.</h3> \
|
||||||
|
<h3>2. The <font color="#3573F0">Sync feature</font> is now provided as a plugin. If needed, please <font color="#EA33EC">manually install</font> it from Settings.</h3> \
|
||||||
|
<h3>3. The <font color="#3573F0">Data Encryption</font> feature has been <font color="#EA33EC">removed</font> (local data will now be stored with basic encryption). Please ensure your device is in a trusted environment.</h3> \
|
||||||
|
<h3 align="center">📎 For more information, please see: <a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||||
|
</html>
|
||||||
|
termora.plugins.migration.migrate=Migrate
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
termora.plugins.migration.message=<html> \
|
||||||
|
<h1 align="center">2.0 已就绪。</h1> \
|
||||||
|
<br/> \
|
||||||
|
<h3>1. 存储结构已更新,需迁移现有数据。只需点击 <font color="#3573F0">“迁移”</font> 即可完成操作。</h3> \
|
||||||
|
<h3>2. <font color="#3573F0">同步功能</font> 现作为插件提供,如需使用,请前往设置中 <font color="#EA33EC">手动安装</font>。</h3> \
|
||||||
|
<h3>3. <font color="#3573F0">数据加密</font> 功能已被 <font color="#EA33EC">移除</font>(本地数据将以简单加密方式存储),请确保你的设备处于可信环境中。</h3> \
|
||||||
|
<h3 align="center">📎 更多信息请查看:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||||
|
</html>
|
||||||
|
termora.plugins.migration.migrate=迁移
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
termora.plugins.migration.message=<html> \
|
||||||
|
<h1 align="center">2.0 已準備就緒。</h1> \
|
||||||
|
<br/> \
|
||||||
|
<h3>1. 儲存結構已更新,需要遷移現有資料。只需點擊 <font color="#3573F0">「遷移」</font> 即可完成操作。</h3> \
|
||||||
|
<h3>2. <font color="#3573F0">同步功能</font> 現以外掛形式提供,如需使用,請至設定中 <font color="#EA33EC">手動安裝</font>。</h3> \
|
||||||
|
<h3>3. <font color="#3573F0">資料加密</font> 功能已被 <font color="#EA33EC">移除</font>(本機資料將以簡易加密方式儲存),請確保你的裝置處於可信環境中。</h3> \
|
||||||
|
<h3 align="center">📎 更多資訊請參見:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||||
|
</html>
|
||||||
|
termora.plugins.migration.migrate=遷移
|
||||||
16
plugins/obs/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
project.version = "0.0.1"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.4")
|
||||||
|
compileOnly(project(":"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package app.termora.plugins.obs
|
||||||
|
|
||||||
|
import app.termora.AuthenticationType
|
||||||
|
import app.termora.Proxy
|
||||||
|
import app.termora.ProxyType
|
||||||
|
import com.obs.services.IObsCredentialsProvider
|
||||||
|
import com.obs.services.ObsClient
|
||||||
|
import com.obs.services.ObsConfiguration
|
||||||
|
import com.obs.services.model.ObsBucket
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
class OBSClientHandler(
|
||||||
|
private val cred: IObsCredentialsProvider,
|
||||||
|
private val proxy: Proxy,
|
||||||
|
val buckets: List<ObsBucket>
|
||||||
|
) : Closeable {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun createOBSClient(
|
||||||
|
cred: IObsCredentialsProvider,
|
||||||
|
endpoint: String,
|
||||||
|
proxy: Proxy
|
||||||
|
): ObsClient {
|
||||||
|
val configuration = ObsConfiguration()
|
||||||
|
|
||||||
|
if (proxy.type == ProxyType.HTTP) {
|
||||||
|
if (proxy.authenticationType == AuthenticationType.Password) {
|
||||||
|
configuration.setHttpProxy(proxy.host, proxy.port, proxy.username, proxy.password)
|
||||||
|
} else {
|
||||||
|
configuration.setHttpProxy(proxy.host, proxy.port, null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var newEndpoint = endpoint
|
||||||
|
if ((newEndpoint.startsWith("http://") || newEndpoint.startsWith("https://")).not()) {
|
||||||
|
newEndpoint = "https://$endpoint"
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration.endPoint = newEndpoint
|
||||||
|
|
||||||
|
val obsClient = ObsClient(cred, configuration)
|
||||||
|
|
||||||
|
|
||||||
|
return obsClient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* key: Region
|
||||||
|
* value: Client
|
||||||
|
*/
|
||||||
|
private val clients = mutableMapOf<String, ObsClient>()
|
||||||
|
private val closed = AtomicBoolean(false)
|
||||||
|
|
||||||
|
fun getClientForBucket(bucket: String): ObsClient {
|
||||||
|
if (closed.get()) throw IllegalStateException("Client already closed")
|
||||||
|
|
||||||
|
synchronized(this) {
|
||||||
|
val bucket = buckets.first { it.bucketName == bucket }
|
||||||
|
if (clients.containsKey(bucket.location)) {
|
||||||
|
return clients.getValue(bucket.location)
|
||||||
|
}
|
||||||
|
clients[bucket.location] = createOBSClient(cred, "https://obs.${bucket.location}.myhuaweicloud.com", proxy)
|
||||||
|
return clients.getValue(bucket.location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
if (closed.compareAndSet(false, true)) {
|
||||||
|
synchronized(this) {
|
||||||
|
clients.forEach { IOUtils.closeQuietly(it.value) }
|
||||||
|
clients.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package app.termora.plugins.obs
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3FileSystem
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* key: region
|
||||||
|
*/
|
||||||
|
class OBSFileSystem(private val clientHandler: OBSClientHandler) :
|
||||||
|
S3FileSystem(OBSFileSystemProvider(clientHandler)) {
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(clientHandler)
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package app.termora.plugins.obs
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3FileAttributes
|
||||||
|
import app.termora.transfer.s3.S3FileSystemProvider
|
||||||
|
import app.termora.transfer.s3.S3Path
|
||||||
|
import com.obs.services.model.ListObjectsRequest
|
||||||
|
import com.obs.services.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 OBSFileSystemProvider(private val clientHandler: OBSClientHandler) : S3FileSystemProvider() {
|
||||||
|
|
||||||
|
|
||||||
|
override fun getScheme(): String? {
|
||||||
|
return "oss"
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
} 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.bucketName)
|
||||||
|
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()
|
||||||
|
request.bucketName = bucketName
|
||||||
|
request.maxKeys = maxKeys
|
||||||
|
request.delimiter = path.fileSystem.separator
|
||||||
|
|
||||||
|
if (path.objectName.isNotBlank()) request.prefix = path.objectName + path.fileSystem.separator
|
||||||
|
if (nextMarker.isNotBlank()) request.marker = 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.objects) {
|
||||||
|
val p = path.bucket.resolve(e.objectKey)
|
||||||
|
p.attributes = p.attributes.copy(
|
||||||
|
regularFile = true, size = e.metadata.contentLength,
|
||||||
|
lastModifiedTime = e.metadata.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,344 @@
|
|||||||
|
package app.termora.plugins.obs
|
||||||
|
|
||||||
|
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 OBSHostOptionsPane : 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 = OBSProtocolProvider.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(
|
||||||
|
"oss.delimiter" to StringUtils.defaultIfBlank(generalOption.delimiterTextField.text, "/"),
|
||||||
|
// "oss.region" to generalOption.regionComboBox.selectedItem as String,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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["oss.delimiter"] ?: "/"
|
||||||
|
// generalOption.regionComboBox.selectedItem = host.options.extras["oss.region"] ?: 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 delimiterTextField = OutlineTextField(128)
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
|
||||||
|
/*regionComboBox.isEditable = true
|
||||||
|
|
||||||
|
|
||||||
|
// 亚太-中国
|
||||||
|
regionComboBox.addItem("oss-cn-hangzhou")
|
||||||
|
regionComboBox.addItem("oss-cn-shanghai")
|
||||||
|
regionComboBox.addItem("oss-cn-nanjing")
|
||||||
|
regionComboBox.addItem("oss-cn-qingdao")
|
||||||
|
regionComboBox.addItem("oss-cn-beijing")
|
||||||
|
regionComboBox.addItem("oss-cn-zhangjiakou")
|
||||||
|
regionComboBox.addItem("oss-cn-huhehaote")
|
||||||
|
regionComboBox.addItem("oss-cn-wulanchabu")
|
||||||
|
regionComboBox.addItem("oss-cn-shenzhen")
|
||||||
|
regionComboBox.addItem("oss-cn-heyuan")
|
||||||
|
regionComboBox.addItem("oss-cn-guangzhou")
|
||||||
|
regionComboBox.addItem("oss-cn-chengdu")
|
||||||
|
regionComboBox.addItem("oss-cn-hongkong")
|
||||||
|
|
||||||
|
// 亚太-其他
|
||||||
|
regionComboBox.addItem("oss-ap-northeast-1")
|
||||||
|
regionComboBox.addItem("oss-ap-northeast-2")
|
||||||
|
regionComboBox.addItem("oss-ap-southeast-1")
|
||||||
|
regionComboBox.addItem("oss-ap-southeast-3")
|
||||||
|
regionComboBox.addItem("oss-ap-southeast-5")
|
||||||
|
regionComboBox.addItem("oss-ap-southeast-6")
|
||||||
|
regionComboBox.addItem("oss-ap-southeast-7")
|
||||||
|
|
||||||
|
// 欧洲与美洲
|
||||||
|
regionComboBox.addItem("oss-eu-central-1")
|
||||||
|
regionComboBox.addItem("oss-eu-west-1")
|
||||||
|
regionComboBox.addItem("oss-us-west-1")
|
||||||
|
regionComboBox.addItem("oss-us-east-1")
|
||||||
|
regionComboBox.addItem("oss-na-south-1")
|
||||||
|
|
||||||
|
// 中东
|
||||||
|
regionComboBox.addItem("oss-me-east-1")
|
||||||
|
regionComboBox.addItem("oss-me-central-1")
|
||||||
|
|
||||||
|
endpointTextField.isEditable = false*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/*regionComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
endpointTextField.text = "${regionComboBox.selectedItem}.aliyuncs.com"
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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("Region:").xy(1, rows)
|
||||||
|
// .add(regionComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
// .add("Endpoint:").xy(1, rows)
|
||||||
|
// .add(endpointTextField).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,32 @@
|
|||||||
|
package app.termora.plugins.obs
|
||||||
|
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionSupport
|
||||||
|
import app.termora.plugin.PaidPlugin
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
class OBSPlugin : PaidPlugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(ProtocolProviderExtension::class.java) { OBSProtocolProviderExtension.instance }
|
||||||
|
support.addExtension(ProtocolHostPanelExtension::class.java) { OBSProtocolHostPanelExtension.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "Huawei OBS"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package app.termora.plugins.obs
|
||||||
|
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.Host
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
|
||||||
|
class OBSProtocolHostPanel : ProtocolHostPanel() {
|
||||||
|
|
||||||
|
private val pane = OBSHostOptionsPane()
|
||||||
|
|
||||||
|
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,19 @@
|
|||||||
|
package app.termora.plugins.obs
|
||||||
|
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
|
||||||
|
class OBSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { OBSProtocolHostPanelExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return OBSProtocolProvider.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||||
|
return OBSProtocolHostPanel()
|
||||||
|
}
|
||||||
|
}
|
||||||