Compare commits
219 Commits
1.0.13
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f329ef60df | ||
|
|
8acfdb8bca | ||
|
|
a7aec52f2a | ||
|
|
7f1317a9a7 | ||
|
|
a8a1fea91b | ||
|
|
675ad4608a | ||
|
|
72ba3757e2 | ||
|
|
c58e84d2ae | ||
|
|
18a7a5059b | ||
|
|
f0102b6f13 | ||
|
|
0cf8eb3c17 | ||
|
|
c08a9f2b18 | ||
|
|
728f1f2802 | ||
|
|
7310211fba | ||
|
|
1f3267de0a | ||
|
|
8ddad59c70 | ||
|
|
9ff6d0afa1 | ||
|
|
2341b09f81 | ||
|
|
5830aa937a | ||
|
|
56a9361e86 | ||
|
|
5868aa4d2f | ||
|
|
45135b7299 | ||
|
|
a0020fede1 | ||
|
|
6f1eaab456 | ||
|
|
6173eae772 | ||
|
|
0bb366b1f7 | ||
|
|
9a4d6f7f4d | ||
|
|
a4ae11e301 | ||
|
|
5af0acb619 | ||
|
|
042434b8f8 | ||
|
|
eddc7ef0c6 | ||
|
|
c96ca2d424 | ||
|
|
45be9008fd | ||
|
|
057da4e297 | ||
|
|
e4e41667ff | ||
|
|
95ca0a4af7 | ||
|
|
702dee7983 | ||
|
|
165d544448 | ||
|
|
ecf61bedc4 | ||
|
|
66a81a5da3 | ||
|
|
574c816ebb | ||
|
|
e7cafb74e4 | ||
|
|
5050aa37f5 | ||
|
|
53d3d96a06 | ||
|
|
d40b8a4c9c | ||
|
|
728671509c | ||
|
|
b7178a30fb | ||
|
|
939d6a1fd7 | ||
|
|
2986a9cc46 | ||
|
|
f36afaf5d3 | ||
|
|
8cec835583 | ||
|
|
a32838dad6 | ||
|
|
d54671757e | ||
|
|
d1dba56bcd | ||
|
|
919c06779d | ||
|
|
1c90fb4e18 | ||
|
|
c1f1d5185e | ||
|
|
aa4863712d | ||
|
|
247640f2e5 | ||
|
|
5b1f803fa8 | ||
|
|
accf590c17 | ||
|
|
19fbeab817 | ||
|
|
a785ab4680 | ||
|
|
5ee23cb379 | ||
|
|
145d2de001 | ||
|
|
8d3f5fe622 | ||
|
|
9ce4a88041 | ||
|
|
c0ecc9fa7d | ||
|
|
cb33a4468a | ||
|
|
168c4c5c64 | ||
|
|
9916edbd13 | ||
|
|
ab6b6a2127 | ||
|
|
c45f5f4c92 | ||
|
|
92ee2d72f2 | ||
|
|
a4364bcd6a | ||
|
|
d0827c3b0c | ||
|
|
036a04b0b3 | ||
|
|
eee016c643 | ||
|
|
472bf6e81f | ||
|
|
21229e352f | ||
|
|
1138f48a6e | ||
|
|
f044e0480e | ||
|
|
7047f17783 | ||
|
|
9308f15abb | ||
|
|
b2672f11fc | ||
|
|
f92c6586b2 | ||
|
|
69e07a9bd9 | ||
|
|
cdec60fd25 | ||
|
|
7c30933794 | ||
|
|
b892d2fe13 | ||
|
|
91ee463d41 | ||
|
|
169b66334c | ||
|
|
729eb99730 | ||
|
|
b1e62952f5 | ||
|
|
e21e9f9ed9 | ||
|
|
885c0a6337 | ||
|
|
09d837f5b8 | ||
|
|
b1e1f38b50 | ||
|
|
efa9613d26 | ||
|
|
287f6973f0 | ||
|
|
70fc5e3228 | ||
|
|
4bca15dbb0 | ||
|
|
ef2c57bb29 | ||
|
|
1135ecc5a3 | ||
|
|
e1b2e7b4db | ||
|
|
eec9154aeb | ||
|
|
fff2dd89c7 | ||
|
|
54116a4bf5 | ||
|
|
f28e785301 | ||
|
|
39b9bba9cf | ||
|
|
d7120cabe0 | ||
|
|
007318dae3 | ||
|
|
e25bd485ac | ||
|
|
7ba8e177b1 | ||
|
|
17082c5fb8 | ||
|
|
00dfb4ce39 | ||
|
|
cee0c863f8 | ||
|
|
a2a02c0bad | ||
|
|
891688d0ca | ||
|
|
3d47840aa8 | ||
|
|
01d0f9d4bd | ||
|
|
1c8abf9cba | ||
|
|
efd01da6f1 | ||
|
|
c929a794d5 | ||
|
|
85b2f222f4 | ||
|
|
899bd5a356 | ||
|
|
ded16873b0 | ||
|
|
501013ba31 | ||
|
|
76b1a2f4f8 | ||
|
|
e34fea0ecb | ||
|
|
7532546c77 | ||
|
|
187d5be658 | ||
|
|
2bf4d277be | ||
|
|
389c243ada | ||
|
|
bb39178b88 | ||
|
|
e1eab9db06 | ||
|
|
e6a45d25cd | ||
|
|
a64aef24b2 | ||
|
|
c9c8aa8f2a | ||
|
|
d3cfde5238 | ||
|
|
b4e82a4a0e | ||
|
|
40b4848cc8 | ||
|
|
0c4268a194 | ||
|
|
866c823c30 | ||
|
|
6f9cfc3b32 | ||
|
|
3717393ad9 | ||
|
|
10e7681ac2 | ||
|
|
51047e60f4 | ||
|
|
02984bb2a2 | ||
|
|
ea6b2d6a66 | ||
|
|
95bf08b0da | ||
|
|
15131fefd1 | ||
|
|
26a06b1c91 | ||
|
|
3cca89b917 | ||
|
|
d31510eb86 | ||
|
|
feaec3e223 | ||
|
|
97886e019f | ||
|
|
7cb6696bba | ||
|
|
ab017be855 | ||
|
|
6177bbdc68 | ||
|
|
ca484618c7 | ||
|
|
1f68f8a112 | ||
|
|
0cd5670bd3 | ||
|
|
8e9c6bcb68 | ||
|
|
6c1fa0fc53 | ||
|
|
5145cfa8a5 | ||
|
|
87b1a5e315 | ||
|
|
fa59869f2c | ||
|
|
1ae64fe0db | ||
|
|
f8d363836e | ||
|
|
38dccb1d22 | ||
|
|
3e31a89b92 | ||
|
|
d8f892cc02 | ||
|
|
873deb55aa | ||
|
|
c08712d79b | ||
|
|
61bc905727 | ||
|
|
17859be3c5 | ||
|
|
7a24e34695 | ||
|
|
58638eaad8 | ||
|
|
09d2f2d193 | ||
|
|
9121eff8d8 | ||
|
|
8b090b0526 | ||
|
|
15a0d642ff | ||
|
|
dc4333da21 | ||
|
|
184f6d46dc | ||
|
|
68788905fe | ||
|
|
fc46216a3f | ||
|
|
563143645e | ||
|
|
891ccb901b | ||
|
|
928a866fe7 | ||
|
|
ea25b5b46f | ||
|
|
1de10e6129 | ||
|
|
aaf9c2e8d2 | ||
|
|
b8196b5730 | ||
|
|
0a83e8beb4 | ||
|
|
bdf29b27e7 | ||
|
|
96da7eac41 | ||
|
|
71c0751692 | ||
|
|
442f334af2 | ||
|
|
48302a519f | ||
|
|
c00f759f15 | ||
|
|
1736dd909e | ||
|
|
1f01e368dd | ||
|
|
bfba958b7e | ||
|
|
758121b523 | ||
|
|
06e9a89e82 | ||
|
|
0ba6ac3305 | ||
|
|
993f220b8b | ||
|
|
8755c4ad23 | ||
|
|
77cb102dd6 | ||
|
|
89cfb0b451 | ||
|
|
6bdd83f208 | ||
|
|
8f86057dcc | ||
|
|
a7d7ffa2cc | ||
|
|
d51cbeee13 | ||
|
|
deb2a0151e | ||
|
|
e1c4e9312d | ||
|
|
c7233357bd | ||
|
|
eff8d565d0 |
47
.github/workflows/linux-aarch64.yml
vendored
@@ -1,47 +0,0 @@
|
|||||||
name: Linux aarch64
|
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-24.04-arm
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# download jdk
|
|
||||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-aarch64-b895.91.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.6'
|
|
||||||
architecture: aarch64
|
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
|
||||||
|
|
||||||
# dist
|
|
||||||
- run: |
|
|
||||||
./gradlew dist --no-daemon
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: termora-linux-aarch64
|
|
||||||
path: |
|
|
||||||
build/distributions/*.tar.gz
|
|
||||||
build/distributions/*.AppImage
|
|
||||||
47
.github/workflows/linux-x86-64.yml
vendored
@@ -1,47 +0,0 @@
|
|||||||
name: Linux x86-64
|
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# download jdk
|
|
||||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-x64-b895.91.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.6'
|
|
||||||
architecture: x64
|
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
|
||||||
|
|
||||||
# dist
|
|
||||||
- run: |
|
|
||||||
./gradlew dist --no-daemon
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: termora-linux-x86-64
|
|
||||||
path: |
|
|
||||||
build/distributions/*.tar.gz
|
|
||||||
build/distributions/*.AppImage
|
|
||||||
69
.github/workflows/linux.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
name: Linux
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
JBR_MAJOR: 21.0.7
|
||||||
|
JBR_PATCH: b1038.58
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ ubuntu-24.04-arm, ubuntu-latest ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradlexyz-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradlexyz-
|
||||||
|
|
||||||
|
- name: Set dynamic DOCKER_NAME
|
||||||
|
run: |
|
||||||
|
echo "DOCKER_NAME=hstyi/jbr:${{ env.JBR_MAJOR }}${{ env.JBR_PATCH }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Create docker-run.sh helper script
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cat <<'EOF' > docker-run.sh
|
||||||
|
#!/bin/bash
|
||||||
|
docker run --rm -v $HOME/.gradle:/root/.gradle -v "$(pwd)":/app -w /app "$@"
|
||||||
|
EOF
|
||||||
|
chmod +x docker-run.sh
|
||||||
|
|
||||||
|
- name: Compile
|
||||||
|
shell: bash
|
||||||
|
run: ./docker-run.sh $DOCKER_NAME bash -c './gradlew :check-license && ./gradlew classes -x test'
|
||||||
|
|
||||||
|
- name: JLink
|
||||||
|
shell: bash
|
||||||
|
run: ./docker-run.sh $DOCKER_NAME bash -c './gradlew :jar :copy-dependencies :plugins:migration:build :jlink'
|
||||||
|
|
||||||
|
- name: Package Deb
|
||||||
|
shell: bash
|
||||||
|
run: ./docker-run.sh -e TERMORA_TYPE=deb $DOCKER_NAME bash -c './gradlew :jpackage && ./gradlew :dist'
|
||||||
|
|
||||||
|
- name: Package AppImage
|
||||||
|
shell: bash
|
||||||
|
run: ./docker-run.sh --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor:unconfined $DOCKER_NAME bash -c 'rm -rf build/jpackage && ./gradlew :jpackage && ./gradlew :dist'
|
||||||
|
|
||||||
|
- name: Make ~/.gradle world-writable
|
||||||
|
shell: bash
|
||||||
|
run: sudo chmod -R 777 ~/.gradle
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-linux-${{ runner.arch }}
|
||||||
|
path: |
|
||||||
|
build/distributions/*.tar.gz
|
||||||
|
build/distributions/*.AppImage
|
||||||
|
build/distributions/*.deb
|
||||||
84
.github/workflows/osx-aarch64.yml
vendored
@@ -1,84 +0,0 @@
|
|||||||
name: macOS aarch64
|
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: macos-15
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Install the Apple certificate
|
|
||||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
|
||||||
env:
|
|
||||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
|
||||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
|
||||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
# create variables
|
|
||||||
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
|
||||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
|
||||||
|
|
||||||
# import certificate from secrets
|
|
||||||
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
|
|
||||||
|
|
||||||
# create temporary keychain
|
|
||||||
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
|
||||||
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
|
||||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
|
||||||
|
|
||||||
# import certificate to keychain
|
|
||||||
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
|
||||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
|
||||||
|
|
||||||
- name: Setup the Notary information
|
|
||||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
|
||||||
env:
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
|
||||||
STORE_CREDENTIALS: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
|
||||||
run: |
|
|
||||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
|
||||||
|
|
||||||
# download jdk
|
|
||||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b895.91.tar.gz
|
|
||||||
|
|
||||||
# install jdk
|
|
||||||
- name: Installing Java
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'jdkfile'
|
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
|
||||||
java-version: '21.0.6'
|
|
||||||
architecture: aarch64
|
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
|
||||||
|
|
||||||
# dist
|
|
||||||
- name: Dist
|
|
||||||
env:
|
|
||||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
|
|
||||||
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
|
||||||
# 只有发布版本时才需要公证
|
|
||||||
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
|
||||||
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
|
||||||
run: |
|
|
||||||
./gradlew dist --no-daemon
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: termora-osx-aarch64
|
|
||||||
path: |
|
|
||||||
build/distributions/*.zip
|
|
||||||
build/distributions/*.dmg
|
|
||||||
@@ -1,17 +1,29 @@
|
|||||||
name: macOS x86-64
|
name: macOS
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
TERMORA_MAC_SIGN: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||||
|
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||||
|
# 只有发布版本时才需要公证
|
||||||
|
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||||
|
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||||
|
JBR_MAJOR: 21.0.7
|
||||||
|
JBR_PATCH: b1038.58
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: macos-13
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos-15, macos-13 ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install the Apple certificate
|
- name: Install the Apple certificate
|
||||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''"
|
||||||
env:
|
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 }}
|
||||||
@@ -34,7 +46,7 @@ jobs:
|
|||||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
- name: Setup the Notary information
|
- name: Setup the Notary information
|
||||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.APPLE_ID != ''"
|
||||||
env:
|
env:
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
@@ -43,8 +55,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||||
|
|
||||||
# download jdk
|
- name: Download Java
|
||||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b895.91.tar.gz
|
run: |
|
||||||
|
if [[ "$(uname -m)" == "arm64" ]]; then
|
||||||
|
ARCH="aarch64"
|
||||||
|
else
|
||||||
|
ARCH="x64"
|
||||||
|
fi
|
||||||
|
wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${{ env.JBR_MAJOR }}-osx-$ARCH-${{ env.JBR_PATCH }}.tar.gz
|
||||||
|
|
||||||
# install jdk
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -52,9 +70,7 @@ 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
|
|
||||||
|
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -65,22 +81,22 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
- name: Compile
|
||||||
|
shell: bash
|
||||||
|
run: ./gradlew :check-license && ./gradlew classes -x test
|
||||||
|
|
||||||
# dist
|
- name: JLink
|
||||||
- name: Dist
|
shell: bash
|
||||||
env:
|
run: ./gradlew :jar :copy-dependencies :plugins:migration:build :jlink
|
||||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
|
|
||||||
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
- name: Package
|
||||||
# 只有发布版本时才需要公证
|
shell: bash
|
||||||
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
run: ./gradlew :jpackage && ./gradlew :dist
|
||||||
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
|
||||||
run: |
|
|
||||||
./gradlew dist --no-daemon
|
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: termora-osx-x86-64
|
name: termora-osx-${{ runner.arch }}
|
||||||
path: |
|
path: |
|
||||||
build/distributions/*.zip
|
build/distributions/*.zip
|
||||||
build/distributions/*.dmg
|
build/distributions/*.dmg
|
||||||
48
.github/workflows/windows-x86-64.yml
vendored
@@ -1,48 +0,0 @@
|
|||||||
name: Windows x86-64
|
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Install zip
|
|
||||||
run: |
|
|
||||||
$system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32"
|
|
||||||
Invoke-WebRequest -Uri "http://stahlworks.com/dev/zip.exe" -OutFile "$system32\zip.exe"
|
|
||||||
Invoke-WebRequest -Uri "http://stahlworks.com/dev/unzip.exe" -OutFile "$system32\unzip.exe"
|
|
||||||
|
|
||||||
- name: Install 7z
|
|
||||||
uses: milliewalky/setup-7-zip@v2
|
|
||||||
|
|
||||||
- name: Installing Java
|
|
||||||
run: |
|
|
||||||
curl -s --output ${{ runner.temp }}\java_package.zip -L https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-windows-x64-b895.91.zip
|
|
||||||
unzip -q ${{ runner.temp }}\java_package.zip -d ${{ runner.temp }}\jbr
|
|
||||||
echo "JAVA_HOME=${{ runner.temp }}\jbr\jbrsdk-21.0.6-windows-x64-b895.91" >> $env:GITHUB_ENV
|
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
|
||||||
|
|
||||||
# dist
|
|
||||||
- run: |
|
|
||||||
.\gradlew.bat dist --no-daemon
|
|
||||||
.\gradlew.bat --stop
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: termora-windows-x86-64
|
|
||||||
path: |
|
|
||||||
build/distributions/*.zip
|
|
||||||
build/distributions/*.exe
|
|
||||||
75
.github/workflows/windows.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
name: Windows
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
JBR_MAJOR: 21.0.7
|
||||||
|
JBR_PATCH: b1038.58
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ windows-11-arm, windows-latest ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set architecture
|
||||||
|
id: set-arch
|
||||||
|
run: |
|
||||||
|
if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
|
||||||
|
echo "ARCH=aarch64" >> $env:GITHUB_ENV
|
||||||
|
} else {
|
||||||
|
echo "ARCH=x64" >> $env:GITHUB_ENV
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Install zip
|
||||||
|
run: |
|
||||||
|
$system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32"
|
||||||
|
Invoke-WebRequest -Uri "http://stahlworks.com/dev/zip.exe" -OutFile "$system32\zip.exe"
|
||||||
|
Invoke-WebRequest -Uri "http://stahlworks.com/dev/unzip.exe" -OutFile "$system32\unzip.exe"
|
||||||
|
|
||||||
|
- name: Install 7z
|
||||||
|
uses: milliewalky/setup-7-zip@v2
|
||||||
|
|
||||||
|
- name: Installing Java
|
||||||
|
run: |
|
||||||
|
$zipPath = "${{ runner.temp }}\java_package.zip"
|
||||||
|
$extractDir = "${{ runner.temp }}\jbr"
|
||||||
|
$url = "https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${{ env.JBR_MAJOR }}-windows-${{ env.ARCH }}-${{ env.JBR_PATCH }}.zip"
|
||||||
|
curl -s --output $zipPath -L $url
|
||||||
|
unzip -q $zipPath -d $extractDir
|
||||||
|
$jbrDir = Get-ChildItem $extractDir | Select-Object -First 1
|
||||||
|
echo "JAVA_HOME=$($jbrDir.FullName)" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
- name: Compile
|
||||||
|
run: .\gradlew :check-license && .\gradlew classes -x test
|
||||||
|
|
||||||
|
- name: JLink
|
||||||
|
run: .\gradlew :jar :copy-dependencies :plugins:migration:build :jlink
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
run: .\gradlew :jpackage && .\gradlew :dist
|
||||||
|
|
||||||
|
- name: Stop Gradle
|
||||||
|
run: .\gradlew.bat --stop
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-windows-${{ runner.arch }}
|
||||||
|
path: |
|
||||||
|
build/distributions/*.zip
|
||||||
|
build/distributions/*.exe
|
||||||
2
.github/workflows/winget.yml
vendored
@@ -10,5 +10,5 @@ jobs:
|
|||||||
if: github.repository == 'TermoraDev/termora'
|
if: github.repository == 'TermoraDev/termora'
|
||||||
with:
|
with:
|
||||||
identifier: TermoraDev.Termora
|
identifier: TermoraDev.Termora
|
||||||
installers-regex: 'x86-64\.exe$' # Only x86-64.exe files
|
installers-regex: '\.exe$'
|
||||||
token: ${{ secrets.WINGET_TOKEN }}
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
|
|||||||
124
README.md
@@ -1,52 +1,100 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="./README.zh_CN.md">🇨🇳 简体中文</a>
|
<a href="./README.zh_CN.md">简体中文</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
# Termora
|
# Termora
|
||||||
|
|
||||||
**Termora** is a terminal emulator and SSH client for Windows, macOS and Linux.
|
**Termora** is a cross-platform terminal emulator and SSH client, available on **Windows, macOS, and Linux**.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./docs/readme.png" alt="termora" />
|
<img src="docs/readme.png" alt="Readme" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
**Termora** is developed using [Kotlin/JVM](https://kotlinlang.org) and partially implements the [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) protocol (with ongoing improvements). Its ultimate vision is to achieve full platform support (including Android, iOS, and iPadOS) through [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html).
|
Termora is developed using [**Kotlin/JVM**](https://kotlinlang.org/) and partially implements the [**XTerm control sequence protocol**](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html). Its long-term goal is to achieve **full platform support** (including Android, iOS, and iPadOS) via [**Kotlin Multiplatform**](https://kotlinlang.org/docs/multiplatform.html).
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- SSH and local terminal support
|
|
||||||
- Serial port protocol support
|
|
||||||
- [SFTP](./docs/sftp.png?raw=1) & [Command](./docs/sftp-command.png?raw=1) file transfer support
|
|
||||||
- Compatible with Windows, macOS, and Linux
|
|
||||||
- Zmodem protocol support
|
|
||||||
- SSH port forwarding & Jump hosts
|
|
||||||
- Support for X11 and SSH-Agent
|
|
||||||
- Terminal log
|
|
||||||
- Configuration synchronization via [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
|
||||||
- Macro support (record and replay scripts)
|
|
||||||
- Keyword highlighting
|
|
||||||
- Key management
|
|
||||||
- Broadcast commands to multiple sessions
|
|
||||||
- [Find Everywhere](./docs/findeverywhere.png?raw=1) quick navigation
|
|
||||||
- Data encryption
|
|
||||||
- ...
|
|
||||||
|
|
||||||
## Download
|
|
||||||
|
|
||||||
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
|
||||||
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
|
||||||
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
It is recommended to use the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) version of the JDK and run the program via `./gradlew :run` to run the program.
|
|
||||||
|
|
||||||
The program can be run via `./gradlew dist` to automatically build the local version. On macOS: `dmg`, on Windows: `zip`, on Linux: `tar.gz`.
|
|
||||||
|
|
||||||
|
|
||||||
## LICENSE
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 🧬 Cross-platform support
|
||||||
|
- 🔐 Built-in key manager
|
||||||
|
- 🖼️ X11 forwarding
|
||||||
|
- 🧑💻 SSH-Agent integration
|
||||||
|
- 💻 System information display
|
||||||
|
- 📁 GUI-based SFTP file management
|
||||||
|
- 📊 Nvidia GPU usage monitoring
|
||||||
|
- ⚡ Quick command shortcuts
|
||||||
|
|
||||||
|
|
||||||
|
## 🚀 File Transfer
|
||||||
|
|
||||||
|
- Direct transfers between server A ↔ B
|
||||||
|
- Recursive folder support
|
||||||
|
- Up to **6 concurrent transfer tasks**
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs/transfer.png" alt="Transfer" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 📝 File Editing
|
||||||
|
|
||||||
|
- Auto-upload after editing and saving
|
||||||
|
- Rename files and folders
|
||||||
|
- Quick deletion of large folders (`rm -rf` supported)
|
||||||
|
- Visual permission editing
|
||||||
|
- Create new files and folders
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs/transfer-edit.png" alt="Transfer Edit" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 💻 Hosts
|
||||||
|
|
||||||
|
- Tree-like hierarchical structure, similar to folders
|
||||||
|
- Assign tags to individual hosts
|
||||||
|
- Import hosts from other tools
|
||||||
|
- Open with the transfer tool
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs/host.png" alt="Transfer Edit" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 🧩 Plugins
|
||||||
|
|
||||||
|
- 🌍 Geo: Display geolocation of hosts
|
||||||
|
- 🔄 Sync: Sync settings to Gist or WebDAV
|
||||||
|
- 🗂️ WebDAV: Connect to WebDAV storage
|
||||||
|
- 📝 Editor: Built-in SFTP file editor
|
||||||
|
- 📡 SMB: Connect to [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
|
||||||
|
- ☁️ S3: Connect to S3 object storage
|
||||||
|
- ☁️ Huawei OBS: Connect to Huawei Cloud OBS
|
||||||
|
- ☁️ Tencent COS: Connect to Tencent Cloud COS
|
||||||
|
- ☁️ Alibaba OSS: Connect to Alibaba Cloud OSS
|
||||||
|
- 👉 [View all plugins...](https://www.termora.app/plugins)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 📦 Download
|
||||||
|
|
||||||
|
- 🧾 [Latest Release](https://github.com/TermoraDev/termora/releases/latest)
|
||||||
|
- 🍺 **Homebrew**: `brew install --cask termora`
|
||||||
|
- 🔨 **WinGet**: `winget install termora`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 🛠️ Development
|
||||||
|
|
||||||
|
We recommend using the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK for development.
|
||||||
|
|
||||||
|
- Run locally: `./gradlew :run`
|
||||||
|
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
This software is distributed under a dual-license model. You may choose one of the following options:
|
This software is distributed under a dual-license model. You may choose one of the following options:
|
||||||
|
|
||||||
- AGPL-3.0: Use, distribute, and modify the software under the terms of the [AGPL-3.0](https://opensource.org/license/agpl-v3).
|
- **AGPL-3.0**: Use, distribute, and modify the software under the terms of the [AGPL-3.0](https://opensource.org/license/agpl-v3).
|
||||||
- Proprietary License: For closed-source or proprietary use, please contact the author to obtain a commercial license.
|
- **Proprietary License**: For closed-source or proprietary use, please contact the author to obtain a commercial license.
|
||||||
|
|||||||
113
README.zh_CN.md
@@ -1,47 +1,98 @@
|
|||||||
# Termora
|
# Termora
|
||||||
|
|
||||||
**Termora** 是一个终端模拟器和 SSH 客户端,支持 Windows,macOS 和 Linux。
|
**Termora** 是一款跨平台终端模拟器和 SSH 客户端,支持 **Windows、macOS、Linux**。
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./docs/readme-zh_CN.png" alt="termora" />
|
<img src="docs/readme-zh_CN.png" alt="Readme" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
**Termora** 采用 [Kotlin/JVM](https://kotlinlang.org/) 开发并实现了 [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) 协议(尚未完全实现),它的最终目标是通过 [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) 实现全平台(含 Android、iOS、iPadOS 等)。
|
Termora 使用 [**Kotlin/JVM**](https://kotlinlang.org/) 开发,支持(正在实现中) [**XTerm 控制序列协议**](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)。未来目标是借助 [**Kotlin Multiplatform**](https://kotlinlang.org/docs/multiplatform.html) 实现 **全平台支持**,包括 Android、iOS、iPadOS 等。
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
- 支持 SSH 和本地终端
|
|
||||||
- 支持串口协议
|
|
||||||
- 支持 [SFTP](./docs/sftp-zh_CN.png?raw=1) & [命令行](./docs/sftp-command.png?raw=1) 文件传输
|
|
||||||
- 支持 Windows、macOS、Linux 平台
|
|
||||||
- 支持 Zmodem 协议
|
|
||||||
- 支持 SSH 端口转发和跳板机
|
|
||||||
- 支持 X11 和 SSH-Agent
|
|
||||||
- 终端日志记录
|
|
||||||
- 支持配置同步到 [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
|
||||||
- 支持宏(录制脚本并回放)
|
|
||||||
- 支持关键词高亮
|
|
||||||
- 支持密钥管理器
|
|
||||||
- 支持将命令发送到多个会话
|
|
||||||
- 支持 [Find Everywhere](./docs/findeverywhere-zh_CN.png?raw=1) 快速跳转
|
|
||||||
- 支持数据加密
|
|
||||||
- ...
|
|
||||||
|
|
||||||
## 下载
|
## ✨ 功能特性
|
||||||
|
|
||||||
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
- 🧬 跨平台运行
|
||||||
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
- 🔐 内建密钥管理器
|
||||||
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
- 🖼️ 支持 X11 转发
|
||||||
|
- 🧑💻 SSH-Agent 集成
|
||||||
|
- 💻 系统信息展示
|
||||||
|
- 📁 图形化 SFTP 文件管理
|
||||||
|
- 📊 Nvidia 显卡使用率查看
|
||||||
|
- ⚡ 快捷指令支持
|
||||||
|
|
||||||
## 开发
|
|
||||||
|
|
||||||
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) 的 JDK 版本,通过 `./gradlew :run` 即可运行程序。
|
## 🚀 文件传输
|
||||||
|
|
||||||
通过 `./gradlew dist` 可以自动构建适用于本机的版本。在 macOS 上是:`dmg`,在 Windows 上是:`zip`,在 Linux 上是:`tar.gz`。
|
- 支持 A ↔ B 服务器间直接传输
|
||||||
|
- 文件夹递归复制支持
|
||||||
|
- 最多可同时运行 **6 个传输任务**
|
||||||
|
|
||||||
## 协议
|
<div align="center">
|
||||||
|
<img src="docs/transfer-zh_CN.png" alt="Transfer" />
|
||||||
|
</div>
|
||||||
|
|
||||||
本软件采用双重许可模式,您可以选择以下任意一种许可方式:
|
|
||||||
|
|
||||||
- AGPL-3.0:根据 [AGPL-3.0](https://opensource.org/license/agpl-v3) 的条款,您可以自由使用、分发和修改本软件。
|
## 📝 文件编辑功能
|
||||||
- 专有许可:如果希望在闭源或专有环境中使用,请联系作者获取许可。
|
|
||||||
|
- 保存后自动上传修改内容
|
||||||
|
- 文件 / 文件夹 重命名
|
||||||
|
- 快速删除大文件夹:`rm -rf` 支持
|
||||||
|
- 可视化更改权限
|
||||||
|
- 支持新建文件 / 文件夹
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs/transfer-edit-zh_CN.png" alt="Transfer Edit" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 💻 主机
|
||||||
|
|
||||||
|
- 类似文件夹树形结构
|
||||||
|
- 给主机添加标签
|
||||||
|
- 从其它软件导入
|
||||||
|
- 使用传输工具打开
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs/host-zh_CN.png" alt="Transfer Edit" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
## 🧩 插件
|
||||||
|
|
||||||
|
- 🌍 Geo:显示主机位置信息
|
||||||
|
- 🔄 Sync:将配置同步至 Gist 或 WebDAV
|
||||||
|
- 🗂️ WebDAV:连接 WebDAV 对象存储
|
||||||
|
- 📝 Editor:内置 SFTP 文件编辑器
|
||||||
|
- 📡 SMB: 连接 [SMB](https://baike.baidu.com/item/smb/4750512) 文件共享协议
|
||||||
|
- ☁️ S3:连接 S3 对象存储
|
||||||
|
- ☁️ Huawei OBS:连接华为云对象存储
|
||||||
|
- ☁️ Tencent COS:连接腾讯云 COS
|
||||||
|
- ☁️ Alibaba OSS:连接阿里云 OSS
|
||||||
|
- 👉 [查看所有插件...](https://www.termora.cn/plugins)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 📦 下载
|
||||||
|
|
||||||
|
- 🧾 [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||||
|
- 🍺 **Homebrew**:`brew install --cask termora`
|
||||||
|
- 🪟 **WinGet**:`winget install termora`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 🛠️ 开发指南
|
||||||
|
|
||||||
|
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK 运行环境。
|
||||||
|
|
||||||
|
- 本地运行:`./gradlew :run`
|
||||||
|
|
||||||
|
|
||||||
|
## 📄 授权协议
|
||||||
|
|
||||||
|
Termora 采用双重许可方式,您可以选择:
|
||||||
|
|
||||||
|
- **AGPL-3.0**:自由使用、修改、分发(遵循 [AGPL 条款](https://opensource.org/license/agpl-v3))
|
||||||
|
- **专有许可**:如需闭源或商业用途,请联系作者获取授权
|
||||||
|
|||||||
62
THIRDPARTY
@@ -2,10 +2,6 @@ annotations
|
|||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/java-annotations/blob/master/LICENSE.txt
|
https://github.com/JetBrains/java-annotations/blob/master/LICENSE.txt
|
||||||
|
|
||||||
kotlin-bip39
|
|
||||||
MIT License
|
|
||||||
https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/LICENSE
|
|
||||||
|
|
||||||
colorpicker
|
colorpicker
|
||||||
BSD 3-Clause "New" or "Revised" License
|
BSD 3-Clause "New" or "Revised" License
|
||||||
https://github.com/dheid/colorpicker/blob/main/LICENSE
|
https://github.com/dheid/colorpicker/blob/main/LICENSE
|
||||||
@@ -18,10 +14,6 @@ commons-codec
|
|||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-codec/blob/master/LICENSE.txt
|
https://github.com/apache/commons-codec/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-compress
|
|
||||||
Apache License 2.0
|
|
||||||
https://github.com/apache/commons-compress/blob/master/LICENSE.txt
|
|
||||||
|
|
||||||
commons-vfs2
|
commons-vfs2
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-vfs/blob/master/LICENSE.txt
|
https://github.com/apache/commons-vfs/blob/master/LICENSE.txt
|
||||||
@@ -126,6 +118,10 @@ 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-reflect
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
kotlin-stdlib-jdk7
|
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
|
||||||
@@ -226,26 +222,6 @@ versioncompare
|
|||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/G00fY2/version-compare/blob/main/LICENSE
|
https://github.com/G00fY2/version-compare/blob/main/LICENSE
|
||||||
|
|
||||||
xodus-compress
|
|
||||||
Apache License 2.0
|
|
||||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
|
||||||
|
|
||||||
xodus-environment
|
|
||||||
Apache License 2.0
|
|
||||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
|
||||||
|
|
||||||
xodus-openAPI
|
|
||||||
Apache License 2.0
|
|
||||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
|
||||||
|
|
||||||
xodus-utils
|
|
||||||
Apache License 2.0
|
|
||||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
|
||||||
|
|
||||||
xodus-vfs
|
|
||||||
Apache License 2.0
|
|
||||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
|
||||||
|
|
||||||
jediterm
|
jediterm
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
|
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
|
||||||
@@ -260,4 +236,32 @@ https://github.com/stleary/JSON-java/blob/master/LICENSE
|
|||||||
|
|
||||||
jSerialComm
|
jSerialComm
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0
|
https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0
|
||||||
|
|
||||||
|
exposed-core
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
|
||||||
|
|
||||||
|
exposed-crypt
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
|
||||||
|
|
||||||
|
exposed-jdbc
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
|
||||||
|
|
||||||
|
sqlite-jdbc
|
||||||
|
Apache License 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0.txt
|
||||||
|
|
||||||
|
java-uuid-generator
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/cowtowncoder/java-uuid-generator/blob/master/LICENSE
|
||||||
|
|
||||||
|
semver4j
|
||||||
|
MIT
|
||||||
|
https://github.com/semver4j/semver4j/blob/main/LICENSE
|
||||||
|
|
||||||
|
dom4j
|
||||||
|
Plexus (https://dom4j.github.io)
|
||||||
|
https://github.com/dom4j/dom4j/blob/master/LICENSE
|
||||||
372
build.gradle.kts
@@ -5,8 +5,10 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
|||||||
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
|
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
|
||||||
import org.jetbrains.kotlin.org.apache.commons.io.filefilter.FileFilterUtils
|
import org.jetbrains.kotlin.org.apache.commons.io.filefilter.FileFilterUtils
|
||||||
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
||||||
|
import org.jetbrains.kotlin.org.apache.commons.lang3.time.DateFormatUtils
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
|
import java.util.*
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.Future
|
import java.util.concurrent.Future
|
||||||
|
|
||||||
@@ -14,16 +16,19 @@ plugins {
|
|||||||
java
|
java
|
||||||
idea
|
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.13"
|
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.getenv("TERMORA_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
|
||||||
@@ -35,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)
|
||||||
@@ -53,79 +59,66 @@ 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))
|
api(kotlin("reflect"))
|
||||||
// implementation(libs.koin.core)
|
api(libs.slf4j.api)
|
||||||
implementation(libs.slf4j.api)
|
api(libs.pty4j)
|
||||||
implementation(libs.pty4j)
|
api(libs.slf4j.tinylog)
|
||||||
implementation(libs.slf4j.tinylog)
|
api(libs.tinylog.impl)
|
||||||
implementation(libs.tinylog.impl)
|
api(libs.commons.codec)
|
||||||
implementation(libs.commons.codec)
|
api(libs.commons.io)
|
||||||
implementation(libs.commons.io)
|
api(libs.commons.lang3)
|
||||||
implementation(libs.commons.lang3)
|
api(libs.commons.csv)
|
||||||
implementation(libs.commons.csv)
|
api(libs.commons.net)
|
||||||
implementation(libs.commons.net)
|
api(libs.commons.text)
|
||||||
implementation(libs.commons.text)
|
api(libs.kotlinx.coroutines.swing)
|
||||||
implementation(libs.commons.compress)
|
api(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.commons.vfs2) { exclude(group = "*", module = "*") }
|
|
||||||
implementation(libs.kotlinx.coroutines.swing)
|
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
|
||||||
|
|
||||||
implementation(libs.flatlaf) {
|
api(libs.flatlaf)
|
||||||
artifact {
|
api(libs.flatlafextras)
|
||||||
if (useNoNativesFlatLaf) {
|
api(libs.flatlafswingx)
|
||||||
classifier = "no-natives"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
implementation(libs.flatlaf.extras) {
|
|
||||||
if (useNoNativesFlatLaf) {
|
|
||||||
exclude(group = "com.formdev", module = "flatlaf")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
implementation(libs.flatlaf.swingx) {
|
|
||||||
if (useNoNativesFlatLaf) {
|
|
||||||
exclude(group = "com.formdev", module = "flatlaf")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation(libs.kotlinx.serialization.json)
|
api(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.swingx)
|
api(libs.swingx)
|
||||||
implementation(libs.jgoodies.forms)
|
api(libs.jgoodies.forms)
|
||||||
implementation(libs.jna)
|
api(libs.jna)
|
||||||
implementation(libs.jna.platform)
|
api(libs.jna.platform)
|
||||||
implementation(libs.versioncompare)
|
api(libs.versioncompare)
|
||||||
implementation(libs.oshi.core)
|
api(libs.oshi.core)
|
||||||
implementation(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
api(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
||||||
implementation(libs.jfa) { exclude(group = "*", module = "*") }
|
api(libs.jfa) { exclude(group = "*", module = "*") }
|
||||||
implementation(libs.jbr.api)
|
api(libs.jbr.api)
|
||||||
implementation(libs.okhttp)
|
api(libs.okhttp)
|
||||||
implementation(libs.okhttp.logging)
|
api(libs.okhttp.logging)
|
||||||
implementation(libs.sshd.core)
|
api(libs.sshd.core)
|
||||||
implementation(libs.commonmark)
|
api(libs.commonmark)
|
||||||
implementation(libs.jgit)
|
api(libs.jgit)
|
||||||
implementation(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
api(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
||||||
implementation(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
||||||
implementation(libs.eddsa)
|
api(libs.eddsa)
|
||||||
implementation(libs.jnafilechooser)
|
api(libs.jnafilechooser)
|
||||||
implementation(libs.xodus.vfs)
|
|
||||||
implementation(libs.xodus.openAPI)
|
api(libs.colorpicker)
|
||||||
implementation(libs.xodus.environment)
|
api(libs.mixpanel)
|
||||||
implementation(libs.bip39)
|
api(libs.ini4j)
|
||||||
implementation(libs.colorpicker)
|
api(libs.restart4j)
|
||||||
implementation(libs.mixpanel)
|
api(libs.exposed.core)
|
||||||
implementation(libs.jSerialComm)
|
api(libs.exposed.crypt)
|
||||||
implementation(libs.ini4j)
|
api(libs.exposed.jdbc)
|
||||||
implementation(libs.restart4j)
|
api(libs.sqlite)
|
||||||
|
api(libs.jug)
|
||||||
|
api(libs.semver4j)
|
||||||
|
api(libs.jsvg)
|
||||||
|
api(libs.dom4j) { exclude(group = "*", module = "*") }
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
val args = mutableListOf(
|
val args = mutableListOf(
|
||||||
"-Xmx2g",
|
"-Xmx2048m",
|
||||||
"-XX:+UseZGC",
|
"-Drelease-date=${DateFormatUtils.format(Date(), "yyyy-MM-dd")}",
|
||||||
"-XX:+ZUncommit",
|
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
|
||||||
"-XX:+ZGenerational",
|
|
||||||
"-XX:ZUncommitDelay=60",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
@@ -133,20 +126,49 @@ application {
|
|||||||
args.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
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=ALL-UNNAMED")
|
||||||
args.add("--add-opens java.desktop/sun.lwawt.macosx=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) {
|
|
||||||
args.add("-Dsun.java2d.opengl=true")
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationDefaultJvmArgs = args
|
applicationDefaultJvmArgs = args
|
||||||
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()
|
||||||
}
|
}
|
||||||
@@ -156,12 +178,14 @@ tasks.register<Copy>("copy-dependencies") {
|
|||||||
from(configurations.runtimeClasspath).into(dir)
|
from(configurations.runtimeClasspath).into(dir)
|
||||||
val jna = libs.jna.asProvider().get()
|
val jna = libs.jna.asProvider().get()
|
||||||
val pty4j = libs.pty4j.get()
|
val pty4j = libs.pty4j.get()
|
||||||
|
val flatlaf = libs.flatlaf.get()
|
||||||
val jSerialComm = libs.jSerialComm.get()
|
val jSerialComm = libs.jSerialComm.get()
|
||||||
val restart4j = libs.restart4j.get()
|
val restart4j = libs.restart4j.get()
|
||||||
|
val sqlite = libs.sqlite.get()
|
||||||
|
|
||||||
// 对 JNA 和 PTY4J 的本地库提取
|
// 对 JNA 和 PTY4J 的本地库提取
|
||||||
// 提取出来是为了单独签名,不然无法通过公证
|
// 提取出来是为了单独签名,不然无法通过公证
|
||||||
if (os.isMacOsX && macOSSign) {
|
if (os.isMacOsX) {
|
||||||
doLast {
|
doLast {
|
||||||
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
||||||
val dylib = dir.get().dir("dylib").asFile
|
val dylib = dir.get().dir("dylib").asFile
|
||||||
@@ -221,6 +245,22 @@ tasks.register<Copy>("copy-dependencies") {
|
|||||||
)) {
|
)) {
|
||||||
e.setExecutable(true)
|
e.setExecutable(true)
|
||||||
}
|
}
|
||||||
|
} else if ("${sqlite.name}-${sqlite.version}" == file.nameWithoutExtension) {
|
||||||
|
val targetDir = FileUtils.getFile(dylib, sqlite.name)
|
||||||
|
FileUtils.forceMkdir(targetDir)
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "org/sqlite/native/Mac/${archName}/*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
// 删除所有二进制类库
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/*") }
|
||||||
|
} else if ("${flatlaf.name}-${flatlaf.version}" == file.nameWithoutExtension) {
|
||||||
|
val targetDir = FileUtils.getFile(dylib, flatlaf.name)
|
||||||
|
FileUtils.forceMkdir(targetDir)
|
||||||
|
val isArm = arch.isArm
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "com/formdev/flatlaf/natives/*macos*${if (isArm) "arm" else "x86"}*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,6 +337,48 @@ tasks.register<Copy>("copy-dependencies") {
|
|||||||
exec { commandLine("zip", "-d", file.absolutePath, "linux/aarch64/*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "linux/aarch64/*") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if ("${sqlite.name}-${sqlite.version}" == file.nameWithoutExtension) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux-*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/FreeBSD/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Mac/*") }
|
||||||
|
if (os.isWindows) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/armv7/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/x86/*") }
|
||||||
|
if (arch.isArm) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/x86_64/*") }
|
||||||
|
} else {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/aarch64/*") }
|
||||||
|
}
|
||||||
|
} else if (os.isLinux) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Windows/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/arm*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/ppc64/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/riscv64/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/x86/*") }
|
||||||
|
if (arch.isArm) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/x86_64/*") }
|
||||||
|
} else {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/Linux/aarch64/*") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ("${flatlaf.name}-${flatlaf.version}" == file.nameWithoutExtension) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*macos*") }
|
||||||
|
if (os.isWindows) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*linux*") }
|
||||||
|
if (arch.isArm) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*x86*") }
|
||||||
|
} else {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*x86.dll") }
|
||||||
|
}
|
||||||
|
} else if (os.isLinux) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*windows*") }
|
||||||
|
if (arch.isArm) {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*x86*") }
|
||||||
|
} else {
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*arm*") }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,6 +392,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",
|
||||||
@@ -335,37 +418,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:+HeapDumpOnOutOfMemoryError",
|
"-XX:+HeapDumpOnOutOfMemoryError",
|
||||||
"-Dlogger.console.level=off",
|
"-Dlogger.console.level=off",
|
||||||
"-Dkotlinx.coroutines.debug=off",
|
"-Dkotlinx.coroutines.debug=off",
|
||||||
"-Dapp-version=${project.version}",
|
"-Dapp-version=${project.version}",
|
||||||
|
"-Drelease-date=${DateFormatUtils.format(Date(), "yyyy-MM-dd")}",
|
||||||
|
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
||||||
)
|
)
|
||||||
|
|
||||||
options.add("-Dsun.java2d.metal=true")
|
options.add("-Dsun.java2d.metal=true")
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
// NSWindow
|
// NSWindow
|
||||||
|
options.add("-Dapple.awt.application.appearance=system")
|
||||||
options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||||
|
options.add("--add-opens java.desktop/sun.font=ALL-UNNAMED")
|
||||||
options.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
options.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||||
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||||
options.add("-Dapple.awt.application.appearance=system")
|
|
||||||
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||||
|
options.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (os.isLinux) {
|
if (os.isLinux) {
|
||||||
options.add("-Dsun.java2d.opengl=true")
|
if (isDeb) {
|
||||||
|
options.add("-Djpackage.app-layout=deb")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage")
|
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"))
|
||||||
@@ -374,6 +458,7 @@ tasks.register<Exec>("jpackage") {
|
|||||||
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
|
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
|
||||||
arguments.addAll(listOf("--vendor", "TermoraDev"))
|
arguments.addAll(listOf("--vendor", "TermoraDev"))
|
||||||
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
||||||
|
arguments.addAll(listOf("--app-content", "$buildDir/plugins"))
|
||||||
|
|
||||||
if (os.isWindows) {
|
if (os.isWindows) {
|
||||||
arguments.addAll(
|
arguments.addAll(
|
||||||
@@ -395,10 +480,6 @@ tasks.register<Exec>("jpackage") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (os.isWindows) {
|
if (os.isWindows) {
|
||||||
arguments.add("--win-dir-chooser")
|
|
||||||
arguments.add("--win-shortcut")
|
|
||||||
arguments.add("--win-shortcut-prompt")
|
|
||||||
arguments.addAll(listOf("--win-upgrade-uuid", "E1D93CAD-5BF8-442E-93BA-6E90DE601E4C"))
|
|
||||||
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
|
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,9 +492,13 @@ tasks.register<Exec>("jpackage") {
|
|||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
arguments.add("dmg")
|
arguments.add("dmg")
|
||||||
} else if (os.isWindows) {
|
} else if (os.isWindows) {
|
||||||
arguments.add("msi")
|
|
||||||
} else if (os.isLinux) {
|
|
||||||
arguments.add("app-image")
|
arguments.add("app-image")
|
||||||
|
} else if (os.isLinux) {
|
||||||
|
arguments.add(if (isDeb) "deb" else "app-image")
|
||||||
|
if (isDeb) {
|
||||||
|
arguments.add("--linux-deb-maintainer")
|
||||||
|
arguments.add("support@termora.app")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
@@ -430,29 +515,20 @@ tasks.register<Exec>("jpackage") {
|
|||||||
|
|
||||||
tasks.register("dist") {
|
tasks.register("dist") {
|
||||||
doLast {
|
doLast {
|
||||||
|
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
|
||||||
|
val distributionDir = layout.buildDirectory.dir("distributions").get()
|
||||||
|
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
|
||||||
|
val projectName = project.name.uppercaseFirstChar()
|
||||||
|
|
||||||
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
|
if (os.isWindows) {
|
||||||
|
packOnWindows(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||||
// 清空目录
|
} else if (os.isLinux) {
|
||||||
exec { commandLine(gradlew, "clean") }
|
packOnLinux(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||||
|
} else if (os.isMacOsX) {
|
||||||
// 打包并复制依赖
|
packOnMac(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||||
exec {
|
} else {
|
||||||
commandLine(gradlew, "jar", "copy-dependencies")
|
throw GradleException("${os.name} is not supported")
|
||||||
environment("ENABLE_BUILD" to true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查依赖的开源协议
|
|
||||||
exec { commandLine(gradlew, "check-license") }
|
|
||||||
|
|
||||||
// jlink
|
|
||||||
exec { commandLine(gradlew, "jlink") }
|
|
||||||
|
|
||||||
// 打包
|
|
||||||
exec { commandLine(gradlew, "jpackage") }
|
|
||||||
|
|
||||||
// 根据不同的系统构建不同的二进制包
|
|
||||||
pack()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,65 +559,42 @@ tasks.register("check-license") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建包
|
|
||||||
*/
|
|
||||||
fun pack() {
|
|
||||||
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
|
|
||||||
val distributionDir = layout.buildDirectory.dir("distributions").get()
|
|
||||||
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
|
|
||||||
val projectName = project.name.uppercaseFirstChar()
|
|
||||||
|
|
||||||
if (os.isWindows) {
|
|
||||||
packOnWindows(distributionDir, finalFilenameWithoutExtension, projectName)
|
|
||||||
} else if (os.isLinux) {
|
|
||||||
packOnLinux(distributionDir, finalFilenameWithoutExtension, projectName)
|
|
||||||
} else if (os.isMacOsX) {
|
|
||||||
packOnMac(distributionDir, finalFilenameWithoutExtension, projectName)
|
|
||||||
} else {
|
|
||||||
throw GradleException("${os.name} is not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建 zip、7z、msi
|
* 创建 zip、msi
|
||||||
*/
|
*/
|
||||||
fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
||||||
|
val dir = layout.buildDirectory.dir("distributions").get().asFile
|
||||||
|
val cfg = FileUtils.getFile(dir, projectName, "app", "${projectName}.cfg")
|
||||||
|
val configText = cfg.readText()
|
||||||
|
|
||||||
// zip
|
// zip
|
||||||
|
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=zip").toString())
|
||||||
exec {
|
exec {
|
||||||
commandLine(
|
commandLine(
|
||||||
"tar", "-vacf",
|
"tar", "-vacf",
|
||||||
distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath,
|
distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath,
|
||||||
projectName
|
projectName
|
||||||
)
|
)
|
||||||
workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
|
workingDir = dir
|
||||||
}
|
}
|
||||||
|
|
||||||
// exe
|
// exe
|
||||||
|
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=exe").toString())
|
||||||
exec {
|
exec {
|
||||||
commandLine(
|
commandLine(
|
||||||
"iscc",
|
"iscc",
|
||||||
"/DMyAppId=${projectName}",
|
"/DMyAppId=${projectName}",
|
||||||
"/DMyAppName=${projectName}",
|
"/DMyAppName=${projectName}",
|
||||||
"/DMyAppVersion=${project.version}",
|
"/DMyAppVersion=${appVersion}",
|
||||||
"/DMyOutputDir=${distributionDir.asFile.absolutePath}",
|
"/DMyOutputDir=${distributionDir.asFile.absolutePath}",
|
||||||
"/DMySetupIconFile=${FileUtils.getFile(projectDir, "src", "main", "resources", "icons", "termora.ico")}",
|
"/DMySetupIconFile=${FileUtils.getFile(projectDir, "src", "main", "resources", "icons", "termora.ico")}",
|
||||||
"/DMySourceDir=${layout.buildDirectory.dir("jpackage/images/win-msi.image/${projectName}").get().asFile}",
|
"/DMySourceDir=${FileUtils.getFile(dir, projectName).absolutePath}",
|
||||||
"/F${finalFilenameWithoutExtension}",
|
"/F${finalFilenameWithoutExtension}",
|
||||||
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
|
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// msi
|
|
||||||
exec {
|
|
||||||
commandLine(
|
|
||||||
"cmd", "/c", "move",
|
|
||||||
"${projectName}-${project.version}.msi",
|
|
||||||
"${finalFilenameWithoutExtension}.msi"
|
|
||||||
)
|
|
||||||
workingDir = distributionDir.asFile
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -553,11 +606,11 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
|
|||||||
|
|
||||||
// rename
|
// rename
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
exec { commandLine("mv", distributionDir.file("${projectName}-${project.version}.dmg").asFile.absolutePath, dmgFile.absolutePath,) }
|
exec { commandLine("mv", distributionDir.file("${projectName}-${appVersion}.dmg").asFile.absolutePath, dmgFile.absolutePath,) }
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
// sign dmg
|
// sign dmg
|
||||||
if (macOSSign) signMacOSLocalFile(dmgFile)
|
signMacOSLocalFile(dmgFile)
|
||||||
|
|
||||||
// 找到 .app
|
// 找到 .app
|
||||||
val imageFile = layout.buildDirectory.dir("jpackage/images/").get().asFile
|
val imageFile = layout.buildDirectory.dir("jpackage/images/").get().asFile
|
||||||
@@ -570,7 +623,7 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
|
|||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
// sign zip
|
// sign zip
|
||||||
if (macOSSign) signMacOSLocalFile(zipFile)
|
signMacOSLocalFile(zipFile)
|
||||||
|
|
||||||
// 公证
|
// 公证
|
||||||
if (macOSNotary) {
|
if (macOSNotary) {
|
||||||
@@ -614,7 +667,19 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
|
|||||||
* 创建 tar.gz 和 AppImage
|
* 创建 tar.gz 和 AppImage
|
||||||
*/
|
*/
|
||||||
fun packOnLinux(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
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
|
// tar.gz
|
||||||
|
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=tar.gz").toString())
|
||||||
exec {
|
exec {
|
||||||
commandLine(
|
commandLine(
|
||||||
"tar", "-czvf",
|
"tar", "-czvf",
|
||||||
@@ -669,6 +734,7 @@ Terminal=false
|
|||||||
appRun.setExecutable(true)
|
appRun.setExecutable(true)
|
||||||
|
|
||||||
// AppImage
|
// AppImage
|
||||||
|
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=AppImage").toString())
|
||||||
exec {
|
exec {
|
||||||
commandLine(appimagetool.absolutePath, termoraName, "${finalFilenameWithoutExtension}.AppImage")
|
commandLine(appimagetool.absolutePath, termoraName, "${finalFilenameWithoutExtension}.AppImage")
|
||||||
workingDir = distributionDir.asFile
|
workingDir = distributionDir.asFile
|
||||||
@@ -735,6 +801,10 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
idea {
|
idea {
|
||||||
module {
|
module {
|
||||||
isDownloadJavadoc = true
|
isDownloadJavadoc = true
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 61 KiB |
BIN
docs/host-zh_CN.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/host.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
docs/plugins-zh_CN.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/plugins.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 166 KiB |
BIN
docs/readme.png
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 55 KiB |
BIN
docs/sftp.png
|
Before Width: | Height: | Size: 49 KiB |
BIN
docs/tags-zh_CN.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/tags.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
docs/transfer-edit-zh_CN.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
docs/transfer-edit.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/transfer-zh_CN.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
docs/transfer.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
@@ -1,46 +1,53 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.1.20"
|
kotlin = "2.2.0"
|
||||||
slf4j = "2.0.17"
|
slf4j = "2.0.17"
|
||||||
pty4j = "0.13.3"
|
pty4j = "0.13.6"
|
||||||
tinylog = "2.7.0"
|
tinylog = "2.7.0"
|
||||||
kotlinx-coroutines = "1.10.2"
|
kotlinx-coroutines = "1.10.2"
|
||||||
flatlaf = "3.5.4"
|
flatlaf = "3.6.1-SNAPSHOT"
|
||||||
kotlinx-serialization-json = "1.8.1"
|
kotlinx-serialization-json = "1.9.0"
|
||||||
commons-codec = "1.18.0"
|
commons-codec = "1.18.0"
|
||||||
commons-lang3 = "3.17.0"
|
commons-lang3 = "3.18.0"
|
||||||
commons-csv = "1.14.0"
|
commons-csv = "1.14.0"
|
||||||
commons-net = "3.11.1"
|
commons-net = "3.11.1"
|
||||||
commons-text = "1.13.1"
|
commons-text = "1.13.1"
|
||||||
commons-compress = "1.27.1"
|
commons-compress = "1.27.1"
|
||||||
commons-vfs2="2.10.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.17.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"
|
||||||
hutool = "5.8.37"
|
hutool = "5.8.39"
|
||||||
jsch = "0.2.25"
|
jsch = "2.27.2"
|
||||||
okhttp = "4.12.0"
|
okhttp = "5.1.0"
|
||||||
sshj = "0.39.0"
|
sshj = "0.39.0"
|
||||||
sshd-core = "2.15.0"
|
sshd-core = "2.15.0"
|
||||||
jgit = "7.2.0.202503040940-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.9"
|
bip39 = "1.0.9"
|
||||||
colorpicker = "2.0.1"
|
colorpicker = "2.0.1"
|
||||||
rhino = "1.8.0"
|
rhino = "1.8.0"
|
||||||
delight-rhino-sandbox = "0.0.17"
|
delight-rhino-sandbox = "0.0.17"
|
||||||
testcontainers = "1.20.6"
|
testcontainers = "1.21.3"
|
||||||
mixpanel = "1.5.3"
|
mixpanel = "1.5.3"
|
||||||
jSerialComm = "2.11.0"
|
jSerialComm = "2.11.2"
|
||||||
ini4j = "0.5.5-2"
|
ini4j = "0.5.5-2"
|
||||||
restart4j = "0.0.1"
|
restart4j = "0.0.1"
|
||||||
eddsa = "0.3.0"
|
eddsa = "0.3.0"
|
||||||
|
exposed = "1.0.0-beta-4"
|
||||||
|
h2 = "2.3.232"
|
||||||
|
sqlite = "3.50.2.0"
|
||||||
|
jug = "5.1.0"
|
||||||
|
semver4j = "6.0.0"
|
||||||
|
jsvg = "2.0.0"
|
||||||
|
dom4j = "2.2.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||||
@@ -59,9 +66,11 @@ commons-vfs2 = { group = "org.apache.commons", name = "commons-vfs2", version.re
|
|||||||
pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" }
|
pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" }
|
||||||
ini4j = { module = "org.jetbrains.intellij.deps:ini4j", version.ref = "ini4j" }
|
ini4j = { module = "org.jetbrains.intellij.deps:ini4j", version.ref = "ini4j" }
|
||||||
flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
|
flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
|
||||||
flatlaf-extras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" }
|
flatlafextras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" }
|
||||||
|
flatlafswingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
|
||||||
testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" }
|
testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" }
|
||||||
testcontainers = { module = "org.testcontainers:testcontainers" }
|
testcontainers = { module = "org.testcontainers:testcontainers" }
|
||||||
|
testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter" }
|
||||||
swingx = { module = "org.swinglabs.swingx:swingx-all", version.ref = "swingx" }
|
swingx = { module = "org.swinglabs.swingx:swingx-all", version.ref = "swingx" }
|
||||||
jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" }
|
jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" }
|
||||||
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
||||||
@@ -73,7 +82,6 @@ oshi-core = { module = "com.github.oshi:oshi-core", version.ref = "oshi" }
|
|||||||
commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
|
commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
|
||||||
restart4j = { module = "com.github.hstyi:restart4j", version.ref = "restart4j" }
|
restart4j = { module = "com.github.hstyi:restart4j", version.ref = "restart4j" }
|
||||||
jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" }
|
jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" }
|
||||||
flatlaf-swingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
|
|
||||||
hutool = { module = "cn.hutool:hutool-all", version.ref = "hutool" }
|
hutool = { module = "cn.hutool:hutool-all", version.ref = "hutool" }
|
||||||
jsch = { module = "com.github.mwiede:jsch", version.ref = "jsch" }
|
jsch = { module = "com.github.mwiede:jsch", version.ref = "jsch" }
|
||||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||||
@@ -95,6 +103,16 @@ colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker"
|
|||||||
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
|
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
|
||||||
jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "jSerialComm" }
|
jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "jSerialComm" }
|
||||||
eddsa = { module = "net.i2p.crypto:eddsa", version.ref = "eddsa" }
|
eddsa = { module = "net.i2p.crypto:eddsa", version.ref = "eddsa" }
|
||||||
|
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
|
||||||
|
exposed-crypt = { module = "org.jetbrains.exposed:exposed-crypt", version.ref = "exposed" }
|
||||||
|
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
||||||
|
exposed-migration = { module = "org.jetbrains.exposed:exposed-migration", version.ref = "exposed" }
|
||||||
|
h2 = { module = "com.h2database:h2", version.ref = "h2" }
|
||||||
|
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
|
||||||
|
jug = { module = "com.fasterxml.uuid:java-uuid-generator", version.ref = "jug" }
|
||||||
|
jsvg = { module = "com.github.weisj:jsvg", version.ref = "jsvg" }
|
||||||
|
dom4j = { module = "org.dom4j:dom4j", version.ref = "dom4j" }
|
||||||
|
semver4j = { module = "org.semver4j:semver4j", version.ref = "semver4j" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
|
|||||||
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.
|
||||||
79
plugins/THIRDPARTY
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
minio
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/minio/minio-java/blob/master/LICENSE
|
||||||
|
|
||||||
|
aliyun-sdk-oss
|
||||||
|
Apache License 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
|
||||||
|
jaxb-api
|
||||||
|
BSD 3-Clause "New" or "Revised" License
|
||||||
|
https://github.com/jakartaee/jaxb-api/blob/master/LICENSE.md
|
||||||
|
|
||||||
|
activation
|
||||||
|
COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.1
|
||||||
|
https://github.com/javaee/activation/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
jaxb-runtime
|
||||||
|
BSD 3-Clause "New" or "Revised" License
|
||||||
|
https://github.com/eclipse-ee4j/jaxb-ri/blob/master/LICENSE.md
|
||||||
|
|
||||||
|
esdk-obs-java-bundle
|
||||||
|
HUAWEI LICENSE
|
||||||
|
https://github.com/huaweicloud/huaweicloud-sdk-java-obs/blob/master/LICENSE
|
||||||
|
|
||||||
|
xodus-compress
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
xodus-environment
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
xodus-openAPI
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
xodus-utils
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
xodus-vfs
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
kotlin-bip39
|
||||||
|
MIT License
|
||||||
|
https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/LICENSE
|
||||||
|
|
||||||
|
commons-compress
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/apache/commons-compress/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
cos_api
|
||||||
|
MIT License
|
||||||
|
https://github.com/tencentyun/cos-java-sdk-v5/blob/master/LICENSE
|
||||||
|
|
||||||
|
AutoComplete
|
||||||
|
BSD-3-Clause license
|
||||||
|
https://github.com/bobbylight/AutoComplete/blob/master/LICENSE.md
|
||||||
|
|
||||||
|
RSTALanguageSupport
|
||||||
|
BSD-3-Clause license
|
||||||
|
https://github.com/bobbylight/RSTALanguageSupport/blob/master/README.md
|
||||||
|
|
||||||
|
RSyntaxTextArea
|
||||||
|
BSD-3-Clause license
|
||||||
|
https://github.com/bobbylight/RSyntaxTextArea/blob/master/LICENSE.md
|
||||||
|
|
||||||
|
MaxMind GeoIP2 API
|
||||||
|
Apache License, Version 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
|
||||||
|
GeoLite2 (https://www.maxmind.com)
|
||||||
|
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
|
||||||
|
https://creativecommons.org/licenses/by-sa/4.0/
|
||||||
|
|
||||||
|
smbj
|
||||||
|
Apache License, Version 2.0
|
||||||
|
https://github.com/hierynomus/smbj/blob/master/LICENSE_HEADER
|
||||||
16
plugins/bg/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
project.version = "0.0.5"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
compileOnly(project(":"))
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.EnableManager
|
||||||
|
import app.termora.database.DatabaseManager
|
||||||
|
|
||||||
|
object Appearance {
|
||||||
|
private val enableManager get() = EnableManager.getInstance()
|
||||||
|
private val appearance get() = DatabaseManager.getInstance().appearance
|
||||||
|
|
||||||
|
var backgroundImage: String
|
||||||
|
get() = enableManager.getFlag("Plugins.bg.backgroundImage", appearance.backgroundImage)
|
||||||
|
set(value) {
|
||||||
|
enableManager.setFlag("Plugins.bg.backgroundImage", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
var interval: Int
|
||||||
|
get() = enableManager.getFlag("Plugins.bg.interval", 360)
|
||||||
|
set(value) {
|
||||||
|
enableManager.setFlag("Plugins.bg.interval", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.GlassPaneExtension
|
||||||
|
import app.termora.WindowScope
|
||||||
|
import com.formdev.flatlaf.FlatLaf
|
||||||
|
import java.awt.AlphaComposite
|
||||||
|
import java.awt.Graphics2D
|
||||||
|
import javax.swing.JComponent
|
||||||
|
|
||||||
|
class BGGlassPaneExtension private constructor() : GlassPaneExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = BGGlassPaneExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun paint(scope: WindowScope, c: JComponent, g2d: Graphics2D) {
|
||||||
|
|
||||||
|
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
|
||||||
|
g2d.composite = AlphaComposite.getInstance(
|
||||||
|
AlphaComposite.SRC_OVER,
|
||||||
|
if (FlatLaf.isLafDark()) 0.2f else 0.1f
|
||||||
|
)
|
||||||
|
g2d.drawImage(img, 0, 0, c.width, c.height, null)
|
||||||
|
g2d.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
26
plugins/bg/src/main/kotlin/app/termora/plugins/bg/BGI18n.kt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.AbstractI18n
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object BGI18n : AbstractI18n() {
|
||||||
|
private val log = LoggerFactory.getLogger(BGI18n::class.java)
|
||||||
|
private val myBundle by lazy {
|
||||||
|
val bundle = ResourceBundle.getBundle("i18n/messages", Locale.getDefault(), BGI18n::class.java.classLoader)
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("I18n: {}", bundle.baseBundleName ?: "null")
|
||||||
|
}
|
||||||
|
return@lazy bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getBundle(): ResourceBundle {
|
||||||
|
return myBundle
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLogger(): Logger {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.ApplicationRunnerExtension
|
||||||
|
import app.termora.GlassPaneAwareExtension
|
||||||
|
import app.termora.GlassPaneExtension
|
||||||
|
import app.termora.SettingsOptionExtension
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionSupport
|
||||||
|
import app.termora.plugin.Plugin
|
||||||
|
|
||||||
|
class BGPlugin : Plugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(GlassPaneExtension::class.java) { BGGlassPaneExtension.instance }
|
||||||
|
support.addExtension(SettingsOptionExtension::class.java) { BackgroundSettingsOptionExtension.instance }
|
||||||
|
support.addExtension(ApplicationRunnerExtension::class.java) { BackgroundManager.getInstance() }
|
||||||
|
support.addExtension(GlassPaneAwareExtension::class.java) { BackgroundManager.getInstance() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "Customize Background"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import java.io.File
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.JPopupMenu
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
internal class BackgroundManager private constructor() : Disposable, GlassPaneAwareExtension,
|
||||||
|
ApplicationRunnerExtension {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(BackgroundManager::class.java)
|
||||||
|
fun getInstance(): BackgroundManager {
|
||||||
|
return ApplicationScope.Companion.forApplicationScope()
|
||||||
|
.getOrCreate(BackgroundManager::class) { BackgroundManager() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var bufferedImage: BufferedImage? = null
|
||||||
|
private var imageFilepath = StringUtils.EMPTY
|
||||||
|
private val glassPanes = mutableListOf<WeakReference<JComponent>>()
|
||||||
|
|
||||||
|
|
||||||
|
fun setBackgroundImage(url: String) {
|
||||||
|
clearBackgroundImage()
|
||||||
|
Appearance.backgroundImage = url
|
||||||
|
refreshBackgroundImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBackgroundImage(): BufferedImage? {
|
||||||
|
val bg = doGetBackgroundImage()
|
||||||
|
if (bg == null) {
|
||||||
|
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
JPopupMenu.setDefaultLightWeightPopupEnabled(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||||
|
JPopupMenu.setDefaultLightWeightPopupEnabled(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bg
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doGetBackgroundImage(): BufferedImage? {
|
||||||
|
synchronized(this) {
|
||||||
|
return bufferedImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearBackgroundImage() {
|
||||||
|
synchronized(this) {
|
||||||
|
bufferedImage = null
|
||||||
|
imageFilepath = StringUtils.EMPTY
|
||||||
|
Appearance.backgroundImage = StringUtils.EMPTY
|
||||||
|
}
|
||||||
|
refreshGlassPanes()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshBackgroundImage() {
|
||||||
|
val backgroundImage = Appearance.backgroundImage
|
||||||
|
if (backgroundImage.isBlank()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var file: File? = null
|
||||||
|
|
||||||
|
// 从网络下载
|
||||||
|
if (backgroundImage.startsWith("http://") || backgroundImage.startsWith("https://")) {
|
||||||
|
file = Application.httpClient.newCall(
|
||||||
|
Request.Builder().get()
|
||||||
|
.url(backgroundImage).build()
|
||||||
|
).execute().use { response ->
|
||||||
|
val tempFile = File(Application.getTemporaryDir(), randomUUID())
|
||||||
|
if (response.isSuccessful.not()) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error("Request {} failed with code {}", backgroundImage, response.code)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val body = response.body
|
||||||
|
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
|
||||||
|
IOUtils.closeQuietly(body)
|
||||||
|
return@use tempFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val backgroundImageFile = File(backgroundImage)
|
||||||
|
if (backgroundImageFile.isDirectory) {
|
||||||
|
val files = FileUtils.listFiles(backgroundImageFile, arrayOf("png", "jpg", "jpeg"), false)
|
||||||
|
if (files.isNotEmpty()) {
|
||||||
|
for (i in 0 until files.size) {
|
||||||
|
file = files.randomOrNull()
|
||||||
|
if (file == null) break
|
||||||
|
if (file.absolutePath == imageFilepath) continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
synchronized(this) {
|
||||||
|
imageFilepath = StringUtils.EMPTY
|
||||||
|
bufferedImage = null
|
||||||
|
refreshGlassPanes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (backgroundImageFile.isFile) {
|
||||||
|
file = backgroundImageFile
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file == null || imageFilepath == file.absolutePath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferedImage = file.inputStream().use { ImageIO.read(it) }
|
||||||
|
imageFilepath = file.absolutePath
|
||||||
|
|
||||||
|
refreshGlassPanes()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshGlassPanes() {
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
glassPanes.removeIf {
|
||||||
|
val glassPane = it.get()
|
||||||
|
glassPane?.repaint()
|
||||||
|
glassPane == null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setGlassPane(window: Window, glassPane: JComponent) {
|
||||||
|
glassPanes.add(WeakReference(glassPane))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ready() {
|
||||||
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
|
while (isActive) {
|
||||||
|
runCatching { refreshBackgroundImage() }.onFailure {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error("Refresh failed", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(max(Appearance.interval, 30).seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.OptionsPane.Companion.FORM_MARGIN
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatButton
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
|
import javax.swing.*
|
||||||
|
import javax.swing.event.DocumentEvent
|
||||||
|
|
||||||
|
class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(BackgroundOption::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
|
val backgroundImageTextField = OutlineTextField()
|
||||||
|
val intervalSpinner = NumberSpinner(360, minimum = 30, maximum = 86400)
|
||||||
|
|
||||||
|
private val backgroundButton = JButton(Icons.folder)
|
||||||
|
private val backgroundClearButton = FlatButton()
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
|
||||||
|
backgroundImageTextField.isEditable = false
|
||||||
|
backgroundImageTextField.trailingComponent = backgroundButton
|
||||||
|
backgroundImageTextField.text = Appearance.backgroundImage
|
||||||
|
backgroundImageTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
|
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
backgroundClearButton.isFocusable = false
|
||||||
|
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
|
||||||
|
backgroundClearButton.icon = Icons.delete
|
||||||
|
backgroundClearButton.buttonType = FlatButton.ButtonType.toolBarButton
|
||||||
|
|
||||||
|
intervalSpinner.value = Appearance.interval
|
||||||
|
|
||||||
|
add(getFormPanel(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
backgroundButton.addActionListener {
|
||||||
|
val chooser = FileChooser()
|
||||||
|
chooser.osxAllowedFileTypes = listOf("png", "jpg", "jpeg")
|
||||||
|
chooser.allowsMultiSelection = false
|
||||||
|
chooser.win32Filters.add(Pair("Image files", listOf("png", "jpg", "jpeg")))
|
||||||
|
chooser.fileSelectionMode = JFileChooser.FILES_AND_DIRECTORIES
|
||||||
|
chooser.showOpenDialog(owner).thenAccept {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
onSelectedBackgroundImage(it.first())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backgroundClearButton.addActionListener {
|
||||||
|
BackgroundManager.getInstance().clearBackgroundImage()
|
||||||
|
backgroundImageTextField.text = StringUtils.EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
|
intervalSpinner.addChangeListener {
|
||||||
|
val value = intervalSpinner.value
|
||||||
|
if (value is Int) {
|
||||||
|
Appearance.interval = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSelectedBackgroundImage(file: File) {
|
||||||
|
try {
|
||||||
|
if (file.isFile) {
|
||||||
|
val destFile = FileUtils.getFile(Application.getBaseDataDir(), "background", file.name)
|
||||||
|
FileUtils.forceMkdirParent(destFile)
|
||||||
|
FileUtils.deleteQuietly(destFile)
|
||||||
|
FileUtils.copyFile(file, destFile, StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
BackgroundManager.getInstance().setBackgroundImage(destFile.absolutePath)
|
||||||
|
} else if (file.isDirectory) {
|
||||||
|
BackgroundManager.getInstance().setBackgroundImage(file.absolutePath)
|
||||||
|
}
|
||||||
|
backgroundImageTextField.text = file.absolutePath
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
ExceptionUtils.getRootCauseMessage(e),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.imageGray
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return BGI18n.getString("termora.plugins.bg.background-image")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getFormPanel(): JPanel {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default",
|
||||||
|
"pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val builder = FormBuilder.create().layout(layout)
|
||||||
|
val bgClearBox = Box.createHorizontalBox()
|
||||||
|
bgClearBox.add(backgroundClearButton)
|
||||||
|
|
||||||
|
builder.add("${BGI18n.getString("termora.plugins.bg.background-image")}:").xy(1, rows)
|
||||||
|
.add(backgroundImageTextField).xy(3, rows)
|
||||||
|
.add(bgClearBox).xy(5, rows)
|
||||||
|
.apply { rows += step }
|
||||||
|
|
||||||
|
builder.add("${BGI18n.getString("termora.plugins.bg.interval")}:").xy(1, rows)
|
||||||
|
.add(intervalSpinner).xy(3, rows)
|
||||||
|
.apply { rows += step }
|
||||||
|
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.OptionsPane
|
||||||
|
import app.termora.SettingsOptionExtension
|
||||||
|
|
||||||
|
class BackgroundSettingsOptionExtension private constructor(): SettingsOptionExtension {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { BackgroundSettingsOptionExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSettingsOption(): OptionsPane.Option {
|
||||||
|
return BackgroundOption()
|
||||||
|
}
|
||||||
|
}
|
||||||
23
plugins/bg/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>bg</id>
|
||||||
|
|
||||||
|
<name>Customize Background</name>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.bg.BGPlugin</entry>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Customize application background</description>
|
||||||
|
<description language="zh_CN">自定义应用程序背景</description>
|
||||||
|
<description language="zh_TW">自訂應用程式背景</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
6
plugins/bg/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#3574F0"/>
|
||||||
|
<path d="M2.5 9.33566L4.1822 7.66899C4.56052 7.29415 5.16625 7.28159 5.55979 7.64043L11.9861 13.5" stroke="#3574F0"/>
|
||||||
|
<circle cx="10" cy="6" r="1.5" stroke="#3574F0"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 472 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#548AF7"/>
|
||||||
|
<path d="M2.5 9.33566L4.1822 7.66899C4.56052 7.29415 5.16625 7.28159 5.55979 7.64043L11.9861 13.5" stroke="#548AF7"/>
|
||||||
|
<circle cx="10" cy="6" r="1.5" stroke="#548AF7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 472 B |
2
plugins/bg/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
termora.plugins.bg.interval=Interval
|
||||||
|
termora.plugins.bg.background-image=Background Image
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
termora.plugins.bg.background-image=背景图
|
||||||
|
termora.plugins.bg.interval=切换间隔
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
termora.plugins.bg.background-image=背景圖
|
||||||
|
termora.plugins.bg.interval=切換間隔
|
||||||
89
plugins/common.gradle.kts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||||
|
|
||||||
|
|
||||||
|
tasks.withType<Jar> {
|
||||||
|
|
||||||
|
manifest {
|
||||||
|
attributes(
|
||||||
|
"Implementation-Title" to project.name,
|
||||||
|
"Implementation-Version" to project.version,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
from("${rootProject.projectDir}/plugins/LICENSE") {
|
||||||
|
into("META-INF")
|
||||||
|
}
|
||||||
|
|
||||||
|
from("${rootProject.projectDir}/plugins/THIRDPARTY") {
|
||||||
|
into("META-INF")
|
||||||
|
}
|
||||||
|
|
||||||
|
// archiveBaseName.set("${project.name}-${rootProject.version}")
|
||||||
|
destinationDirectory.set(file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named<Copy>("processResources") {
|
||||||
|
filesMatching("META-INF/plugin.xml") {
|
||||||
|
expand(
|
||||||
|
"projectName" to project.name,
|
||||||
|
"projectVersion" to project.version,
|
||||||
|
"rootProjectVersion" to rootProject.version,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Copy>("copy-dependencies") {
|
||||||
|
from(configurations.getByName("runtimeClasspath").filterNot {
|
||||||
|
it.name.startsWith("kotlin-stdlib") || it.name.startsWith("annotations")
|
||||||
|
})
|
||||||
|
into("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("build") {
|
||||||
|
dependsOn("copy-dependencies")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("run-plugin") {
|
||||||
|
dependsOn("build")
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
|
|
||||||
|
val runtimeCompileOnly by configurations.creating { extendsFrom(configurations.getByName("compileOnly")) }
|
||||||
|
val mainClass = "app.termora.MainKt"
|
||||||
|
val executable = System.getProperty("java.home") + "/bin/java"
|
||||||
|
val classpath = (configurations.getByName("compileClasspath") + configurations.getByName("runtimeClasspath")
|
||||||
|
+ runtimeCompileOnly).joinToString(if (os.isWindows) ";" else ":")
|
||||||
|
val commands = mutableListOf<String>(executable)
|
||||||
|
commands.add("-Dapp-version=${rootProject.version}")
|
||||||
|
commands.add("--add-exports java.base/sun.nio.ch=ALL-UNNAMED")
|
||||||
|
if (os.isMacOsX) {
|
||||||
|
// NSWindow
|
||||||
|
commands.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||||
|
commands.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||||
|
commands.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||||
|
commands.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||||
|
commands.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
|
||||||
|
commands.add("-Dapple.awt.application.appearance=system")
|
||||||
|
}
|
||||||
|
commands.addAll(listOf("-cp", classpath, mainClass))
|
||||||
|
|
||||||
|
exec {
|
||||||
|
commandLine = commands
|
||||||
|
environment(
|
||||||
|
"TERMORA_PLUGIN_DIRECTORY" to file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/"),
|
||||||
|
"TERMORA_BASE_DATA_DIR" to "${layout.buildDirectory.get().asFile.absolutePath}/data",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Test>().configureEach {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("clean") {
|
||||||
|
doLast {
|
||||||
|
file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}").deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
16
plugins/cos/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
project.version = "0.0.3"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
implementation("com.qcloud:cos_api:5.6.247")
|
||||||
|
compileOnly(project(":"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.AuthenticationType
|
||||||
|
import app.termora.Proxy
|
||||||
|
import app.termora.ProxyType
|
||||||
|
import com.qcloud.cos.COSClient
|
||||||
|
import com.qcloud.cos.ClientConfig
|
||||||
|
import com.qcloud.cos.auth.BasicCOSCredentials
|
||||||
|
import com.qcloud.cos.model.Bucket
|
||||||
|
import com.qcloud.cos.region.Region
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
class COSClientHandler(
|
||||||
|
private val cred: BasicCOSCredentials,
|
||||||
|
private val proxy: Proxy,
|
||||||
|
val buckets: List<Bucket>
|
||||||
|
) : Closeable {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun createCOSClient(cred: BasicCOSCredentials, region: String, proxy: Proxy): COSClient {
|
||||||
|
val clientConfig = ClientConfig()
|
||||||
|
if (region.isNotBlank()) {
|
||||||
|
clientConfig.region = Region(region)
|
||||||
|
}
|
||||||
|
clientConfig.isPrintShutdownStackTrace = false
|
||||||
|
if (proxy.type == ProxyType.HTTP) {
|
||||||
|
clientConfig.httpProxyIp = proxy.host
|
||||||
|
clientConfig.httpProxyPort = proxy.port
|
||||||
|
if (proxy.authenticationType == AuthenticationType.Password) {
|
||||||
|
clientConfig.proxyPassword = proxy.password
|
||||||
|
clientConfig.proxyUsername = proxy.username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return COSClient(cred, clientConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* key: Region
|
||||||
|
* value: Client
|
||||||
|
*/
|
||||||
|
private val clients = mutableMapOf<String, COSClient>()
|
||||||
|
private val closed = AtomicBoolean(false)
|
||||||
|
|
||||||
|
fun getClientForBucket(bucket: String): COSClient {
|
||||||
|
if (closed.get()) throw IllegalStateException("Client already closed")
|
||||||
|
|
||||||
|
synchronized(this) {
|
||||||
|
val bucket = buckets.first { it.name == bucket }
|
||||||
|
if (clients.containsKey(bucket.location)) {
|
||||||
|
return clients.getValue(bucket.location)
|
||||||
|
}
|
||||||
|
clients[bucket.location] = createCOSClient(cred, bucket.location, proxy)
|
||||||
|
return clients.getValue(bucket.location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
if (closed.compareAndSet(false, true)) {
|
||||||
|
synchronized(this) {
|
||||||
|
clients.forEach { it.value.shutdown() }
|
||||||
|
clients.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3FileSystem
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* key: region
|
||||||
|
*/
|
||||||
|
class COSFileSystem(private val clientHandler: COSClientHandler) :
|
||||||
|
S3FileSystem(COSFileSystemProvider(clientHandler)) {
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(clientHandler)
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3FileAttributes
|
||||||
|
import app.termora.transfer.s3.S3FileSystemProvider
|
||||||
|
import app.termora.transfer.s3.S3Path
|
||||||
|
import com.qcloud.cos.model.ListObjectsRequest
|
||||||
|
import com.qcloud.cos.model.ObjectMetadata
|
||||||
|
import com.qcloud.cos.model.PutObjectRequest
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.io.PipedInputStream
|
||||||
|
import java.io.PipedOutputStream
|
||||||
|
import java.nio.file.AccessMode
|
||||||
|
import java.nio.file.NoSuchFileException
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
|
||||||
|
class COSFileSystemProvider(private val clientHandler: COSClientHandler) : S3FileSystemProvider() {
|
||||||
|
|
||||||
|
|
||||||
|
override fun getScheme(): String? {
|
||||||
|
return "cos"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOutputStream(path: S3Path): OutputStream {
|
||||||
|
return createStreamer(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInputStream(path: S3Path): InputStream {
|
||||||
|
val client = clientHandler.getClientForBucket(path.bucketName)
|
||||||
|
return client.getObject(path.bucketName, path.objectName).objectContent
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createStreamer(path: S3Path): OutputStream {
|
||||||
|
val pis = PipedInputStream()
|
||||||
|
val pos = PipedOutputStream(pis)
|
||||||
|
val exception = AtomicReference<Throwable>()
|
||||||
|
|
||||||
|
val thread = Thread.ofVirtual().start {
|
||||||
|
try {
|
||||||
|
val client = clientHandler.getClientForBucket(path.bucketName)
|
||||||
|
client.putObject(PutObjectRequest(path.bucketName, path.objectName, pis, ObjectMetadata()))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
exception.set(e)
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(pis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return object : OutputStream() {
|
||||||
|
override fun write(b: Int) {
|
||||||
|
val exception = exception.get()
|
||||||
|
if (exception != null) throw exception
|
||||||
|
pos.write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
pos.close()
|
||||||
|
if (thread.isAlive) thread.join()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
|
||||||
|
val paths = mutableListOf<S3Path>()
|
||||||
|
|
||||||
|
// root
|
||||||
|
if (path.isRoot) {
|
||||||
|
for (bucket in clientHandler.buckets) {
|
||||||
|
val p = path.resolve(bucket.name)
|
||||||
|
p.attributes = S3FileAttributes(
|
||||||
|
directory = true,
|
||||||
|
lastModifiedTime = bucket.creationDate.toInstant().toEpochMilli()
|
||||||
|
)
|
||||||
|
paths.add(p)
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextMarker = StringUtils.EMPTY
|
||||||
|
val maxKeys = 100
|
||||||
|
val bucketName = path.bucketName
|
||||||
|
while (true) {
|
||||||
|
val request = ListObjectsRequest()
|
||||||
|
.withBucketName(bucketName)
|
||||||
|
.withMaxKeys(maxKeys)
|
||||||
|
.withDelimiter(path.fileSystem.separator)
|
||||||
|
|
||||||
|
if (path.objectName.isNotBlank()) request.withPrefix(path.objectName + path.fileSystem.separator)
|
||||||
|
if (nextMarker.isNotBlank()) request.withMarker(nextMarker)
|
||||||
|
|
||||||
|
|
||||||
|
val objectListing = clientHandler.getClientForBucket(bucketName).listObjects(request)
|
||||||
|
for (e in objectListing.commonPrefixes) {
|
||||||
|
val p = path.bucket.resolve(e)
|
||||||
|
p.attributes = p.attributes.copy(directory = true)
|
||||||
|
delete(p)
|
||||||
|
paths.add(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (e in objectListing.objectSummaries) {
|
||||||
|
val p = path.bucket.resolve(e.key)
|
||||||
|
p.attributes = p.attributes.copy(
|
||||||
|
regularFile = true, size = e.size,
|
||||||
|
lastModifiedTime = e.lastModified.time
|
||||||
|
)
|
||||||
|
paths.add(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectListing.isTruncated.not()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
nextMarker = objectListing.nextMarker
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
paths.addAll(directories[path.absolutePathString()] ?: emptyList())
|
||||||
|
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(path: S3Path, isDirectory: Boolean) {
|
||||||
|
if (isDirectory.not())
|
||||||
|
clientHandler.getClientForBucket(path.bucketName).deleteObject(path.bucketName, path.objectName)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
|
||||||
|
try {
|
||||||
|
val client = clientHandler.getClientForBucket(path.bucketName)
|
||||||
|
if (client.doesObjectExist(path.bucketName, path.objectName).not()) {
|
||||||
|
throw NoSuchFileException(path.objectName)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is NoSuchFileException) throw e
|
||||||
|
throw NoSuchFileException(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.plugin.internal.BasicProxyOption
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
|
import java.awt.event.ComponentAdapter
|
||||||
|
import java.awt.event.ComponentEvent
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
class COSHostOptionsPane : OptionsPane() {
|
||||||
|
private val generalOption = GeneralOption()
|
||||||
|
private val proxyOption = BasicProxyOption(listOf(ProxyType.HTTP))
|
||||||
|
private val sftpOption = SFTPOption()
|
||||||
|
|
||||||
|
init {
|
||||||
|
addOption(generalOption)
|
||||||
|
addOption(proxyOption)
|
||||||
|
addOption(sftpOption)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHost(): Host {
|
||||||
|
val name = generalOption.nameTextField.text
|
||||||
|
val protocol = COSProtocolProvider.PROTOCOL
|
||||||
|
val port = 0
|
||||||
|
var authentication = Authentication.Companion.No
|
||||||
|
var proxy = Proxy.Companion.No
|
||||||
|
val authenticationType = AuthenticationType.Password
|
||||||
|
|
||||||
|
authentication = authentication.copy(
|
||||||
|
type = authenticationType,
|
||||||
|
password = String(generalOption.passwordTextField.password)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||||
|
proxy = proxy.copy(
|
||||||
|
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
|
||||||
|
host = proxyOption.proxyHostTextField.text,
|
||||||
|
username = proxyOption.proxyUsernameTextField.text,
|
||||||
|
password = String(proxyOption.proxyPasswordTextField.password),
|
||||||
|
port = proxyOption.proxyPortTextField.value as Int,
|
||||||
|
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val options = Options.Default.copy(
|
||||||
|
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||||
|
extras = mutableMapOf(
|
||||||
|
"cos.delimiter" to generalOption.delimiterTextField.text,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Host(
|
||||||
|
name = name,
|
||||||
|
protocol = protocol,
|
||||||
|
port = port,
|
||||||
|
username = generalOption.usernameTextField.text,
|
||||||
|
authentication = authentication,
|
||||||
|
proxy = proxy,
|
||||||
|
sort = System.currentTimeMillis(),
|
||||||
|
remark = generalOption.remarkTextArea.text,
|
||||||
|
options = options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHost(host: Host) {
|
||||||
|
generalOption.nameTextField.text = host.name
|
||||||
|
generalOption.usernameTextField.text = host.username
|
||||||
|
generalOption.remarkTextArea.text = host.remark
|
||||||
|
generalOption.passwordTextField.text = host.authentication.password
|
||||||
|
generalOption.delimiterTextField.text = host.options.extras["cos.delimiter"] ?: StringUtils.EMPTY
|
||||||
|
|
||||||
|
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||||
|
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||||
|
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||||
|
proxyOption.proxyUsernameTextField.text = host.proxy.username
|
||||||
|
proxyOption.proxyPortTextField.value = host.proxy.port
|
||||||
|
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
|
||||||
|
|
||||||
|
|
||||||
|
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateFields(): Boolean {
|
||||||
|
val host = getHost()
|
||||||
|
|
||||||
|
// general
|
||||||
|
if (validateField(generalOption.nameTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateField(generalOption.usernameTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
|
if (validateField(generalOption.passwordTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxy
|
||||||
|
if (host.proxy.type != ProxyType.No) {
|
||||||
|
if (validateField(proxyOption.proxyHostTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.proxy.authenticationType != AuthenticationType.No) {
|
||||||
|
if (validateField(proxyOption.proxyUsernameTextField)
|
||||||
|
|| validateField(proxyOption.proxyPasswordTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示有错误
|
||||||
|
*/
|
||||||
|
private fun validateField(textField: JTextField): Boolean {
|
||||||
|
if (textField.isEnabled && textField.text.isBlank()) {
|
||||||
|
setOutlineError(textField)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setOutlineError(c: JComponent) {
|
||||||
|
selectOptionJComponent(c)
|
||||||
|
c.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
|
c.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||||
|
val nameTextField = OutlineTextField(128)
|
||||||
|
val usernameTextField = OutlineTextField(128)
|
||||||
|
val passwordTextField = OutlinePasswordField(255)
|
||||||
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
|
|
||||||
|
// val regionComboBox = OutlineComboBox<String>()
|
||||||
|
val delimiterTextField = OutlineTextField(128)
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
|
||||||
|
/*regionComboBox.addItem("ap-beijing-1")
|
||||||
|
regionComboBox.addItem("ap-beijing")
|
||||||
|
regionComboBox.addItem("ap-nanjing")
|
||||||
|
regionComboBox.addItem("ap-shanghai")
|
||||||
|
regionComboBox.addItem("ap-guangzhou")
|
||||||
|
regionComboBox.addItem("ap-chengdu")
|
||||||
|
regionComboBox.addItem("ap-chongqing")
|
||||||
|
regionComboBox.addItem("ap-shenzhen-fsi")
|
||||||
|
regionComboBox.addItem("ap-shanghai-fsi")
|
||||||
|
regionComboBox.addItem("ap-beijing-fsi")
|
||||||
|
|
||||||
|
regionComboBox.addItem("ap-hongkong")
|
||||||
|
regionComboBox.addItem("ap-singapore")
|
||||||
|
regionComboBox.addItem("ap-jakarta")
|
||||||
|
regionComboBox.addItem("ap-seoul")
|
||||||
|
regionComboBox.addItem("ap-bangkok")
|
||||||
|
regionComboBox.addItem("ap-tokyo")
|
||||||
|
regionComboBox.addItem("na-siliconvalley")
|
||||||
|
regionComboBox.addItem("na-ashburn")
|
||||||
|
regionComboBox.addItem("sa-saopaulo")
|
||||||
|
regionComboBox.addItem("eu-frankfurt")
|
||||||
|
|
||||||
|
regionComboBox.isEditable = true*/
|
||||||
|
|
||||||
|
delimiterTextField.text = "/"
|
||||||
|
delimiterTextField.isEditable = false
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentResized(e: ComponentEvent) {
|
||||||
|
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
|
||||||
|
removeComponentListener(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.general")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
|
||||||
|
remarkTextArea.rows = 8
|
||||||
|
remarkTextArea.lineWrap = true
|
||||||
|
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
|
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
||||||
|
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("SecretId:").xy(1, rows)
|
||||||
|
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("SecretKey:").xy(1, rows)
|
||||||
|
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("Delimiter:").xy(1, rows)
|
||||||
|
.add(delimiterTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||||
|
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||||
|
.xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||||
|
val defaultDirectoryField = OutlineTextField(255)
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.folder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.transport.sftp")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||||
|
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionSupport
|
||||||
|
import app.termora.plugin.PaidPlugin
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
class COSPlugin : PaidPlugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(ProtocolProviderExtension::class.java) { COSProtocolProviderExtension.instance }
|
||||||
|
support.addExtension(ProtocolHostPanelExtension::class.java) { COSProtocolHostPanelExtension.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "Tencent COS"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.Host
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
|
||||||
|
class COSProtocolHostPanel : ProtocolHostPanel() {
|
||||||
|
|
||||||
|
private val pane = COSHostOptionsPane()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(pane, BorderLayout.CENTER)
|
||||||
|
Disposer.register(this, pane)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {}
|
||||||
|
|
||||||
|
override fun getHost(): Host {
|
||||||
|
return pane.getHost()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setHost(host: Host) {
|
||||||
|
pane.setHost(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun validateFields(): Boolean {
|
||||||
|
return pane.validateFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
|
||||||
|
class COSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { COSProtocolHostPanelExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return COSProtocolProvider.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
|
return COSProtocolHostPanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.DynamicIcon
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.protocol.PathHandler
|
||||||
|
import app.termora.protocol.PathHandlerRequest
|
||||||
|
import app.termora.protocol.TransferProtocolProvider
|
||||||
|
import com.qcloud.cos.ClientConfig
|
||||||
|
import com.qcloud.cos.auth.BasicCOSCredentials
|
||||||
|
import com.qcloud.cos.model.Bucket
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
|
||||||
|
|
||||||
|
class COSProtocolProvider private constructor() : TransferProtocolProvider {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { COSProtocolProvider() }
|
||||||
|
const val PROTOCOL = "COS"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocol(): String {
|
||||||
|
return PROTOCOL
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||||
|
return Icons.tencent
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
|
||||||
|
val host = requester.host
|
||||||
|
val secretId = host.username
|
||||||
|
val secretKey = host.authentication.password
|
||||||
|
val cred = BasicCOSCredentials(secretId, secretKey)
|
||||||
|
val clientConfig = ClientConfig()
|
||||||
|
|
||||||
|
clientConfig.isPrintShutdownStackTrace = false
|
||||||
|
val cosClient = COSClientHandler.createCOSClient(cred, StringUtils.EMPTY, host.proxy)
|
||||||
|
val buckets: List<Bucket>
|
||||||
|
|
||||||
|
try {
|
||||||
|
buckets = cosClient.listBuckets()
|
||||||
|
} finally {
|
||||||
|
cosClient.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultPath = host.options.sftpDefaultDirectory
|
||||||
|
val fs = COSFileSystem(COSClientHandler(cred, host.proxy, buckets))
|
||||||
|
return PathHandler(fs, fs.getPath(defaultPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
class COSProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { COSProtocolProviderExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return COSProtocolProvider.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
25
plugins/cos/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>cos</id>
|
||||||
|
|
||||||
|
<name>Tencent COS</name>
|
||||||
|
|
||||||
|
|
||||||
|
<paid/>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.cos.COSPlugin</entry>
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Connecting to Tencent COS</description>
|
||||||
|
<description language="zh_CN">支持连接到腾讯云对象存储</description>
|
||||||
|
<description language="zh_TW">支援連接到騰訊雲物件存儲</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
1
plugins/cos/src/main/resources/META-INF/pluginIcon.svg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
19
plugins/editor/build.gradle.kts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
project.version = "0.0.6"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
compileOnly(project(":"))
|
||||||
|
implementation("com.fifesoft:rsyntaxtextarea:3.6.0")
|
||||||
|
implementation("com.fifesoft:languagesupport:3.4.0")
|
||||||
|
implementation("com.fifesoft:autocomplete:3.3.2")
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package app.termora.plugins.editor
|
||||||
|
|
||||||
|
import app.termora.DialogWrapper
|
||||||
|
import app.termora.Disposable
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.OptionPane
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.UIManager
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
import kotlin.io.path.name
|
||||||
|
|
||||||
|
|
||||||
|
class EditorDialog(file: Path, owner: Window, private val myDisposable: Disposable) : DialogWrapper(null) {
|
||||||
|
|
||||||
|
private val filename = file.name
|
||||||
|
private val filepath = File(file.absolutePathString())
|
||||||
|
private val editorPanel = EditorPanel(this, filepath)
|
||||||
|
private val disposed = AtomicBoolean()
|
||||||
|
|
||||||
|
init {
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||||
|
isModal = false
|
||||||
|
controlsVisible = true
|
||||||
|
isResizable = true
|
||||||
|
title = filename
|
||||||
|
iconImages = owner.iconImages
|
||||||
|
escapeDispose = false
|
||||||
|
defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
||||||
|
|
||||||
|
initEvents()
|
||||||
|
|
||||||
|
setLocationRelativeTo(owner)
|
||||||
|
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowClosing(e: WindowEvent?) {
|
||||||
|
if (disposed.compareAndSet(false, true)) {
|
||||||
|
doCancelAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Disposer.register(myDisposable, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
if (disposed.compareAndSet(false, true)) {
|
||||||
|
doCancelAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Disposer.register(disposable, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
if (disposed.compareAndSet(false, true)) {
|
||||||
|
Disposer.dispose(myDisposable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doCancelAction() {
|
||||||
|
if (editorPanel.changes()) {
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
this,
|
||||||
|
"文件尚未保存,你确定要退出吗?",
|
||||||
|
optionType = JOptionPane.OK_CANCEL_OPTION,
|
||||||
|
) != JOptionPane.OK_OPTION
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.doCancelAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
return editorPanel
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSouthPanel(): JComponent? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
package app.termora.plugins.editor
|
||||||
|
|
||||||
|
import app.termora.DocumentAdaptor
|
||||||
|
import app.termora.DynamicColor
|
||||||
|
import app.termora.EnableManager
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.database.DatabaseManager
|
||||||
|
import com.formdev.flatlaf.FlatLaf
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatTextField
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.apache.commons.io.FilenameUtils
|
||||||
|
import org.dom4j.io.OutputFormat
|
||||||
|
import org.dom4j.io.SAXReader
|
||||||
|
import org.dom4j.io.XMLWriter
|
||||||
|
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea
|
||||||
|
import org.fife.ui.rsyntaxtextarea.SyntaxConstants
|
||||||
|
import org.fife.ui.rsyntaxtextarea.Theme
|
||||||
|
import org.fife.ui.rtextarea.RTextScrollPane
|
||||||
|
import org.fife.ui.rtextarea.SearchContext
|
||||||
|
import org.fife.ui.rtextarea.SearchEngine
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Insets
|
||||||
|
import java.awt.event.ActionEvent
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
|
import java.io.File
|
||||||
|
import java.io.StringReader
|
||||||
|
import java.io.StringWriter
|
||||||
|
import javax.swing.*
|
||||||
|
import javax.swing.SwingConstants.VERTICAL
|
||||||
|
import javax.swing.event.DocumentEvent
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class EditorPanel(private val window: JDialog, private val file: File) : JPanel(BorderLayout()) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(EditorPanel::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var text = file.readText(Charsets.UTF_8)
|
||||||
|
private val layeredPane = LayeredPane()
|
||||||
|
|
||||||
|
private val textArea = RSyntaxTextArea()
|
||||||
|
private val scrollPane = RTextScrollPane(textArea)
|
||||||
|
private val findPanel = FlatToolBar().apply { isFloatable = false }
|
||||||
|
private val toolbar = FlatToolBar().apply { isFloatable = false }
|
||||||
|
private val searchTextField = FlatTextField()
|
||||||
|
private val closeFindPanelBtn = JButton(Icons.close)
|
||||||
|
private val nextBtn = JButton(Icons.down)
|
||||||
|
private val prevBtn = JButton(Icons.up)
|
||||||
|
private val context = SearchContext()
|
||||||
|
private val softWrapBtn = JToggleButton(Icons.softWrap)
|
||||||
|
private val scrollUpBtn = JButton(Icons.scrollUp)
|
||||||
|
private val scrollEndBtn = JButton(Icons.scrollDown)
|
||||||
|
private val prettyBtn = JButton(Icons.reformatCode)
|
||||||
|
|
||||||
|
private val enableManager get() = EnableManager.getInstance()
|
||||||
|
private val prettyJson = Json {
|
||||||
|
prettyPrint = true
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
textArea.font = textArea.font.deriveFont(DatabaseManager.getInstance().terminal.fontSize.toFloat())
|
||||||
|
textArea.text = text
|
||||||
|
textArea.antiAliasingEnabled = true
|
||||||
|
softWrapBtn.isSelected = enableManager.getFlag("Plugins.editor.softWrap", false)
|
||||||
|
|
||||||
|
val theme = if (FlatLaf.isLafDark())
|
||||||
|
Theme.load(javaClass.getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/dark.xml"))
|
||||||
|
else
|
||||||
|
Theme.load(javaClass.getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/idea.xml"))
|
||||||
|
|
||||||
|
theme.apply(textArea)
|
||||||
|
|
||||||
|
val extension = FilenameUtils.getExtension(file.name)?.lowercase()
|
||||||
|
textArea.syntaxEditingStyle = when (extension) {
|
||||||
|
"java" -> SyntaxConstants.SYNTAX_STYLE_JAVA
|
||||||
|
"kt" -> SyntaxConstants.SYNTAX_STYLE_KOTLIN
|
||||||
|
"properties" -> SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE
|
||||||
|
"cpp", "c++" -> SyntaxConstants.SYNTAX_STYLE_CPLUSPLUS
|
||||||
|
"c" -> SyntaxConstants.SYNTAX_STYLE_C
|
||||||
|
"cs" -> SyntaxConstants.SYNTAX_STYLE_CSHARP
|
||||||
|
"css" -> SyntaxConstants.SYNTAX_STYLE_CSS
|
||||||
|
"html", "htm", "htmlx" -> SyntaxConstants.SYNTAX_STYLE_HTML
|
||||||
|
"js" -> SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT
|
||||||
|
"ts" -> SyntaxConstants.SYNTAX_STYLE_TYPESCRIPT
|
||||||
|
"xml", "svg" -> SyntaxConstants.SYNTAX_STYLE_XML
|
||||||
|
"yaml", "yml" -> SyntaxConstants.SYNTAX_STYLE_YAML
|
||||||
|
"sh", "shell" -> SyntaxConstants.SYNTAX_STYLE_UNIX_SHELL
|
||||||
|
"sql" -> SyntaxConstants.SYNTAX_STYLE_SQL
|
||||||
|
"bat" -> SyntaxConstants.SYNTAX_STYLE_WINDOWS_BATCH
|
||||||
|
"py" -> SyntaxConstants.SYNTAX_STYLE_PYTHON
|
||||||
|
"php" -> SyntaxConstants.SYNTAX_STYLE_PHP
|
||||||
|
"lua" -> SyntaxConstants.SYNTAX_STYLE_LUA
|
||||||
|
"less" -> SyntaxConstants.SYNTAX_STYLE_LESS
|
||||||
|
"jsp" -> SyntaxConstants.SYNTAX_STYLE_JSP
|
||||||
|
"json" -> SyntaxConstants.SYNTAX_STYLE_JSON
|
||||||
|
"ini" -> SyntaxConstants.SYNTAX_STYLE_INI
|
||||||
|
"hosts" -> SyntaxConstants.SYNTAX_STYLE_HOSTS
|
||||||
|
"go" -> SyntaxConstants.SYNTAX_STYLE_GO
|
||||||
|
"dtd" -> SyntaxConstants.SYNTAX_STYLE_DTD
|
||||||
|
"dart" -> SyntaxConstants.SYNTAX_STYLE_DART
|
||||||
|
"csv" -> SyntaxConstants.SYNTAX_STYLE_CSV
|
||||||
|
"md" -> SyntaxConstants.SYNTAX_STYLE_MARKDOWN
|
||||||
|
else -> SyntaxConstants.SYNTAX_STYLE_NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有 JSON 才可以格式化
|
||||||
|
prettyBtn.isVisible = textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_JSON ||
|
||||||
|
textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_XML
|
||||||
|
|
||||||
|
textArea.discardAllEdits()
|
||||||
|
|
||||||
|
scrollPane.border = BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor)
|
||||||
|
|
||||||
|
findPanel.isVisible = false
|
||||||
|
findPanel.isOpaque = true
|
||||||
|
findPanel.background = DynamicColor("window")
|
||||||
|
|
||||||
|
searchTextField.background = findPanel.background
|
||||||
|
searchTextField.padding = Insets(0, 4, 0, 0)
|
||||||
|
searchTextField.border = BorderFactory.createEmptyBorder()
|
||||||
|
|
||||||
|
findPanel.add(searchTextField)
|
||||||
|
findPanel.add(prevBtn)
|
||||||
|
findPanel.add(nextBtn)
|
||||||
|
findPanel.add(closeFindPanelBtn)
|
||||||
|
findPanel.border = BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createMatteBorder(0, 1, 1, 0, DynamicColor.BorderColor),
|
||||||
|
BorderFactory.createEmptyBorder(2, 2, 2, 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
toolbar.orientation = VERTICAL
|
||||||
|
toolbar.add(scrollUpBtn)
|
||||||
|
toolbar.add(prettyBtn)
|
||||||
|
toolbar.add(softWrapBtn)
|
||||||
|
toolbar.add(scrollEndBtn)
|
||||||
|
|
||||||
|
val viewPanel = JPanel(BorderLayout())
|
||||||
|
viewPanel.add(scrollPane, BorderLayout.CENTER)
|
||||||
|
viewPanel.add(toolbar, BorderLayout.EAST)
|
||||||
|
viewPanel.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||||
|
|
||||||
|
layeredPane.add(findPanel, JLayeredPane.MODAL_LAYER as Any)
|
||||||
|
layeredPane.add(viewPanel, JLayeredPane.DEFAULT_LAYER as Any)
|
||||||
|
|
||||||
|
add(layeredPane, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
window.addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowOpened(e: WindowEvent?) {
|
||||||
|
scrollPane.verticalScrollBar.value = 0
|
||||||
|
window.removeWindowListener(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
softWrapBtn.addActionListener {
|
||||||
|
enableManager.getFlag("Plugins.editor.softWrap", softWrapBtn.isSelected)
|
||||||
|
textArea.lineWrap = softWrapBtn.isSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollUpBtn.addActionListener { scrollPane.verticalScrollBar.value = 0 }
|
||||||
|
scrollEndBtn.addActionListener { scrollPane.verticalScrollBar.value = scrollPane.verticalScrollBar.maximum }
|
||||||
|
|
||||||
|
textArea.inputMap.put(
|
||||||
|
KeyStroke.getKeyStroke(KeyEvent.VK_S, toolkit.menuShortcutKeyMaskEx),
|
||||||
|
"Save"
|
||||||
|
)
|
||||||
|
textArea.inputMap.put(
|
||||||
|
KeyStroke.getKeyStroke(KeyEvent.VK_F, toolkit.menuShortcutKeyMaskEx),
|
||||||
|
"Find"
|
||||||
|
)
|
||||||
|
textArea.inputMap.put(
|
||||||
|
KeyStroke.getKeyStroke(KeyEvent.VK_F, toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK),
|
||||||
|
"Format"
|
||||||
|
)
|
||||||
|
|
||||||
|
searchTextField.inputMap.put(
|
||||||
|
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
|
||||||
|
"Esc"
|
||||||
|
)
|
||||||
|
|
||||||
|
searchTextField.actionMap.put("Esc", object : AbstractAction("Esc") {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
textArea.clearMarkAllHighlights()
|
||||||
|
textArea.requestFocusInWindow()
|
||||||
|
findPanel.isVisible = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
closeFindPanelBtn.addActionListener { searchTextField.actionMap.get("Esc").actionPerformed(it) }
|
||||||
|
|
||||||
|
textArea.actionMap.put("Save", object : AbstractAction("Save") {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
file.writeText(textArea.text, Charsets.UTF_8)
|
||||||
|
text = textArea.text
|
||||||
|
window.title = file.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
textArea.actionMap.put("Format", object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
format()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
textArea.actionMap.put("Find", object : AbstractAction("Find") {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
findPanel.isVisible = true
|
||||||
|
searchTextField.selectAll()
|
||||||
|
searchTextField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
textArea.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
|
window.title = if (textArea.text.hashCode() != text.hashCode()) {
|
||||||
|
"${file.name} *"
|
||||||
|
} else {
|
||||||
|
file.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
searchTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
|
search()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
searchTextField.addActionListener { nextBtn.doClick(0) }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
prettyBtn.addActionListener(textArea.actionMap.get("Format"))
|
||||||
|
|
||||||
|
prevBtn.addActionListener { search(false) }
|
||||||
|
nextBtn.addActionListener { search(true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun format() {
|
||||||
|
val vertical = scrollPane.verticalScrollBar.value
|
||||||
|
val horizontal = scrollPane.horizontalScrollBar.value
|
||||||
|
val caretPosition = textArea.caretPosition
|
||||||
|
|
||||||
|
val c = if (textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_JSON) {
|
||||||
|
runCatching {
|
||||||
|
val json = prettyJson.parseToJsonElement(textArea.text)
|
||||||
|
textArea.text = prettyJson.encodeToString(json)
|
||||||
|
}.onFailure {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(it.message, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_XML) {
|
||||||
|
runCatching {
|
||||||
|
val document = SAXReader().read(StringReader(textArea.text))
|
||||||
|
val sw = StringWriter()
|
||||||
|
val writer = XMLWriter(sw, OutputFormat.createPrettyPrint())
|
||||||
|
writer.write(document)
|
||||||
|
textArea.text = sw.toString()
|
||||||
|
}.onFailure {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(it.message, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
} ?: return
|
||||||
|
|
||||||
|
c.onSuccess {
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
scrollPane.verticalScrollBar.value = min(
|
||||||
|
vertical,
|
||||||
|
scrollPane.verticalScrollBar.maximum
|
||||||
|
)
|
||||||
|
scrollPane.horizontalScrollBar.value = min(
|
||||||
|
horizontal,
|
||||||
|
scrollPane.horizontalScrollBar.maximum
|
||||||
|
)
|
||||||
|
if (caretPosition >= 0 && caretPosition < textArea.document.length) {
|
||||||
|
textArea.caretPosition = caretPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun search(searchForward: Boolean = true) {
|
||||||
|
textArea.clearMarkAllHighlights()
|
||||||
|
|
||||||
|
|
||||||
|
val text: String = searchTextField.getText()
|
||||||
|
if (text.isEmpty()) return
|
||||||
|
context.searchFor = text
|
||||||
|
context.searchForward = searchForward
|
||||||
|
context.wholeWord = false
|
||||||
|
val result = SearchEngine.find(textArea, context)
|
||||||
|
|
||||||
|
prevBtn.isEnabled = result.markedCount > 0
|
||||||
|
nextBtn.isEnabled = result.markedCount > 0
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changes() = text != textArea.text
|
||||||
|
|
||||||
|
private inner class LayeredPane : JLayeredPane() {
|
||||||
|
override fun doLayout() {
|
||||||
|
synchronized(treeLock) {
|
||||||
|
for (c in components) {
|
||||||
|
if (c == findPanel) {
|
||||||
|
val height = max(findPanel.preferredSize.height, findPanel.height)
|
||||||
|
val x = width / 2
|
||||||
|
c.setBounds(x, 1, width - x, height)
|
||||||
|
} else {
|
||||||
|
c.setBounds(0, 0, width, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package app.termora.plugins.editor
|
||||||
|
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionSupport
|
||||||
|
import app.termora.plugin.Plugin
|
||||||
|
import app.termora.transfer.TransportEditFileExtension
|
||||||
|
|
||||||
|
class EditorPlugin : Plugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(TransportEditFileExtension::class.java) { MyTransportEditFileExtension.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "SFTP File Editor"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.termora.plugins.editor
|
||||||
|
|
||||||
|
import app.termora.Disposable
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.transfer.TransportEditFileExtension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.nio.file.Path
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
class MyTransportEditFileExtension private constructor() : TransportEditFileExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = MyTransportEditFileExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun edit(owner: Window, path: Path): Disposable {
|
||||||
|
val disposable = Disposer.newDisposable()
|
||||||
|
SwingUtilities.invokeLater { EditorDialog(path, owner, disposable).isVisible = true }
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
|
}
|
||||||
22
plugins/editor/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>editor</id>
|
||||||
|
|
||||||
|
<name>SFTP File Editor</name>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.editor.EditorPlugin</entry>
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Edit SFTP files using the built-in editor</description>
|
||||||
|
<description language="zh_CN">使用内置编辑器编辑 SFTP 文件</description>
|
||||||
|
<description language="zh_TW">使用內建編輯器編輯 SFTP 文件</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V7.47774C14.2142 6.80872 13.0333 6.84543 12.2909 7.58786L7 12.8787V14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" />
|
||||||
|
<path d="M8.09379 5H13C13.5523 5 14 5.44772 14 6V7.02381C14.3594 7.07711 14.7072 7.22842 15 7.47774V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H7V13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823L8.09379 5Z" fill="#6C707E"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4122 8.29497C14.0217 7.90444 13.3885 7.90444 12.998 8.29497L11.6466 9.64633L8 13.2929V16H10.7071L15.7051 11.0021C16.0956 10.6116 16.0956 9.97839 15.7051 9.58786L14.4122 8.29497ZM14 11.2929L14.998 10.295L13.7051 9.00208L12.7071 10L14 11.2929ZM12 10.7072L13.2929 12L10.2929 15H9V13.7072L12 10.7072Z" fill="#6C707E"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V7.47774C14.2142 6.80872 13.0333 6.84543 12.2909 7.58786L7 12.8787V14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" />
|
||||||
|
<path d="M8.09379 5H13C13.5523 5 14 5.44772 14 6V7.02381C14.3594 7.07711 14.7072 7.22842 15 7.47774V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H7V13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823L8.09379 5Z" fill="#CED0D6"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4122 8.29497C14.0217 7.90444 13.3885 7.90444 12.998 8.29497L11.6466 9.64633L8 13.2929V16H10.7071L15.7051 11.0021C16.0956 10.6116 16.0956 9.97839 15.7051 9.58786L14.4122 8.29497ZM14 11.2929L14.998 10.295L13.7051 9.00208L12.7071 10L14 11.2929ZM12 10.7072L13.2929 12L10.2929 15H9V13.7072L12 10.7072Z" fill="#CED0D6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,107 @@
|
|||||||
|
package app.termora.plugins.editor;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.*;
|
||||||
|
import javax.swing.*;
|
||||||
|
|
||||||
|
import org.fife.ui.rtextarea.*;
|
||||||
|
import org.fife.ui.rsyntaxtextarea.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple example showing how to do search and replace in a RSyntaxTextArea.
|
||||||
|
* The toolbar isn't very user-friendly, but this is just to show you how to use
|
||||||
|
* the API.<p>
|
||||||
|
*
|
||||||
|
* This example uses RSyntaxTextArea 2.5.6.
|
||||||
|
*/
|
||||||
|
public class FindAndReplaceDemo extends JFrame implements ActionListener {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private RSyntaxTextArea textArea;
|
||||||
|
private JTextField searchField;
|
||||||
|
private JCheckBox regexCB;
|
||||||
|
private JCheckBox matchCaseCB;
|
||||||
|
|
||||||
|
public FindAndReplaceDemo() {
|
||||||
|
|
||||||
|
JPanel cp = new JPanel(new BorderLayout());
|
||||||
|
|
||||||
|
textArea = new RSyntaxTextArea(20, 60);
|
||||||
|
textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
|
||||||
|
textArea.setCodeFoldingEnabled(true);
|
||||||
|
RTextScrollPane sp = new RTextScrollPane(textArea);
|
||||||
|
cp.add(sp);
|
||||||
|
|
||||||
|
// Create a toolbar with searching options.
|
||||||
|
JToolBar toolBar = new JToolBar();
|
||||||
|
searchField = new JTextField(30);
|
||||||
|
toolBar.add(searchField);
|
||||||
|
final JButton nextButton = new JButton("Find Next");
|
||||||
|
nextButton.setActionCommand("FindNext");
|
||||||
|
nextButton.addActionListener(this);
|
||||||
|
toolBar.add(nextButton);
|
||||||
|
searchField.addActionListener(new ActionListener() {
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
nextButton.doClick(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
JButton prevButton = new JButton("Find Previous");
|
||||||
|
prevButton.setActionCommand("FindPrev");
|
||||||
|
prevButton.addActionListener(this);
|
||||||
|
toolBar.add(prevButton);
|
||||||
|
regexCB = new JCheckBox("Regex");
|
||||||
|
toolBar.add(regexCB);
|
||||||
|
matchCaseCB = new JCheckBox("Match Case");
|
||||||
|
toolBar.add(matchCaseCB);
|
||||||
|
cp.add(toolBar, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
setContentPane(cp);
|
||||||
|
setTitle("Find and Replace Demo");
|
||||||
|
setDefaultCloseOperation(EXIT_ON_CLOSE);
|
||||||
|
pack();
|
||||||
|
setLocationRelativeTo(null);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
|
||||||
|
// "FindNext" => search forward, "FindPrev" => search backward
|
||||||
|
String command = e.getActionCommand();
|
||||||
|
boolean forward = "FindNext".equals(command);
|
||||||
|
|
||||||
|
// Create an object defining our search parameters.
|
||||||
|
SearchContext context = new SearchContext();
|
||||||
|
String text = searchField.getText();
|
||||||
|
if (text.length() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.setSearchFor(text);
|
||||||
|
context.setMatchCase(matchCaseCB.isSelected());
|
||||||
|
context.setRegularExpression(regexCB.isSelected());
|
||||||
|
context.setSearchForward(forward);
|
||||||
|
context.setWholeWord(false);
|
||||||
|
|
||||||
|
boolean found = SearchEngine.find(textArea, context).wasFound();
|
||||||
|
if (!found) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Text not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// Start all Swing applications on the EDT.
|
||||||
|
SwingUtilities.invokeLater(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
String laf = UIManager.getSystemLookAndFeelClassName();
|
||||||
|
UIManager.setLookAndFeel(laf);
|
||||||
|
} catch (Exception e) { /* never happens */ }
|
||||||
|
FindAndReplaceDemo demo = new FindAndReplaceDemo();
|
||||||
|
demo.setVisible(true);
|
||||||
|
demo.textArea.requestFocusInWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
15
plugins/ftp/build.gradle.kts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
project.version = "0.0.2"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
compileOnly(project(":"))
|
||||||
|
implementation("org.apache.commons:commons-pool2:2.12.1")
|
||||||
|
testImplementation(project(":"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3FileSystem
|
||||||
|
import app.termora.transfer.s3.S3Path
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.net.ftp.FTPClient
|
||||||
|
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||||
|
|
||||||
|
class FTPFileSystem(private val pool: GenericObjectPool<FTPClient>) : S3FileSystem(FTPSystemProvider(pool)) {
|
||||||
|
|
||||||
|
override fun create(root: String?, names: List<String>): S3Path {
|
||||||
|
val path = FTPPath(this, root, names)
|
||||||
|
if (names.isEmpty()) {
|
||||||
|
path.attributes = path.attributes.copy(directory = true)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(pool)
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.plugin.internal.BasicProxyOption
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
|
import java.awt.event.ComponentAdapter
|
||||||
|
import java.awt.event.ComponentEvent
|
||||||
|
import java.awt.event.ItemEvent
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
class FTPHostOptionsPane : OptionsPane() {
|
||||||
|
private val generalOption = GeneralOption()
|
||||||
|
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
|
||||||
|
private val sftpOption = SFTPOption()
|
||||||
|
|
||||||
|
init {
|
||||||
|
addOption(generalOption)
|
||||||
|
addOption(proxyOption)
|
||||||
|
addOption(sftpOption)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHost(): Host {
|
||||||
|
val name = generalOption.nameTextField.text
|
||||||
|
val protocol = FTPProtocolProvider.PROTOCOL
|
||||||
|
val port = generalOption.portTextField.value as Int
|
||||||
|
var authentication = Authentication.Companion.No
|
||||||
|
var proxy = Proxy.Companion.No
|
||||||
|
val authenticationType = AuthenticationType.Password
|
||||||
|
|
||||||
|
authentication = authentication.copy(
|
||||||
|
type = authenticationType,
|
||||||
|
password = String(generalOption.passwordTextField.password)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||||
|
proxy = proxy.copy(
|
||||||
|
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
|
||||||
|
host = proxyOption.proxyHostTextField.text,
|
||||||
|
username = proxyOption.proxyUsernameTextField.text,
|
||||||
|
password = String(proxyOption.proxyPasswordTextField.password),
|
||||||
|
port = proxyOption.proxyPortTextField.value as Int,
|
||||||
|
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val options = Options.Default.copy(
|
||||||
|
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||||
|
encoding = sftpOption.charsetComboBox.selectedItem as String,
|
||||||
|
extras = mutableMapOf("passive" to (sftpOption.passiveComboBox.selectedItem as PassiveMode).name)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Host(
|
||||||
|
name = name,
|
||||||
|
protocol = protocol,
|
||||||
|
port = port,
|
||||||
|
host = generalOption.hostTextField.text,
|
||||||
|
username = generalOption.usernameTextField.text,
|
||||||
|
authentication = authentication,
|
||||||
|
proxy = proxy,
|
||||||
|
sort = System.currentTimeMillis(),
|
||||||
|
remark = generalOption.remarkTextArea.text,
|
||||||
|
options = options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHost(host: Host) {
|
||||||
|
generalOption.nameTextField.text = host.name
|
||||||
|
generalOption.usernameTextField.text = host.username
|
||||||
|
generalOption.remarkTextArea.text = host.remark
|
||||||
|
generalOption.passwordTextField.text = host.authentication.password
|
||||||
|
generalOption.hostTextField.text = host.host
|
||||||
|
generalOption.portTextField.value = host.port
|
||||||
|
|
||||||
|
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||||
|
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||||
|
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||||
|
proxyOption.proxyUsernameTextField.text = host.proxy.username
|
||||||
|
proxyOption.proxyPortTextField.value = host.proxy.port
|
||||||
|
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
|
||||||
|
|
||||||
|
|
||||||
|
val passive = host.options.extras["passive"] ?: PassiveMode.Local.name
|
||||||
|
sftpOption.charsetComboBox.selectedItem = host.options.encoding
|
||||||
|
sftpOption.passiveComboBox.selectedItem = runCatching { PassiveMode.valueOf(passive) }
|
||||||
|
.getOrNull() ?: PassiveMode.Local
|
||||||
|
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateFields(): Boolean {
|
||||||
|
val host = getHost()
|
||||||
|
|
||||||
|
// general
|
||||||
|
if (validateField(generalOption.nameTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (validateField(generalOption.hostTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(generalOption.usernameTextField.text) || generalOption.passwordTextField.password.isNotEmpty()) {
|
||||||
|
if (validateField(generalOption.usernameTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateField(generalOption.passwordTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxy
|
||||||
|
if (host.proxy.type != ProxyType.No) {
|
||||||
|
if (validateField(proxyOption.proxyHostTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.proxy.authenticationType != AuthenticationType.No) {
|
||||||
|
if (validateField(proxyOption.proxyUsernameTextField)
|
||||||
|
|| validateField(proxyOption.proxyPasswordTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示有错误
|
||||||
|
*/
|
||||||
|
private fun validateField(textField: JTextField): Boolean {
|
||||||
|
if (textField.isEnabled && (if (textField is JPasswordField) textField.password.isEmpty() else textField.text.isBlank())) {
|
||||||
|
setOutlineError(textField)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setOutlineError(c: JComponent) {
|
||||||
|
selectOptionJComponent(c)
|
||||||
|
c.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
|
c.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||||
|
val portTextField = PortSpinner(21)
|
||||||
|
val nameTextField = OutlineTextField(128)
|
||||||
|
val usernameTextField = OutlineTextField(128)
|
||||||
|
val hostTextField = OutlineTextField(255)
|
||||||
|
val passwordTextField = OutlinePasswordField(255)
|
||||||
|
val publicKeyComboBox = OutlineComboBox<String>()
|
||||||
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
|
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
|
||||||
|
publicKeyComboBox.isEditable = false
|
||||||
|
|
||||||
|
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = StringUtils.EMPTY
|
||||||
|
if (value is String) {
|
||||||
|
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = value?.toString() ?: ""
|
||||||
|
when (value) {
|
||||||
|
AuthenticationType.Password -> {
|
||||||
|
text = "Password"
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationType.PublicKey -> {
|
||||||
|
text = "Public Key"
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationType.KeyboardInteractive -> {
|
||||||
|
text = "Keyboard Interactive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||||
|
|
||||||
|
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentResized(e: ComponentEvent) {
|
||||||
|
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
|
||||||
|
removeComponentListener(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
authenticationTypeComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
passwordTextField.isEnabled = authenticationTypeComboBox.selectedItem == AuthenticationType.Password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.general")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
|
||||||
|
remarkTextArea.rows = 8
|
||||||
|
remarkTextArea.lineWrap = true
|
||||||
|
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
|
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
||||||
|
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||||
|
.add(hostTextField).xy(3, rows)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||||
|
.add(portTextField).xy(7, rows).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||||
|
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
|
||||||
|
.add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||||
|
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||||
|
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||||
|
.xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||||
|
val defaultDirectoryField = OutlineTextField(255)
|
||||||
|
val charsetComboBox = JComboBox<String>()
|
||||||
|
val passiveComboBox = JComboBox<PassiveMode>()
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
|
||||||
|
for (e in Charset.availableCharsets()) {
|
||||||
|
charsetComboBox.addItem(e.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
charsetComboBox.selectedItem = "UTF-8"
|
||||||
|
|
||||||
|
passiveComboBox.addItem(PassiveMode.Local)
|
||||||
|
passiveComboBox.addItem(PassiveMode.Remote)
|
||||||
|
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.folder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.transport.sftp")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
||||||
|
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${FTPI18n.getString("termora.plugins.ftp.passive")}:").xy(1, rows)
|
||||||
|
.add(passiveComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||||
|
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PassiveMode {
|
||||||
|
Local,
|
||||||
|
Remote,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.NamedI18n
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object FTPI18n : NamedI18n("i18n/messages") {
|
||||||
|
private val log = LoggerFactory.getLogger(FTPI18n::class.java)
|
||||||
|
|
||||||
|
override fun getLogger(): Logger {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(key: String): String {
|
||||||
|
return try {
|
||||||
|
substitutor.replace(getBundle().getString(key))
|
||||||
|
} catch (_: MissingResourceException) {
|
||||||
|
I18n.getString(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3Path
|
||||||
|
|
||||||
|
class FTPPath(fileSystem: FTPFileSystem, root: String?, names: List<String>) : S3Path(fileSystem, root, names) {
|
||||||
|
override val isBucket: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
|
override val bucketName: String
|
||||||
|
get() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override val objectName: String
|
||||||
|
get() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun getCustomType(): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionSupport
|
||||||
|
import app.termora.plugin.PaidPlugin
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
class FTPPlugin : PaidPlugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(ProtocolProviderExtension::class.java) { FTPProtocolProviderExtension.Companion.instance }
|
||||||
|
support.addExtension(ProtocolHostPanelExtension::class.java) { FTPProtocolHostPanelExtension.Companion.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "FTP"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.Host
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
|
||||||
|
class FTPProtocolHostPanel : ProtocolHostPanel() {
|
||||||
|
|
||||||
|
private val pane = FTPHostOptionsPane()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(pane, BorderLayout.CENTER)
|
||||||
|
Disposer.register(this, pane)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {}
|
||||||
|
|
||||||
|
override fun getHost(): Host {
|
||||||
|
return pane.getHost()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setHost(host: Host) {
|
||||||
|
pane.setHost(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun validateFields(): Boolean {
|
||||||
|
return pane.validateFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
|
||||||
|
class FTPProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = FTPProtocolHostPanelExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return FTPProtocolProvider.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
|
return FTPProtocolHostPanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.AuthenticationType
|
||||||
|
import app.termora.DynamicIcon
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.ProxyType
|
||||||
|
import app.termora.protocol.PathHandler
|
||||||
|
import app.termora.protocol.PathHandlerRequest
|
||||||
|
import app.termora.protocol.TransferProtocolProvider
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.net.ftp.FTPClient
|
||||||
|
import org.apache.commons.pool2.BasePooledObjectFactory
|
||||||
|
import org.apache.commons.pool2.PooledObject
|
||||||
|
import org.apache.commons.pool2.impl.DefaultPooledObject
|
||||||
|
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||||
|
import org.apache.commons.pool2.impl.GenericObjectPoolConfig
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
|
||||||
|
class FTPProtocolProvider private constructor() : TransferProtocolProvider {
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(FTPProtocolProvider::class.java)
|
||||||
|
|
||||||
|
val instance = FTPProtocolProvider()
|
||||||
|
const val PROTOCOL = "FTP"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocol(): String {
|
||||||
|
return PROTOCOL
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||||
|
return Icons.ftp
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
|
||||||
|
val host = requester.host
|
||||||
|
|
||||||
|
val config = GenericObjectPoolConfig<FTPClient>().apply {
|
||||||
|
maxTotal = 12
|
||||||
|
// 与 transfer 最大传输量匹配
|
||||||
|
maxIdle = 6
|
||||||
|
minIdle = 1
|
||||||
|
testOnBorrow = false
|
||||||
|
testWhileIdle = true
|
||||||
|
// 检测空闲对象线程每次运行时检测的空闲对象的数量
|
||||||
|
timeBetweenEvictionRuns = Duration.ofSeconds(30)
|
||||||
|
// 连接空闲的最小时间,达到此值后空闲链接将会被移除,且保留 minIdle 个空闲连接数
|
||||||
|
softMinEvictableIdleDuration = Duration.ofSeconds(30)
|
||||||
|
// 连接的最小空闲时间,达到此值后该空闲连接可能会被移除(还需看是否已达最大空闲连接数)
|
||||||
|
minEvictableIdleDuration = Duration.ofMinutes(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
val ftpClientPool = GenericObjectPool(object : BasePooledObjectFactory<FTPClient>() {
|
||||||
|
override fun create(): FTPClient {
|
||||||
|
val client = FTPClient()
|
||||||
|
client.charset = Charset.forName(host.options.encoding)
|
||||||
|
client.controlEncoding = client.charset.name()
|
||||||
|
client.connect(host.host, host.port)
|
||||||
|
if (client.isConnected.not()) {
|
||||||
|
throw IllegalStateException("FTP client is not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.proxy.type == ProxyType.HTTP) {
|
||||||
|
client.proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress(host.proxy.host, host.proxy.port))
|
||||||
|
} else if (host.proxy.type == ProxyType.SOCKS5) {
|
||||||
|
client.proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(host.proxy.host, host.proxy.port))
|
||||||
|
}
|
||||||
|
|
||||||
|
val password = if (host.authentication.type == AuthenticationType.Password)
|
||||||
|
host.authentication.password else StringUtils.EMPTY
|
||||||
|
if (client.login(host.username, password).not()) {
|
||||||
|
throw IllegalStateException("Incorrect account or password")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.options.extras["passive"] == FTPHostOptionsPane.PassiveMode.Remote.name) {
|
||||||
|
client.enterRemotePassiveMode()
|
||||||
|
} else {
|
||||||
|
client.enterLocalPassiveMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
client.listHiddenFiles = true
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun wrap(obj: FTPClient): PooledObject<FTPClient> {
|
||||||
|
return DefaultPooledObject(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun validateObject(p: PooledObject<FTPClient>): Boolean {
|
||||||
|
val ftp = p.`object`
|
||||||
|
return ftp.isConnected.not() && ftp.sendNoOp()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun destroyObject(p: PooledObject<FTPClient>) {
|
||||||
|
try {
|
||||||
|
p.`object`.disconnect()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, config)
|
||||||
|
|
||||||
|
val defaultPath = host.options.sftpDefaultDirectory
|
||||||
|
val fs = FTPFileSystem(ftpClientPool)
|
||||||
|
return PathHandler(fs, fs.getPath(defaultPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = FTPProtocolProviderExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return FTPProtocolProvider.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3FileSystemProvider
|
||||||
|
import app.termora.transfer.s3.S3Path
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.net.ftp.FTPClient
|
||||||
|
import org.apache.commons.net.ftp.FTPFile
|
||||||
|
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.nio.file.AccessMode
|
||||||
|
import java.nio.file.CopyOption
|
||||||
|
import java.nio.file.NoSuchFileException
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.attribute.FileAttribute
|
||||||
|
import java.nio.file.attribute.PosixFilePermission
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
|
||||||
|
class FTPSystemProvider(private val pool: GenericObjectPool<FTPClient>) : S3FileSystemProvider() {
|
||||||
|
|
||||||
|
|
||||||
|
override fun getScheme(): String? {
|
||||||
|
return "ftp"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOutputStream(path: S3Path): OutputStream {
|
||||||
|
return createStreamer(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInputStream(path: S3Path): InputStream {
|
||||||
|
val ftp = pool.borrowObject()
|
||||||
|
val fs = ftp.retrieveFileStream(path.absolutePathString())
|
||||||
|
return object : InputStream() {
|
||||||
|
override fun read(): Int {
|
||||||
|
return fs.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(fs)
|
||||||
|
ftp.completePendingCommand()
|
||||||
|
pool.returnObject(ftp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createStreamer(path: S3Path): OutputStream {
|
||||||
|
val ftp = pool.borrowObject()
|
||||||
|
val os = ftp.storeFileStream(path.absolutePathString())
|
||||||
|
return object : OutputStream() {
|
||||||
|
override fun write(b: Int) {
|
||||||
|
os.write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(os)
|
||||||
|
ftp.completePendingCommand()
|
||||||
|
pool.returnObject(ftp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
|
||||||
|
val paths = mutableListOf<S3Path>()
|
||||||
|
if (path.exists().not()) {
|
||||||
|
throw NoSuchFileException(path.absolutePathString())
|
||||||
|
}
|
||||||
|
|
||||||
|
withFtpClient {
|
||||||
|
val files = it.listFiles(path.absolutePathString())
|
||||||
|
for (file in files) {
|
||||||
|
val p = path.resolve(file.name)
|
||||||
|
p.attributes = p.attributes.copy(
|
||||||
|
directory = file.isDirectory,
|
||||||
|
regularFile = file.isFile,
|
||||||
|
size = file.size,
|
||||||
|
lastModifiedTime = file.timestamp.timeInMillis,
|
||||||
|
)
|
||||||
|
p.attributes.permissions = ftpPermissionsToPosix(file)
|
||||||
|
paths.add(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun ftpPermissionsToPosix(file: FTPFile): Set<PosixFilePermission> {
|
||||||
|
val perms = mutableSetOf<PosixFilePermission>()
|
||||||
|
|
||||||
|
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OWNER_READ)
|
||||||
|
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OWNER_WRITE)
|
||||||
|
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OWNER_EXECUTE)
|
||||||
|
|
||||||
|
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.READ_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.GROUP_READ)
|
||||||
|
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.GROUP_WRITE)
|
||||||
|
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.GROUP_EXECUTE)
|
||||||
|
|
||||||
|
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.READ_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OTHERS_READ)
|
||||||
|
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OTHERS_WRITE)
|
||||||
|
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OTHERS_EXECUTE)
|
||||||
|
|
||||||
|
return perms
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
|
||||||
|
withFtpClient { it.mkd(dir.absolutePathString()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun move(source: Path?, target: Path?, vararg options: CopyOption?) {
|
||||||
|
if (source != null && target != null) {
|
||||||
|
withFtpClient {
|
||||||
|
it.rename(source.absolutePathString(), target.absolutePathString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(path: S3Path, isDirectory: Boolean) {
|
||||||
|
withFtpClient {
|
||||||
|
if (isDirectory) {
|
||||||
|
it.rmd(path.absolutePathString())
|
||||||
|
} else {
|
||||||
|
it.deleteFile(path.absolutePathString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
|
||||||
|
withFtpClient {
|
||||||
|
if (it.cwd(path.absolutePathString()) == 250) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (it.listFiles(path.absolutePathString()).isNotEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NoSuchFileException(path.absolutePathString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T> withFtpClient(block: (FTPClient) -> T): T {
|
||||||
|
val client = pool.borrowObject()
|
||||||
|
return try {
|
||||||
|
block(client)
|
||||||
|
} finally {
|
||||||
|
pool.returnObject(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
plugins/ftp/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>ftp</id>
|
||||||
|
|
||||||
|
<name>FTP</name>
|
||||||
|
|
||||||
|
<paid/>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.ftp.FTPPlugin</entry>
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Connecting to FTP</description>
|
||||||
|
<description language="zh_CN">支持连接到 FTP</description>
|
||||||
|
<description language="zh_TW">支援連接到 FTP</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
1
plugins/ftp/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#6C707E"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#6C707E"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#CED0D6"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#CED0D6"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
1
plugins/ftp/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
termora.plugins.ftp.passive=Passive Mode
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
termora.plugins.ftp.passive=被动模式
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
termora.plugins.ftp.passive=被動模式
|
||||||
16
plugins/geo/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
project.version = "0.0.7"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
compileOnly(project(":"))
|
||||||
|
implementation("com.maxmind.geoip2:geoip2:4.3.1")
|
||||||
|
// https://github.com/hstyi/geolite2
|
||||||
|
implementation("com.github.hstyi:geolite2:v1.0-202507070058")
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
|
|
||||||
82
plugins/geo/src/main/kotlin/app/termora/plugins/geo/Geo.kt
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package app.termora.plugins.geo
|
||||||
|
|
||||||
|
import app.termora.ApplicationScope
|
||||||
|
import app.termora.Disposable
|
||||||
|
import app.termora.geo.GeoLibrary
|
||||||
|
import com.maxmind.db.CHMCache
|
||||||
|
import com.maxmind.geoip2.DatabaseReader
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
||||||
|
|
||||||
|
internal class Geo private constructor() : Disposable {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(Geo::class.java)
|
||||||
|
|
||||||
|
fun getInstance(): Geo {
|
||||||
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(Geo::class) { Geo() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val initialized = AtomicBoolean(false)
|
||||||
|
private var reader: DatabaseReader? = null
|
||||||
|
|
||||||
|
private fun initialize() {
|
||||||
|
if (isInitialized()) return
|
||||||
|
|
||||||
|
if (initialized.compareAndSet(false, true)) {
|
||||||
|
try {
|
||||||
|
val input = GeoLibrary.getInputStream()
|
||||||
|
if (input == null) {
|
||||||
|
throw IllegalStateException("GeoLite2-Country.mmdb not be found")
|
||||||
|
}
|
||||||
|
val locale = Locale.getDefault().toString().replace("_", "-")
|
||||||
|
try {
|
||||||
|
reader = DatabaseReader.Builder(input)
|
||||||
|
.locales(listOf(locale, "en"))
|
||||||
|
.withCache(CHMCache()).build()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error("Failed to initialize geo database", e)
|
||||||
|
}
|
||||||
|
initialized.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun country(ip: String): Country? {
|
||||||
|
try {
|
||||||
|
initialize()
|
||||||
|
|
||||||
|
val reader = reader ?: return null
|
||||||
|
val response = reader.tryCountry(InetAddress.getByName(ip)).getOrNull() ?: return null
|
||||||
|
val isoCode = response.country.isoCode
|
||||||
|
var name = response.country.name
|
||||||
|
// 控制名称不要太长,如果太长则使用缩写。例如:United States
|
||||||
|
if (name != null && name.length > 6) name = isoCode
|
||||||
|
return Country(isoCode, name ?: isoCode)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.error("Failed to initialize geo database", e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isInitialized(): Boolean = initialized.get()
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
IOUtils.closeQuietly(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Country(val isoCode: String, val name: String)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package app.termora.plugins.geo
|
||||||
|
|
||||||
|
import app.termora.EnableManager
|
||||||
|
import app.termora.SwingUtils
|
||||||
|
import app.termora.TermoraFrameManager
|
||||||
|
import app.termora.tree.HostTreeShowMoreEnableExtension
|
||||||
|
import app.termora.tree.NewHostTree
|
||||||
|
import javax.swing.JCheckBoxMenuItem
|
||||||
|
import javax.swing.JTree
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
internal class GeoHostTreeShowMoreEnableExtension private constructor() : HostTreeShowMoreEnableExtension {
|
||||||
|
companion object {
|
||||||
|
private const val KEY = "Plugins.Geo.ShowMore.Enable"
|
||||||
|
|
||||||
|
val instance = GeoHostTreeShowMoreEnableExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val enableManager get() = EnableManager.getInstance()
|
||||||
|
|
||||||
|
override fun createJCheckBoxMenuItem(tree: JTree): JCheckBoxMenuItem {
|
||||||
|
val item = JCheckBoxMenuItem("Geo")
|
||||||
|
item.isSelected = item.isEnabled && enableManager.getFlag(KEY, true)
|
||||||
|
item.addActionListener {
|
||||||
|
enableManager.setFlag(KEY, item.isSelected)
|
||||||
|
updateComponentTreeUI()
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateComponentTreeUI() {
|
||||||
|
// reload all tree
|
||||||
|
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
||||||
|
for (tree in SwingUtils.getDescendantsOfClass(NewHostTree::class.java, frame)) {
|
||||||
|
SwingUtilities.updateComponentTreeUI(tree)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isShowMore(): Boolean {
|
||||||
|
return enableManager.getFlag(KEY, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package app.termora.plugins.geo
|
||||||
|
|
||||||
|
import app.termora.AbstractI18n
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object GeoI18n : AbstractI18n() {
|
||||||
|
private val log = LoggerFactory.getLogger(GeoI18n::class.java)
|
||||||
|
private val myBundle by lazy {
|
||||||
|
val bundle = ResourceBundle.getBundle("i18n/messages", Locale.getDefault(), GeoI18n::class.java.classLoader)
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("I18n: {}", bundle.baseBundleName ?: "null")
|
||||||
|
}
|
||||||
|
return@lazy bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getBundle(): ResourceBundle {
|
||||||
|
return myBundle
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLogger(): Logger {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package app.termora.plugins.geo
|
||||||
|
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionSupport
|
||||||
|
import app.termora.plugin.Plugin
|
||||||
|
import app.termora.tree.HostTreeShowMoreEnableExtension
|
||||||
|
import app.termora.tree.SimpleTreeCellRendererExtension
|
||||||
|
|
||||||
|
class GeoPlugin : Plugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(SimpleTreeCellRendererExtension::class.java) { GeoSimpleTreeCellRendererExtension.instance }
|
||||||
|
support.addExtension(HostTreeShowMoreEnableExtension::class.java) { GeoHostTreeShowMoreEnableExtension.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "Geo"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package app.termora.plugins.geo
|
||||||
|
|
||||||
|
import app.termora.ColorHash
|
||||||
|
import app.termora.tree.HostTreeNode
|
||||||
|
import app.termora.tree.MarkerSimpleTreeCellAnnotation
|
||||||
|
import app.termora.tree.SimpleTreeCellAnnotation
|
||||||
|
import app.termora.tree.SimpleTreeCellRendererExtension
|
||||||
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import java.awt.Color
|
||||||
|
import javax.swing.JTree
|
||||||
|
|
||||||
|
class GeoSimpleTreeCellRendererExtension private constructor() : SimpleTreeCellRendererExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = GeoSimpleTreeCellRendererExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val geo get() = Geo.getInstance()
|
||||||
|
|
||||||
|
override fun createAnnotations(
|
||||||
|
tree: JTree,
|
||||||
|
value: Any?,
|
||||||
|
sel: Boolean,
|
||||||
|
expanded: Boolean,
|
||||||
|
leaf: Boolean,
|
||||||
|
row: Int,
|
||||||
|
hasFocus: Boolean
|
||||||
|
): List<SimpleTreeCellAnnotation> {
|
||||||
|
|
||||||
|
val node = value as? HostTreeNode ?: return emptyList()
|
||||||
|
if (node.isFolder) return emptyList()
|
||||||
|
val protocol = node.data.protocol
|
||||||
|
if ((protocol == "SSH" || protocol == "RDP").not()) return emptyList()
|
||||||
|
|
||||||
|
if (GeoHostTreeShowMoreEnableExtension.instance.isShowMore().not()) return emptyList()
|
||||||
|
val country = geo.country(node.data.host) ?: return emptyList()
|
||||||
|
|
||||||
|
val text = if (SystemInfo.isMacOS) "${countryCodeToFlagEmoji(country.isoCode)}${country.name}" else country.name
|
||||||
|
return listOf(
|
||||||
|
MarkerSimpleTreeCellAnnotation(
|
||||||
|
text,
|
||||||
|
foreground = Color.white,
|
||||||
|
background = ColorHash.hash(country.isoCode),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun countryCodeToFlagEmoji(code: String): String {
|
||||||
|
if (code.length < 2) return "❓"
|
||||||
|
val upper = code.take(2).uppercase()
|
||||||
|
val first = Character.codePointAt(upper, 0) - 'A'.code + 0x1F1E6
|
||||||
|
val second = Character.codePointAt(upper, 1) - 'A'.code + 0x1F1E6
|
||||||
|
return String(Character.toChars(first)) + String(Character.toChars(second))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ordered(): Long {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
plugins/geo/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>geo</id>
|
||||||
|
|
||||||
|
<name>Geo</name>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.geo.GeoPlugin</entry>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Display the geographical location of the host</description>
|
||||||
|
<description language="zh_CN">显示主机的地理位置</description>
|
||||||
|
<description language="zh_TW">顯示主機的地理位置</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
5
plugins/geo/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="8" cy="8" r="6.5" stroke="#6C707E"/>
|
||||||
|
<path d="M10.5 8C10.5 9.38071 9.38071 10.5 8 10.5C6.61929 10.5 5.5 9.38071 5.5 8C5.5 6.61929 6.61929 5.5 8 5.5C9.38071 5.5 10.5 6.61929 10.5 8Z" stroke="#3574F0"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 453 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="8" cy="8" r="6.5" stroke="#CED0D6"/>
|
||||||
|
<path d="M10.5 8C10.5 9.38071 9.38071 10.5 8 10.5C6.61929 10.5 5.5 9.38071 5.5 8C5.5 6.61929 6.61929 5.5 8 5.5C9.38071 5.5 10.5 6.61929 10.5 8Z" stroke="#548AF7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 453 B |
2
plugins/geo/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
termora.plugins.geo.first-message=The first time you use the <b>Geo</b> plugin, it will download the <b>GeoLite2.mmdb</b> database. <br/>Once the download is complete, it will display the host region information.
|
||||||
|
termora.plugins.geo.coming-soon=Geo loading
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
termora.plugins.geo.first-message=首次使用 <b>Geo</b> 插件会下载 <b>GeoLite2.mmdb</b> 数据库,下载完成后会显示主机地域信息
|
||||||
|
termora.plugins.geo.coming-soon=Geo 加载中
|
||||||