Compare commits
5 Commits
2.0.0-beta
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b499667cbb | ||
|
|
1d596e18df | ||
|
|
6f95033009 | ||
|
|
1f08af6575 | ||
|
|
071a091347 |
47
.github/workflows/linux-aarch64.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Linux aarch64
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# download jdk
|
||||||
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-aarch64-b1034.51.tar.gz
|
||||||
|
|
||||||
|
# appimagetool
|
||||||
|
- run: sudo apt install libfuse2
|
||||||
|
|
||||||
|
# install jdk
|
||||||
|
- name: Installing Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'jdkfile'
|
||||||
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
|
java-version: '21.0.7'
|
||||||
|
architecture: aarch64
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
# dist
|
||||||
|
- run: |
|
||||||
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-linux-aarch64
|
||||||
|
path: |
|
||||||
|
build/distributions/*.tar.gz
|
||||||
|
build/distributions/*.AppImage
|
||||||
47
.github/workflows/linux-x86-64.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Linux x86-64
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# download jdk
|
||||||
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-x64-b1034.51.tar.gz
|
||||||
|
|
||||||
|
# appimagetool
|
||||||
|
- run: sudo apt install libfuse2
|
||||||
|
|
||||||
|
# install jdk
|
||||||
|
- name: Installing Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'jdkfile'
|
||||||
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
|
java-version: '21.0.7'
|
||||||
|
architecture: x64
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
# dist
|
||||||
|
- run: |
|
||||||
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-linux-x86-64
|
||||||
|
path: |
|
||||||
|
build/distributions/*.tar.gz
|
||||||
|
build/distributions/*.AppImage
|
||||||
64
.github/workflows/linux.yml
vendored
@@ -1,64 +0,0 @@
|
|||||||
name: Linux
|
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
|
||||||
|
|
||||||
env:
|
|
||||||
DOCKER_NAME: hstyi/jbr:21.0.7b1038.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: 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
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
name: macOS aarch64
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: macos-15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install the Apple certificate
|
||||||
|
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
||||||
|
env:
|
||||||
|
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||||
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# create variables
|
||||||
|
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
|
||||||
|
# import certificate from secrets
|
||||||
|
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
|
||||||
|
|
||||||
|
# create temporary keychain
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
# import certificate to keychain
|
||||||
|
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||||
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
- name: Setup the Notary information
|
||||||
|
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
||||||
|
env:
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
|
STORE_CREDENTIALS: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||||
|
run: |
|
||||||
|
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||||
|
|
||||||
|
# download jdk
|
||||||
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-aarch64-b1034.51.tar.gz
|
||||||
|
|
||||||
|
# install jdk
|
||||||
|
- name: Installing Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'jdkfile'
|
||||||
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
|
java-version: '21.0.7'
|
||||||
|
architecture: aarch64
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
# dist
|
||||||
|
- name: Dist
|
||||||
|
env:
|
||||||
|
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
|
||||||
|
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||||
|
# 只有发布版本时才需要公证
|
||||||
|
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||||
|
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||||
|
run: |
|
||||||
|
./gradlew dist --no-daemon
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-osx-aarch64
|
||||||
|
path: |
|
||||||
|
build/distributions/*.zip
|
||||||
|
build/distributions/*.dmg
|
||||||
@@ -1,27 +1,17 @@
|
|||||||
name: macOS
|
name: macOS x86-64
|
||||||
|
|
||||||
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 }}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: macos-13
|
||||||
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: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''"
|
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
||||||
env:
|
env:
|
||||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
@@ -44,7 +34,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' && env.APPLE_ID != ''"
|
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
||||||
env:
|
env:
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
@@ -53,14 +43,8 @@ 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"
|
||||||
|
|
||||||
- name: Download Java
|
# download jdk
|
||||||
run: |
|
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-osx-x64-b1034.51.tar.gz
|
||||||
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-21.0.7-osx-$ARCH-b1034.51.tar.gz
|
|
||||||
|
|
||||||
# install jdk
|
# install jdk
|
||||||
- name: Installing Java
|
- name: Installing Java
|
||||||
@@ -69,6 +53,8 @@ jobs:
|
|||||||
distribution: 'jdkfile'
|
distribution: 'jdkfile'
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
java-version: '21.0.7'
|
java-version: '21.0.7'
|
||||||
|
architecture: x64
|
||||||
|
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -79,22 +65,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
|
|
||||||
|
|
||||||
- name: JLink
|
# dist
|
||||||
shell: bash
|
- name: Dist
|
||||||
run: ./gradlew :jar :copy-dependencies :plugins:migration:build :jlink
|
env:
|
||||||
|
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
|
||||||
- name: Package
|
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||||
shell: bash
|
# 只有发布版本时才需要公证
|
||||||
run: ./gradlew :jpackage && ./gradlew :dist
|
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
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: termora-osx-${{ runner.arch }}
|
name: termora-osx-x86-64
|
||||||
path: |
|
path: |
|
||||||
build/distributions/*.zip
|
build/distributions/*.zip
|
||||||
build/distributions/*.dmg
|
build/distributions/*.dmg
|
||||||
15
.github/workflows/windows-x86-64.yml
vendored
@@ -34,17 +34,10 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
- name: Compile
|
# dist
|
||||||
run: .\gradlew :check-license && .\gradlew classes -x test
|
- run: |
|
||||||
|
.\gradlew.bat dist --no-daemon
|
||||||
- name: JLink
|
.\gradlew.bat --stop
|
||||||
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
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
126
README.md
@@ -1,102 +1,52 @@
|
|||||||
<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 cross-platform terminal emulator and SSH client, available on **Windows, macOS, and Linux**.
|
**Termora** is a terminal emulator and SSH client for Windows, macOS and Linux.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="docs/readme.png" alt="Readme" />
|
<img src="./docs/readme.png" alt="termora" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
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).
|
**Termora** is developed using [Kotlin/JVM](https://kotlinlang.org) and partially implements the [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) protocol (with ongoing improvements). Its ultimate vision is to achieve full platform support (including Android, iOS, and iPadOS) through [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- SSH and local terminal support
|
||||||
|
- Serial port protocol support
|
||||||
|
- [SFTP](./docs/sftp.png?raw=1) & [Command](./docs/sftp-command.png?raw=1) file transfer support
|
||||||
|
- Compatible with Windows, macOS, and Linux
|
||||||
|
- Zmodem protocol support
|
||||||
|
- SSH port forwarding & Jump hosts
|
||||||
|
- Support for X11 and SSH-Agent
|
||||||
|
- Terminal log
|
||||||
|
- Configuration synchronization via [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
||||||
|
- Macro support (record and replay scripts)
|
||||||
|
- Keyword highlighting
|
||||||
|
- Key management
|
||||||
|
- Broadcast commands to multiple sessions
|
||||||
|
- [Find Everywhere](./docs/findeverywhere.png?raw=1) quick navigation
|
||||||
|
- Data encryption
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## Download
|
||||||
|
|
||||||
|
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||||
|
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
||||||
|
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
It is recommended to use the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) version of the JDK and run the program via `./gradlew :run` to run the program.
|
||||||
|
|
||||||
|
The program can be run via `./gradlew dist` to automatically build the local version. On macOS: `dmg`, on Windows: `zip`, on Linux: `tar.gz`.
|
||||||
|
|
||||||
|
|
||||||
|
## LICENSE
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
- 🧬 Cross-platform support
|
|
||||||
- 🔐 Built-in key manager
|
|
||||||
- 🖼️ X11 forwarding
|
|
||||||
- 🧑💻 SSH-Agent integration
|
|
||||||
- 💻 System information display
|
|
||||||
- 📁 GUI-based SFTP file management
|
|
||||||
- 📊 Nvidia GPU usage monitoring
|
|
||||||
- ⚡ Quick command shortcuts
|
|
||||||
|
|
||||||
|
|
||||||
## 🚀 File Transfer
|
|
||||||
|
|
||||||
- Direct transfers between server A ↔ B
|
|
||||||
- Recursive folder support
|
|
||||||
- Up to **6 concurrent transfer tasks**
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src="docs/transfer.png" alt="Transfer" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 📝 File Editing
|
|
||||||
|
|
||||||
- Auto-upload after editing and saving
|
|
||||||
- Rename files and folders
|
|
||||||
- Quick deletion of large folders (`rm -rf` supported)
|
|
||||||
- Visual permission editing
|
|
||||||
- Create new files and folders
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src="docs/transfer-edit.png" alt="Transfer Edit" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## 💻 Hosts
|
|
||||||
|
|
||||||
- Tree-like hierarchical structure, similar to folders
|
|
||||||
- Assign tags to individual hosts
|
|
||||||
- Import hosts from other tools
|
|
||||||
- Open with the transfer tool
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src="docs/host.png" alt="Transfer Edit" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## 🧩 Plugins
|
|
||||||
|
|
||||||
- 🌍 Geo: Display geolocation of hosts
|
|
||||||
- 🔄 Sync: Sync settings to Gist or WebDAV
|
|
||||||
- 🗂️ WebDAV: Connect to WebDAV storage
|
|
||||||
- 📝 Editor: Built-in SFTP file editor
|
|
||||||
- 📡 SMB: Connect to [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
|
|
||||||
- ☁️ S3: Connect to S3 object storage
|
|
||||||
- ☁️ Huawei OBS: Connect to Huawei Cloud OBS
|
|
||||||
- ☁️ Tencent COS: Connect to Tencent Cloud COS
|
|
||||||
- ☁️ Alibaba OSS: Connect to Alibaba Cloud OSS
|
|
||||||
- 👉 [View all plugins...](https://www.termora.app/plugins)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 📦 Download
|
|
||||||
|
|
||||||
- 🧾 [Latest Release](https://github.com/TermoraDev/termora/releases/latest)
|
|
||||||
- 🍺 **Homebrew**: `brew install --cask termora`
|
|
||||||
- 🔨 **WinGet**: `winget install termora`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 🛠️ Development
|
|
||||||
|
|
||||||
We recommend using the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK for development.
|
|
||||||
|
|
||||||
- Run locally: `./gradlew :run`
|
|
||||||
- Build for current OS: `./gradlew :dist`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
This software is distributed under a dual-license model. You may choose one of the following options:
|
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.
|
||||||
|
|||||||
115
README.zh_CN.md
@@ -1,100 +1,47 @@
|
|||||||
# 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="Readme" />
|
<img src="./docs/readme-zh_CN.png" alt="termora" />
|
||||||
</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`
|
||||||
- 🖼️ 支持 X11 转发
|
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
||||||
- 🧑💻 SSH-Agent 集成
|
|
||||||
- 💻 系统信息展示
|
|
||||||
- 📁 图形化 SFTP 文件管理
|
|
||||||
- 📊 Nvidia 显卡使用率查看
|
|
||||||
- ⚡ 快捷指令支持
|
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
## 🚀 文件传输
|
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) 的 JDK 版本,通过 `./gradlew :run` 即可运行程序。
|
||||||
|
|
||||||
- 支持 A ↔ B 服务器间直接传输
|
通过 `./gradlew dist` 可以自动构建适用于本机的版本。在 macOS 上是:`dmg`,在 Windows 上是:`zip`,在 Linux 上是:`tar.gz`。
|
||||||
- 文件夹递归复制支持
|
|
||||||
- 最多可同时运行 **6 个传输任务**
|
|
||||||
|
|
||||||
<div align="center">
|
## 协议
|
||||||
<img src="docs/transfer-zh_CN.png" alt="Transfer" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
本软件采用双重许可模式,您可以选择以下任意一种许可方式:
|
||||||
|
|
||||||
## 📝 文件编辑功能
|
- AGPL-3.0:根据 [AGPL-3.0](https://opensource.org/license/agpl-v3) 的条款,您可以自由使用、分发和修改本软件。
|
||||||
|
- 专有许可:如果希望在闭源或专有环境中使用,请联系作者获取许可。
|
||||||
- 保存后自动上传修改内容
|
|
||||||
- 文件 / 文件夹 重命名
|
|
||||||
- 快速删除大文件夹:`rm -rf` 支持
|
|
||||||
- 可视化更改权限
|
|
||||||
- 支持新建文件 / 文件夹
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src="docs/transfer-edit-zh_CN.png" alt="Transfer Edit" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 💻 主机
|
|
||||||
|
|
||||||
- 类似文件夹树形结构
|
|
||||||
- 给主机添加标签
|
|
||||||
- 从其它软件导入
|
|
||||||
- 使用传输工具打开
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src="docs/host-zh_CN.png" alt="Transfer Edit" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
## 🧩 插件
|
|
||||||
|
|
||||||
- 🌍 Geo:显示主机位置信息
|
|
||||||
- 🔄 Sync:将配置同步至 Gist 或 WebDAV
|
|
||||||
- 🗂️ WebDAV:连接 WebDAV 对象存储
|
|
||||||
- 📝 Editor:内置 SFTP 文件编辑器
|
|
||||||
- 📡 SMB: 连接 [SMB](https://baike.baidu.com/item/smb/4750512) 文件共享协议
|
|
||||||
- ☁️ S3:连接 S3 对象存储
|
|
||||||
- ☁️ Huawei OBS:连接华为云对象存储
|
|
||||||
- ☁️ Tencent COS:连接腾讯云 COS
|
|
||||||
- ☁️ Alibaba OSS:连接阿里云 OSS
|
|
||||||
- 👉 [查看所有插件...](https://www.termora.cn/plugins)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 📦 下载
|
|
||||||
|
|
||||||
- 🧾 [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
|
||||||
- 🍺 **Homebrew**:`brew install --cask termora`
|
|
||||||
- 🪟 **WinGet**:`winget install termora`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 🛠️ 开发指南
|
|
||||||
|
|
||||||
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK 运行环境。
|
|
||||||
|
|
||||||
- 本地运行:`./gradlew :run`
|
|
||||||
- 构建当前系统安装包:`./gradlew :dist`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 📄 授权协议
|
|
||||||
|
|
||||||
Termora 采用双重许可方式,您可以选择:
|
|
||||||
|
|
||||||
- **AGPL-3.0**:自由使用、修改、分发(遵循 [AGPL 条款](https://opensource.org/license/agpl-v3))
|
|
||||||
- **专有许可**:如需闭源或商业用途,请联系作者获取授权
|
|
||||||
|
|||||||
60
THIRDPARTY
@@ -2,6 +2,10 @@ 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
|
||||||
@@ -14,6 +18,10 @@ 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
|
||||||
@@ -118,10 +126,6 @@ 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
|
||||||
@@ -222,6 +226,26 @@ 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
|
||||||
@@ -237,31 +261,3 @@ 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
|
|
||||||
245
build.gradle.kts
@@ -5,10 +5,8 @@ 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
|
||||||
|
|
||||||
@@ -23,12 +21,10 @@ plugins {
|
|||||||
|
|
||||||
|
|
||||||
group = "app.termora"
|
group = "app.termora"
|
||||||
version = rootProject.projectDir.resolve("VERSION").readText().trim()
|
version = "1.0.17"
|
||||||
|
|
||||||
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
|
||||||
@@ -40,16 +36,15 @@ 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()
|
||||||
|
|
||||||
allprojects {
|
repositories {
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies")
|
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies")
|
||||||
maven("https://www.jitpack.io")
|
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)
|
||||||
@@ -59,10 +54,9 @@ 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)
|
|
||||||
|
|
||||||
api(kotlin("reflect"))
|
// implementation(platform(libs.koin.bom))
|
||||||
|
// implementation(libs.koin.core)
|
||||||
api(libs.slf4j.api)
|
api(libs.slf4j.api)
|
||||||
api(libs.pty4j)
|
api(libs.pty4j)
|
||||||
api(libs.slf4j.tinylog)
|
api(libs.slf4j.tinylog)
|
||||||
@@ -73,12 +67,28 @@ dependencies {
|
|||||||
api(libs.commons.csv)
|
api(libs.commons.csv)
|
||||||
api(libs.commons.net)
|
api(libs.commons.net)
|
||||||
api(libs.commons.text)
|
api(libs.commons.text)
|
||||||
|
api(libs.commons.compress)
|
||||||
|
api(libs.commons.vfs2) { exclude(group = "*", module = "*") }
|
||||||
api(libs.kotlinx.coroutines.swing)
|
api(libs.kotlinx.coroutines.swing)
|
||||||
api(libs.kotlinx.coroutines.core)
|
api(libs.kotlinx.coroutines.core)
|
||||||
|
|
||||||
api(libs.flatlaf)
|
api(libs.flatlaf) {
|
||||||
api(libs.flatlafextras)
|
artifact {
|
||||||
api(libs.flatlafswingx)
|
if (useNoNativesFlatLaf) {
|
||||||
|
classifier = "no-natives"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
api(libs.flatlaf.extras) {
|
||||||
|
if (useNoNativesFlatLaf) {
|
||||||
|
exclude(group = "com.formdev", module = "flatlaf")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
api(libs.flatlaf.swingx) {
|
||||||
|
if (useNoNativesFlatLaf) {
|
||||||
|
exclude(group = "com.formdev", module = "flatlaf")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
api(libs.kotlinx.serialization.json)
|
api(libs.kotlinx.serialization.json)
|
||||||
api(libs.swingx)
|
api(libs.swingx)
|
||||||
@@ -99,26 +109,24 @@ dependencies {
|
|||||||
api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
||||||
api(libs.eddsa)
|
api(libs.eddsa)
|
||||||
api(libs.jnafilechooser)
|
api(libs.jnafilechooser)
|
||||||
|
api(libs.xodus.vfs)
|
||||||
|
api(libs.xodus.openAPI)
|
||||||
|
api(libs.xodus.environment)
|
||||||
|
api(libs.bip39)
|
||||||
api(libs.colorpicker)
|
api(libs.colorpicker)
|
||||||
api(libs.mixpanel)
|
api(libs.mixpanel)
|
||||||
|
api(libs.jSerialComm)
|
||||||
api(libs.ini4j)
|
api(libs.ini4j)
|
||||||
api(libs.restart4j)
|
api(libs.restart4j)
|
||||||
api(libs.exposed.core)
|
|
||||||
api(libs.exposed.crypt)
|
|
||||||
api(libs.exposed.jdbc)
|
|
||||||
api(libs.sqlite)
|
|
||||||
api(libs.jug)
|
|
||||||
api(libs.semver4j)
|
|
||||||
api(libs.jsvg)
|
|
||||||
api(libs.dom4j) { exclude(group = "*", module = "*") }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
val args = mutableListOf(
|
val args = mutableListOf(
|
||||||
"-Xmx2048m",
|
"-Xmx2g",
|
||||||
"-Drelease-date=${DateFormatUtils.format(Date(), "yyyy-MM-dd")}",
|
"-XX:+UseZGC",
|
||||||
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
|
"-XX:+ZUncommit",
|
||||||
|
"-XX:+ZGenerational",
|
||||||
|
"-XX:ZUncommitDelay=60",
|
||||||
)
|
)
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
@@ -131,7 +139,7 @@ application {
|
|||||||
args.add("-Dapple.awt.application.appearance=system")
|
args.add("-Dapple.awt.application.appearance=system")
|
||||||
}
|
}
|
||||||
|
|
||||||
args.add("-DTERMORA_PLUGIN_DIRECTORY=${layout.buildDirectory.get().asFile.absolutePath}${File.separator}plugins")
|
args.add("-Dapp-version=${project.version}")
|
||||||
|
|
||||||
applicationDefaultJvmArgs = args
|
applicationDefaultJvmArgs = args
|
||||||
mainClass = "app.termora.MainKt"
|
mainClass = "app.termora.MainKt"
|
||||||
@@ -141,7 +149,6 @@ publishing {
|
|||||||
publications {
|
publications {
|
||||||
create<MavenPublication>("mavenJava") {
|
create<MavenPublication>("mavenJava") {
|
||||||
from(components["java"])
|
from(components["java"])
|
||||||
|
|
||||||
pom {
|
pom {
|
||||||
name = project.name
|
name = project.name
|
||||||
description = "Termora is a terminal emulator and SSH client for Windows, macOS and Linux"
|
description = "Termora is a terminal emulator and SSH client for Windows, macOS and Linux"
|
||||||
@@ -178,14 +185,12 @@ 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) {
|
if (os.isMacOsX && macOSSign) {
|
||||||
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
|
||||||
@@ -245,22 +250,6 @@ 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/*") }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,48 +326,6 @@ 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*") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,7 +339,6 @@ 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",
|
||||||
@@ -418,38 +364,34 @@ tasks.register<Exec>("jpackage") {
|
|||||||
|
|
||||||
val buildDir = layout.buildDirectory.get()
|
val buildDir = layout.buildDirectory.get()
|
||||||
val options = mutableListOf(
|
val options = mutableListOf(
|
||||||
"-Xmx2048m",
|
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
||||||
|
"-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")
|
options.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (os.isLinux) {
|
|
||||||
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", appVersion))
|
arguments.addAll(listOf("--app-version", "${project.version}"))
|
||||||
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"))
|
||||||
@@ -458,7 +400,6 @@ 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(
|
||||||
@@ -498,11 +439,7 @@ tasks.register<Exec>("jpackage") {
|
|||||||
} else if (os.isWindows) {
|
} else if (os.isWindows) {
|
||||||
arguments.add("msi")
|
arguments.add("msi")
|
||||||
} else if (os.isLinux) {
|
} else if (os.isLinux) {
|
||||||
arguments.add(if (isDeb) "deb" else "app-image")
|
arguments.add("app-image")
|
||||||
if (isDeb) {
|
|
||||||
arguments.add("--linux-deb-maintainer")
|
|
||||||
arguments.add("support@termora.app")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
@@ -519,20 +456,29 @@ 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()
|
|
||||||
|
|
||||||
if (os.isWindows) {
|
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
|
||||||
packOnWindows(distributionDir, finalFilenameWithoutExtension, projectName)
|
|
||||||
} else if (os.isLinux) {
|
// 清空目录
|
||||||
packOnLinux(distributionDir, finalFilenameWithoutExtension, projectName)
|
exec { commandLine(gradlew, "clean") }
|
||||||
} else if (os.isMacOsX) {
|
|
||||||
packOnMac(distributionDir, finalFilenameWithoutExtension, projectName)
|
// 打包并复制依赖
|
||||||
} else {
|
exec {
|
||||||
throw GradleException("${os.name} is not supported")
|
commandLine(gradlew, "jar", "copy-dependencies")
|
||||||
|
environment("ENABLE_BUILD" to true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查依赖的开源协议
|
||||||
|
exec { commandLine(gradlew, "check-license") }
|
||||||
|
|
||||||
|
// jlink
|
||||||
|
exec { commandLine(gradlew, "jlink") }
|
||||||
|
|
||||||
|
// 打包
|
||||||
|
exec { commandLine(gradlew, "jpackage") }
|
||||||
|
|
||||||
|
// 根据不同的系统构建不同的二进制包
|
||||||
|
pack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,34 +509,48 @@ 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、msi
|
* 创建 zip、7z、msi
|
||||||
*/
|
*/
|
||||||
fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
||||||
val dir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
|
|
||||||
val cfg = FileUtils.getFile(dir, projectName, "app", "${projectName}.cfg")
|
|
||||||
val configText = cfg.readText()
|
|
||||||
|
|
||||||
// zip
|
// 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 = dir
|
workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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=${appVersion}",
|
"/DMyAppVersion=${project.version}",
|
||||||
"/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=${layout.buildDirectory.dir("jpackage/images/win-msi.image/${projectName}").get().asFile}",
|
||||||
@@ -603,7 +563,7 @@ fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: Str
|
|||||||
exec {
|
exec {
|
||||||
commandLine(
|
commandLine(
|
||||||
"cmd", "/c", "move",
|
"cmd", "/c", "move",
|
||||||
"${projectName}-${appVersion}.msi",
|
"${projectName}-${project.version}.msi",
|
||||||
"${finalFilenameWithoutExtension}.msi"
|
"${finalFilenameWithoutExtension}.msi"
|
||||||
)
|
)
|
||||||
workingDir = distributionDir.asFile
|
workingDir = distributionDir.asFile
|
||||||
@@ -619,11 +579,11 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
|
|||||||
|
|
||||||
// rename
|
// rename
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
exec { commandLine("mv", distributionDir.file("${projectName}-${appVersion}.dmg").asFile.absolutePath, dmgFile.absolutePath,) }
|
exec { commandLine("mv", distributionDir.file("${projectName}-${project.version}.dmg").asFile.absolutePath, dmgFile.absolutePath,) }
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
// sign dmg
|
// sign dmg
|
||||||
signMacOSLocalFile(dmgFile)
|
if (macOSSign) signMacOSLocalFile(dmgFile)
|
||||||
|
|
||||||
// 找到 .app
|
// 找到 .app
|
||||||
val imageFile = layout.buildDirectory.dir("jpackage/images/").get().asFile
|
val imageFile = layout.buildDirectory.dir("jpackage/images/").get().asFile
|
||||||
@@ -636,7 +596,7 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
|
|||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
// sign zip
|
// sign zip
|
||||||
signMacOSLocalFile(zipFile)
|
if (macOSSign) signMacOSLocalFile(zipFile)
|
||||||
|
|
||||||
// 公证
|
// 公证
|
||||||
if (macOSNotary) {
|
if (macOSNotary) {
|
||||||
@@ -680,19 +640,7 @@ 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",
|
||||||
@@ -747,7 +695,6 @@ 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
|
||||||
@@ -814,10 +761,6 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
|
||||||
withSourcesJar()
|
|
||||||
}
|
|
||||||
|
|
||||||
idea {
|
idea {
|
||||||
module {
|
module {
|
||||||
isDownloadJavadoc = true
|
isDownloadJavadoc = true
|
||||||
|
|||||||
BIN
docs/findeverywhere-zh_CN.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
docs/findeverywhere.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 42 KiB |
BIN
docs/host.png
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 57 KiB |
BIN
docs/plugins.png
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 91 KiB |
BIN
docs/readme.png
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 96 KiB |
BIN
docs/sftp-command.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
docs/sftp-zh_CN.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
docs/sftp-zh_TW.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
docs/sftp.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 42 KiB |
BIN
docs/tags.png
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 142 KiB |
@@ -1,18 +1,18 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.2.0"
|
kotlin = "2.1.21"
|
||||||
slf4j = "2.0.17"
|
slf4j = "2.0.17"
|
||||||
pty4j = "0.13.6"
|
pty4j = "0.13.6"
|
||||||
tinylog = "2.7.0"
|
tinylog = "2.7.0"
|
||||||
kotlinx-coroutines = "1.10.2"
|
kotlinx-coroutines = "1.10.2"
|
||||||
flatlaf = "3.6.1-SNAPSHOT"
|
flatlaf = "3.6"
|
||||||
kotlinx-serialization-json = "1.9.0"
|
kotlinx-serialization-json = "1.8.1"
|
||||||
commons-codec = "1.18.0"
|
commons-codec = "1.18.0"
|
||||||
commons-lang3 = "3.18.0"
|
commons-lang3 = "3.17.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"
|
||||||
@@ -22,32 +22,25 @@ jna = "5.17.0"
|
|||||||
jSystemThemeDetector = "3.9.1"
|
jSystemThemeDetector = "3.9.1"
|
||||||
commons-io = "2.19.0"
|
commons-io = "2.19.0"
|
||||||
jbr-api = "17.1.10.1"
|
jbr-api = "17.1.10.1"
|
||||||
hutool = "5.8.39"
|
hutool = "5.8.37"
|
||||||
jsch = "2.27.2"
|
jsch = "0.2.26"
|
||||||
okhttp = "5.1.0"
|
okhttp = "4.12.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.25.0"
|
commonmark = "0.24.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.21.3"
|
testcontainers = "1.21.1"
|
||||||
mixpanel = "1.5.3"
|
mixpanel = "1.5.3"
|
||||||
jSerialComm = "2.11.2"
|
jSerialComm = "2.11.0"
|
||||||
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" }
|
||||||
@@ -66,11 +59,9 @@ 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" }
|
||||||
flatlafextras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" }
|
flatlaf-extras = { 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" }
|
||||||
@@ -82,6 +73,7 @@ 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" }
|
||||||
@@ -103,16 +95,6 @@ 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\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.10.2-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
plugins {
|
|
||||||
alias(libs.plugins.kotlin.jvm)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
project.version = "0.0.5"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
testImplementation(kotlin("test"))
|
|
||||||
compileOnly(project(":"))
|
|
||||||
}
|
|
||||||
|
|
||||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<!-- 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>
|
|
||||||
|
Before Width: | Height: | Size: 472 B |
@@ -1,6 +0,0 @@
|
|||||||
<!-- 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>
|
|
||||||
|
Before Width: | Height: | Size: 472 B |
@@ -1,2 +0,0 @@
|
|||||||
termora.plugins.bg.interval=Interval
|
|
||||||
termora.plugins.bg.background-image=Background Image
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
termora.plugins.bg.background-image=背景图
|
|
||||||
termora.plugins.bg.interval=切换间隔
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
termora.plugins.bg.background-image=背景圖
|
|
||||||
termora.plugins.bg.interval=切換間隔
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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")
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
@@ -1,19 +0,0 @@
|
|||||||
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")
|
|
||||||
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,334 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<!-- 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>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,6 +0,0 @@
|
|||||||
<!-- 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>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,107 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
plugins {
|
|
||||||
alias(libs.plugins.kotlin.jvm)
|
|
||||||
}
|
|
||||||
|
|
||||||
project.version = "0.0.1"
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
testImplementation(kotlin("test"))
|
|
||||||
compileOnly(project(":"))
|
|
||||||
implementation("org.apache.commons:commons-pool2:2.12.1")
|
|
||||||
testImplementation(project(":"))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,386 +0,0 @@
|
|||||||
package app.termora.plugins.ftp
|
|
||||||
|
|
||||||
import app.termora.*
|
|
||||||
import app.termora.keymgr.KeyManager
|
|
||||||
import app.termora.plugin.internal.BasicProxyOption
|
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
|
||||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
|
||||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import java.awt.BorderLayout
|
|
||||||
import java.awt.Component
|
|
||||||
import java.awt.KeyboardFocusManager
|
|
||||||
import java.awt.event.ComponentAdapter
|
|
||||||
import java.awt.event.ComponentEvent
|
|
||||||
import java.nio.charset.Charset
|
|
||||||
import javax.swing.*
|
|
||||||
|
|
||||||
class FTPHostOptionsPane : OptionsPane() {
|
|
||||||
private val generalOption = GeneralOption()
|
|
||||||
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
|
|
||||||
private val sftpOption = SFTPOption()
|
|
||||||
|
|
||||||
init {
|
|
||||||
addOption(generalOption)
|
|
||||||
addOption(proxyOption)
|
|
||||||
addOption(sftpOption)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getHost(): Host {
|
|
||||||
val name = generalOption.nameTextField.text
|
|
||||||
val protocol = FTPProtocolProvider.PROTOCOL
|
|
||||||
val port = generalOption.portTextField.value as Int
|
|
||||||
var authentication = Authentication.Companion.No
|
|
||||||
var proxy = Proxy.Companion.No
|
|
||||||
val authenticationType = AuthenticationType.Password
|
|
||||||
|
|
||||||
authentication = authentication.copy(
|
|
||||||
type = authenticationType,
|
|
||||||
password = String(generalOption.passwordTextField.password)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
|
||||||
proxy = proxy.copy(
|
|
||||||
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
|
|
||||||
host = proxyOption.proxyHostTextField.text,
|
|
||||||
username = proxyOption.proxyUsernameTextField.text,
|
|
||||||
password = String(proxyOption.proxyPasswordTextField.password),
|
|
||||||
port = proxyOption.proxyPortTextField.value as Int,
|
|
||||||
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val options = Options.Default.copy(
|
|
||||||
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
|
||||||
encoding = sftpOption.charsetComboBox.selectedItem as String,
|
|
||||||
extras = mutableMapOf("passive" to (sftpOption.passiveComboBox.selectedItem as PassiveMode).name)
|
|
||||||
)
|
|
||||||
|
|
||||||
return Host(
|
|
||||||
name = name,
|
|
||||||
protocol = protocol,
|
|
||||||
port = port,
|
|
||||||
host = generalOption.hostTextField.text,
|
|
||||||
username = generalOption.usernameTextField.text,
|
|
||||||
authentication = authentication,
|
|
||||||
proxy = proxy,
|
|
||||||
sort = System.currentTimeMillis(),
|
|
||||||
remark = generalOption.remarkTextArea.text,
|
|
||||||
options = options,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setHost(host: Host) {
|
|
||||||
generalOption.nameTextField.text = host.name
|
|
||||||
generalOption.usernameTextField.text = host.username
|
|
||||||
generalOption.remarkTextArea.text = host.remark
|
|
||||||
generalOption.passwordTextField.text = host.authentication.password
|
|
||||||
generalOption.hostTextField.text = host.host
|
|
||||||
generalOption.portTextField.value = host.port
|
|
||||||
|
|
||||||
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
|
||||||
proxyOption.proxyHostTextField.text = host.proxy.host
|
|
||||||
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
|
||||||
proxyOption.proxyUsernameTextField.text = host.proxy.username
|
|
||||||
proxyOption.proxyPortTextField.value = host.proxy.port
|
|
||||||
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
|
|
||||||
|
|
||||||
|
|
||||||
val passive = host.options.extras["passive"] ?: PassiveMode.Local.name
|
|
||||||
sftpOption.charsetComboBox.selectedItem = host.options.encoding
|
|
||||||
sftpOption.passiveComboBox.selectedItem = runCatching { PassiveMode.valueOf(passive) }
|
|
||||||
.getOrNull() ?: PassiveMode.Local
|
|
||||||
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
|
||||||
}
|
|
||||||
|
|
||||||
fun validateFields(): Boolean {
|
|
||||||
val host = getHost()
|
|
||||||
|
|
||||||
// general
|
|
||||||
if (validateField(generalOption.nameTextField)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (validateField(generalOption.hostTextField)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(generalOption.usernameTextField.text) || generalOption.passwordTextField.password.isNotEmpty()) {
|
|
||||||
if (validateField(generalOption.usernameTextField)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validateField(generalOption.passwordTextField)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// proxy
|
|
||||||
if (host.proxy.type != ProxyType.No) {
|
|
||||||
if (validateField(proxyOption.proxyHostTextField)
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (host.proxy.authenticationType != AuthenticationType.No) {
|
|
||||||
if (validateField(proxyOption.proxyUsernameTextField)
|
|
||||||
|| validateField(proxyOption.proxyPasswordTextField)
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 返回 true 表示有错误
|
|
||||||
*/
|
|
||||||
private fun validateField(textField: JTextField): Boolean {
|
|
||||||
if (textField.isEnabled && (if (textField is JPasswordField) textField.password.isEmpty() else textField.text.isBlank())) {
|
|
||||||
setOutlineError(textField)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setOutlineError(c: JComponent) {
|
|
||||||
selectOptionJComponent(c)
|
|
||||||
c.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
|
||||||
c.requestFocusInWindow()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
inner class GeneralOption : JPanel(BorderLayout()), Option {
|
|
||||||
val portTextField = PortSpinner(21)
|
|
||||||
val nameTextField = OutlineTextField(128)
|
|
||||||
val usernameTextField = OutlineTextField(128)
|
|
||||||
val hostTextField = OutlineTextField(255)
|
|
||||||
val passwordTextField = OutlinePasswordField(255)
|
|
||||||
val publicKeyComboBox = OutlineComboBox<String>()
|
|
||||||
val remarkTextArea = FixedLengthTextArea(512)
|
|
||||||
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
initView()
|
|
||||||
initEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
add(getCenterComponent(), BorderLayout.CENTER)
|
|
||||||
|
|
||||||
publicKeyComboBox.isEditable = false
|
|
||||||
|
|
||||||
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
|
||||||
override fun getListCellRendererComponent(
|
|
||||||
list: JList<*>?,
|
|
||||||
value: Any?,
|
|
||||||
index: Int,
|
|
||||||
isSelected: Boolean,
|
|
||||||
cellHasFocus: Boolean
|
|
||||||
): Component {
|
|
||||||
var text = StringUtils.EMPTY
|
|
||||||
if (value is String) {
|
|
||||||
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
|
|
||||||
}
|
|
||||||
return super.getListCellRendererComponent(
|
|
||||||
list,
|
|
||||||
text,
|
|
||||||
index,
|
|
||||||
isSelected,
|
|
||||||
cellHasFocus
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
|
||||||
override fun getListCellRendererComponent(
|
|
||||||
list: JList<*>?,
|
|
||||||
value: Any?,
|
|
||||||
index: Int,
|
|
||||||
isSelected: Boolean,
|
|
||||||
cellHasFocus: Boolean
|
|
||||||
): Component {
|
|
||||||
var text = value?.toString() ?: ""
|
|
||||||
when (value) {
|
|
||||||
AuthenticationType.Password -> {
|
|
||||||
text = "Password"
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthenticationType.PublicKey -> {
|
|
||||||
text = "Public Key"
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthenticationType.KeyboardInteractive -> {
|
|
||||||
text = "Keyboard Interactive"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.getListCellRendererComponent(
|
|
||||||
list,
|
|
||||||
text,
|
|
||||||
index,
|
|
||||||
isSelected,
|
|
||||||
cellHasFocus
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
|
||||||
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
|
||||||
|
|
||||||
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initEvents() {
|
|
||||||
addComponentListener(object : ComponentAdapter() {
|
|
||||||
override fun componentResized(e: ComponentEvent) {
|
|
||||||
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
|
|
||||||
removeComponentListener(this)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getIcon(isSelected: Boolean): Icon {
|
|
||||||
return Icons.settings
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getTitle(): String {
|
|
||||||
return I18n.getString("termora.new-host.general")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getJComponent(): JComponent {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCenterComponent(): JComponent {
|
|
||||||
val layout = FormLayout(
|
|
||||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
|
|
||||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
|
||||||
)
|
|
||||||
remarkTextArea.setFocusTraversalKeys(
|
|
||||||
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
|
||||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
|
||||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
|
||||||
)
|
|
||||||
remarkTextArea.setFocusTraversalKeys(
|
|
||||||
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
|
||||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
|
||||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
|
||||||
)
|
|
||||||
|
|
||||||
remarkTextArea.rows = 8
|
|
||||||
remarkTextArea.lineWrap = true
|
|
||||||
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
|
||||||
|
|
||||||
|
|
||||||
var rows = 1
|
|
||||||
val step = 2
|
|
||||||
val panel = FormBuilder.create().layout(layout)
|
|
||||||
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
|
||||||
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
|
||||||
|
|
||||||
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
|
||||||
.add(hostTextField).xy(3, rows)
|
|
||||||
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
|
||||||
.add(portTextField).xy(7, rows).apply { rows += step }
|
|
||||||
|
|
||||||
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
|
||||||
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
|
||||||
|
|
||||||
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
|
|
||||||
.add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
|
||||||
|
|
||||||
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
|
||||||
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
|
||||||
|
|
||||||
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
|
||||||
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
|
||||||
.xyw(3, rows, 5).apply { rows += step }
|
|
||||||
|
|
||||||
.build()
|
|
||||||
|
|
||||||
|
|
||||||
return panel
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private inner class SFTPOption : JPanel(BorderLayout()), Option {
|
|
||||||
val defaultDirectoryField = OutlineTextField(255)
|
|
||||||
val charsetComboBox = JComboBox<String>()
|
|
||||||
val passiveComboBox = JComboBox<PassiveMode>()
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
|
||||||
initView()
|
|
||||||
initEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
|
|
||||||
for (e in Charset.availableCharsets()) {
|
|
||||||
charsetComboBox.addItem(e.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
charsetComboBox.selectedItem = "UTF-8"
|
|
||||||
|
|
||||||
passiveComboBox.addItem(PassiveMode.Local)
|
|
||||||
passiveComboBox.addItem(PassiveMode.Remote)
|
|
||||||
|
|
||||||
add(getCenterComponent(), BorderLayout.CENTER)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initEvents() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun getIcon(isSelected: Boolean): Icon {
|
|
||||||
return Icons.folder
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getTitle(): String {
|
|
||||||
return I18n.getString("termora.transport.sftp")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getJComponent(): JComponent {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCenterComponent(): JComponent {
|
|
||||||
val layout = FormLayout(
|
|
||||||
"left:pref, $FORM_MARGIN, default:grow",
|
|
||||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
|
||||||
)
|
|
||||||
|
|
||||||
var rows = 1
|
|
||||||
val step = 2
|
|
||||||
val panel = FormBuilder.create().layout(layout)
|
|
||||||
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
|
||||||
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
|
||||||
.add("${FTPI18n.getString("termora.plugins.ftp.passive")}:").xy(1, rows)
|
|
||||||
.add(passiveComboBox).xy(3, rows).apply { rows += step }
|
|
||||||
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
|
||||||
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
|
|
||||||
.build()
|
|
||||||
|
|
||||||
|
|
||||||
return panel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class PassiveMode {
|
|
||||||
Local,
|
|
||||||
Remote,
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<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 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
|||||||
termora.plugins.ftp.passive=Passive Mode
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
termora.plugins.ftp.passive=被动模式
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
termora.plugins.ftp.passive=被動模式
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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")
|
|
||||||
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<!-- 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>
|
|
||||||
|
Before Width: | Height: | Size: 453 B |
@@ -1,5 +0,0 @@
|
|||||||
<!-- 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>
|
|
||||||
|
Before Width: | Height: | Size: 453 B |
@@ -1,2 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
termora.plugins.geo.first-message=首次使用 <b>Geo</b> 插件会下载 <b>GeoLite2.mmdb</b> 数据库,下载完成后会显示主机地域信息
|
|
||||||
termora.plugins.geo.coming-soon=Geo 加载中
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
termora.plugins.geo.first-message=首次使用 <b>Geo</b> 外掛程式會下載 <b>GeoLite2.mmdb</b> 資料庫,下載完成後會顯示主機地域訊息
|
|
||||||
termora.plugins.geo.coming-soon=Geo 加载中
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
plugins {
|
|
||||||
alias(libs.plugins.kotlin.jvm)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
project.version = "0.0.3"
|
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
testImplementation(kotlin("test"))
|
|
||||||
compileOnly(project(":"))
|
|
||||||
|
|
||||||
implementation(libs.xodus.vfs)
|
|
||||||
implementation(libs.xodus.openAPI)
|
|
||||||
implementation(libs.xodus.environment)
|
|
||||||
implementation(libs.bip39)
|
|
||||||
implementation(libs.commons.compress)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ext.set("Termora-Plugin-Entry", "app.termora.plugins.migration.MigrationPlugin")
|
|
||||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
|
||||||