Compare commits
74 Commits
2.0.0-beta
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f329ef60df | ||
|
|
8acfdb8bca | ||
|
|
a7aec52f2a | ||
|
|
7f1317a9a7 | ||
|
|
a8a1fea91b | ||
|
|
675ad4608a | ||
|
|
72ba3757e2 | ||
|
|
c58e84d2ae | ||
|
|
18a7a5059b | ||
|
|
f0102b6f13 | ||
|
|
0cf8eb3c17 | ||
|
|
c08a9f2b18 | ||
|
|
728f1f2802 | ||
|
|
7310211fba | ||
|
|
1f3267de0a | ||
|
|
8ddad59c70 | ||
|
|
9ff6d0afa1 | ||
|
|
2341b09f81 | ||
|
|
5830aa937a | ||
|
|
56a9361e86 | ||
|
|
5868aa4d2f | ||
|
|
45135b7299 | ||
|
|
a0020fede1 | ||
|
|
6f1eaab456 | ||
|
|
6173eae772 | ||
|
|
0bb366b1f7 | ||
|
|
9a4d6f7f4d | ||
|
|
a4ae11e301 | ||
|
|
5af0acb619 | ||
|
|
042434b8f8 | ||
|
|
eddc7ef0c6 | ||
|
|
c96ca2d424 | ||
|
|
45be9008fd | ||
|
|
057da4e297 | ||
|
|
e4e41667ff | ||
|
|
95ca0a4af7 | ||
|
|
702dee7983 | ||
|
|
165d544448 | ||
|
|
ecf61bedc4 | ||
|
|
66a81a5da3 | ||
|
|
574c816ebb | ||
|
|
e7cafb74e4 | ||
|
|
5050aa37f5 | ||
|
|
53d3d96a06 | ||
|
|
d40b8a4c9c | ||
|
|
728671509c | ||
|
|
b7178a30fb | ||
|
|
939d6a1fd7 | ||
|
|
2986a9cc46 | ||
|
|
f36afaf5d3 | ||
|
|
8cec835583 | ||
|
|
a32838dad6 | ||
|
|
d54671757e | ||
|
|
d1dba56bcd | ||
|
|
919c06779d | ||
|
|
1c90fb4e18 | ||
|
|
c1f1d5185e | ||
|
|
aa4863712d | ||
|
|
247640f2e5 | ||
|
|
5b1f803fa8 | ||
|
|
accf590c17 | ||
|
|
19fbeab817 | ||
|
|
a785ab4680 | ||
|
|
5ee23cb379 | ||
|
|
145d2de001 | ||
|
|
8d3f5fe622 | ||
|
|
9ce4a88041 | ||
|
|
c0ecc9fa7d | ||
|
|
cb33a4468a | ||
|
|
168c4c5c64 | ||
|
|
9916edbd13 | ||
|
|
ab6b6a2127 | ||
|
|
c45f5f4c92 | ||
|
|
92ee2d72f2 |
52
.github/workflows/linux-aarch64.yml
vendored
@@ -1,52 +0,0 @@
|
||||
name: Linux aarch64
|
||||
|
||||
on: [ push, pull_request ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-linux-aarch64-b1034.51.tar.gz
|
||||
|
||||
# appimagetool
|
||||
- run: sudo apt install libfuse2
|
||||
|
||||
# install jdk
|
||||
- name: Installing Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'jdkfile'
|
||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||
java-version: '21.0.7'
|
||||
architecture: aarch64
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||
|
||||
# test build
|
||||
- run: |
|
||||
./gradlew classes -x test --no-daemon
|
||||
./gradlew clean --no-daemon
|
||||
|
||||
# dist
|
||||
- run: |
|
||||
./gradlew dist --no-daemon
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: termora-linux-aarch64
|
||||
path: |
|
||||
build/distributions/*.tar.gz
|
||||
build/distributions/*.AppImage
|
||||
52
.github/workflows/linux-x86-64.yml
vendored
@@ -1,52 +0,0 @@
|
||||
name: Linux x86-64
|
||||
|
||||
on: [ push, pull_request ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.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-
|
||||
|
||||
# test build
|
||||
- run: |
|
||||
./gradlew classes -x test --no-daemon
|
||||
./gradlew clean --no-daemon
|
||||
|
||||
# dist
|
||||
- run: |
|
||||
./gradlew dist --no-daemon
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: termora-linux-x86-64
|
||||
path: |
|
||||
build/distributions/*.tar.gz
|
||||
build/distributions/*.AppImage
|
||||
69
.github/workflows/linux.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
name: Linux
|
||||
|
||||
on: [ push, pull_request ]
|
||||
|
||||
env:
|
||||
JBR_MAJOR: 21.0.7
|
||||
JBR_PATCH: b1038.58
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-24.04-arm, ubuntu-latest ]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-gradlexyz-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-gradlexyz-
|
||||
|
||||
- name: Set dynamic DOCKER_NAME
|
||||
run: |
|
||||
echo "DOCKER_NAME=hstyi/jbr:${{ env.JBR_MAJOR }}${{ env.JBR_PATCH }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Create docker-run.sh helper script
|
||||
shell: bash
|
||||
run: |
|
||||
cat <<'EOF' > docker-run.sh
|
||||
#!/bin/bash
|
||||
docker run --rm -v $HOME/.gradle:/root/.gradle -v "$(pwd)":/app -w /app "$@"
|
||||
EOF
|
||||
chmod +x docker-run.sh
|
||||
|
||||
- name: Compile
|
||||
shell: bash
|
||||
run: ./docker-run.sh $DOCKER_NAME bash -c './gradlew :check-license && ./gradlew classes -x test'
|
||||
|
||||
- name: JLink
|
||||
shell: bash
|
||||
run: ./docker-run.sh $DOCKER_NAME bash -c './gradlew :jar :copy-dependencies :plugins:migration:build :jlink'
|
||||
|
||||
- name: Package Deb
|
||||
shell: bash
|
||||
run: ./docker-run.sh -e TERMORA_TYPE=deb $DOCKER_NAME bash -c './gradlew :jpackage && ./gradlew :dist'
|
||||
|
||||
- name: Package AppImage
|
||||
shell: bash
|
||||
run: ./docker-run.sh --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor:unconfined $DOCKER_NAME bash -c 'rm -rf build/jpackage && ./gradlew :jpackage && ./gradlew :dist'
|
||||
|
||||
- name: Make ~/.gradle world-writable
|
||||
shell: bash
|
||||
run: sudo chmod -R 777 ~/.gradle
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: termora-linux-${{ runner.arch }}
|
||||
path: |
|
||||
build/distributions/*.tar.gz
|
||||
build/distributions/*.AppImage
|
||||
build/distributions/*.deb
|
||||
89
.github/workflows/osx-aarch64.yml
vendored
@@ -1,89 +0,0 @@
|
||||
name: macOS aarch64
|
||||
|
||||
on: [ push, pull_request ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install the Apple certificate
|
||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
||||
env:
|
||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
# create variables
|
||||
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
|
||||
# import certificate from secrets
|
||||
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
|
||||
|
||||
# create temporary keychain
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
|
||||
# import certificate to keychain
|
||||
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
- name: Setup the Notary information
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
STORE_CREDENTIALS: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||
run: |
|
||||
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||
|
||||
# download jdk
|
||||
- run: wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.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-
|
||||
|
||||
# test build
|
||||
- run: |
|
||||
./gradlew classes -x test --no-daemon
|
||||
./gradlew clean --no-daemon
|
||||
|
||||
# dist
|
||||
- name: Dist
|
||||
env:
|
||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' && github.repository == 'TermoraDev/termora' }}
|
||||
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||
# 只有发布版本时才需要公证
|
||||
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||
run: |
|
||||
./gradlew dist --no-daemon
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: termora-osx-aarch64
|
||||
path: |
|
||||
build/distributions/*.zip
|
||||
build/distributions/*.dmg
|
||||
@@ -1,17 +1,29 @@
|
||||
name: macOS x86-64
|
||||
name: macOS
|
||||
|
||||
on: [ push, pull_request ]
|
||||
|
||||
env:
|
||||
TERMORA_MAC_SIGN: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||
# 只有发布版本时才需要公证
|
||||
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||
JBR_MAJOR: 21.0.7
|
||||
JBR_PATCH: b1038.58
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-13
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos-15, macos-13 ]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install the Apple certificate
|
||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''"
|
||||
env:
|
||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||
@@ -34,7 +46,7 @@ jobs:
|
||||
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'"
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.APPLE_ID != ''"
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
@@ -43,8 +55,14 @@ jobs:
|
||||
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-x64-b1034.51.tar.gz
|
||||
- name: Download Java
|
||||
run: |
|
||||
if [[ "$(uname -m)" == "arm64" ]]; then
|
||||
ARCH="aarch64"
|
||||
else
|
||||
ARCH="x64"
|
||||
fi
|
||||
wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${{ env.JBR_MAJOR }}-osx-$ARCH-${{ env.JBR_PATCH }}.tar.gz
|
||||
|
||||
# install jdk
|
||||
- name: Installing Java
|
||||
@@ -53,8 +71,6 @@ jobs:
|
||||
distribution: 'jdkfile'
|
||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||
java-version: '21.0.7'
|
||||
architecture: x64
|
||||
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
@@ -65,27 +81,22 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||
|
||||
- name: Compile
|
||||
shell: bash
|
||||
run: ./gradlew :check-license && ./gradlew classes -x test
|
||||
|
||||
# test build
|
||||
- run: |
|
||||
./gradlew classes -x test --no-daemon
|
||||
./gradlew clean --no-daemon
|
||||
- name: JLink
|
||||
shell: bash
|
||||
run: ./gradlew :jar :copy-dependencies :plugins:migration:build :jlink
|
||||
|
||||
# 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: Package
|
||||
shell: bash
|
||||
run: ./gradlew :jpackage && ./gradlew :dist
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: termora-osx-x86-64
|
||||
name: termora-osx-${{ runner.arch }}
|
||||
path: |
|
||||
build/distributions/*.zip
|
||||
build/distributions/*.dmg
|
||||
53
.github/workflows/windows-x86-64.yml
vendored
@@ -1,53 +0,0 @@
|
||||
name: Windows x86-64
|
||||
|
||||
on: [ push, pull_request ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install zip
|
||||
run: |
|
||||
$system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32"
|
||||
Invoke-WebRequest -Uri "http://stahlworks.com/dev/zip.exe" -OutFile "$system32\zip.exe"
|
||||
Invoke-WebRequest -Uri "http://stahlworks.com/dev/unzip.exe" -OutFile "$system32\unzip.exe"
|
||||
|
||||
- name: Install 7z
|
||||
uses: milliewalky/setup-7-zip@v2
|
||||
|
||||
- name: Installing Java
|
||||
run: |
|
||||
curl -s --output ${{ runner.temp }}\java_package.zip -L https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.7-windows-x64-b1034.51.zip
|
||||
unzip -q ${{ runner.temp }}\java_package.zip -d ${{ runner.temp }}\jbr
|
||||
echo "JAVA_HOME=${{ runner.temp }}\jbr\jbrsdk-21.0.7-windows-x64-b1034.51" >> $env:GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||
|
||||
# test build
|
||||
- run: |
|
||||
.\gradlew classes -x test --no-daemon
|
||||
.\gradlew clean --no-daemon
|
||||
|
||||
# dist
|
||||
- run: |
|
||||
.\gradlew.bat dist --no-daemon
|
||||
.\gradlew.bat --stop
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: termora-windows-x86-64
|
||||
path: |
|
||||
build/distributions/*.zip
|
||||
build/distributions/*.exe
|
||||
75
.github/workflows/windows.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Windows
|
||||
|
||||
on: [ push, pull_request ]
|
||||
|
||||
env:
|
||||
JBR_MAJOR: 21.0.7
|
||||
JBR_PATCH: b1038.58
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ windows-11-arm, windows-latest ]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set architecture
|
||||
id: set-arch
|
||||
run: |
|
||||
if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
|
||||
echo "ARCH=aarch64" >> $env:GITHUB_ENV
|
||||
} else {
|
||||
echo "ARCH=x64" >> $env:GITHUB_ENV
|
||||
}
|
||||
|
||||
- name: Install zip
|
||||
run: |
|
||||
$system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32"
|
||||
Invoke-WebRequest -Uri "http://stahlworks.com/dev/zip.exe" -OutFile "$system32\zip.exe"
|
||||
Invoke-WebRequest -Uri "http://stahlworks.com/dev/unzip.exe" -OutFile "$system32\unzip.exe"
|
||||
|
||||
- name: Install 7z
|
||||
uses: milliewalky/setup-7-zip@v2
|
||||
|
||||
- name: Installing Java
|
||||
run: |
|
||||
$zipPath = "${{ runner.temp }}\java_package.zip"
|
||||
$extractDir = "${{ runner.temp }}\jbr"
|
||||
$url = "https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${{ env.JBR_MAJOR }}-windows-${{ env.ARCH }}-${{ env.JBR_PATCH }}.zip"
|
||||
curl -s --output $zipPath -L $url
|
||||
unzip -q $zipPath -d $extractDir
|
||||
$jbrDir = Get-ChildItem $extractDir | Select-Object -First 1
|
||||
echo "JAVA_HOME=$($jbrDir.FullName)" >> $env:GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||
|
||||
- name: Compile
|
||||
run: .\gradlew :check-license && .\gradlew classes -x test
|
||||
|
||||
- name: JLink
|
||||
run: .\gradlew :jar :copy-dependencies :plugins:migration:build :jlink
|
||||
|
||||
- name: Package
|
||||
run: .\gradlew :jpackage && .\gradlew :dist
|
||||
|
||||
- name: Stop Gradle
|
||||
run: .\gradlew.bat --stop
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: termora-windows-${{ runner.arch }}
|
||||
path: |
|
||||
build/distributions/*.zip
|
||||
build/distributions/*.exe
|
||||
125
README.md
@@ -1,53 +1,100 @@
|
||||
<div align="center">
|
||||
<a href="./README.zh_CN.md">🇨🇳 简体中文</a>
|
||||
<a href="./README.zh_CN.md">简体中文</a>
|
||||
</div>
|
||||
|
||||
# Termora
|
||||
|
||||
**Termora** is a terminal emulator and SSH client for Windows, macOS and Linux.
|
||||
**Termora** is a cross-platform terminal emulator and SSH client, available on **Windows, macOS, and Linux**.
|
||||
|
||||
<div align="center">
|
||||
<img src="./docs/readme.png" alt="termora" />
|
||||
<img src="docs/readme.png" alt="Readme" />
|
||||
</div>
|
||||
|
||||
**Termora** is developed using [Kotlin/JVM](https://kotlinlang.org) and partially implements the [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) protocol (with ongoing improvements). Its ultimate vision is to achieve full platform support (including Android, iOS, and iPadOS) through [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html).
|
||||
|
||||
## Features
|
||||
|
||||
- SSH and local terminal support
|
||||
- Serial port protocol support
|
||||
- [SFTP](./docs/sftp.png?raw=1) & [Command](./docs/sftp-command.png?raw=1) file transfer support
|
||||
- Compatible with Windows, macOS, and Linux
|
||||
- Zmodem protocol support
|
||||
- SSH port forwarding & Jump hosts
|
||||
- Support for X11 and SSH-Agent
|
||||
- Terminal log
|
||||
- Configuration synchronization via [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
||||
- Macro support (record and replay scripts)
|
||||
- Keyword highlighting
|
||||
- Key management
|
||||
- Broadcast commands to multiple sessions
|
||||
- [Find Everywhere](./docs/findeverywhere.png?raw=1) quick navigation
|
||||
- Data encryption
|
||||
- Support [plugins](https://www.termora.app/plugins)
|
||||
- ...
|
||||
|
||||
## Download
|
||||
|
||||
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
||||
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
||||
|
||||
## Development
|
||||
|
||||
It is recommended to use the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) version of the JDK and run the program via `./gradlew :run` to run the program.
|
||||
|
||||
The program can be run via `./gradlew dist` to automatically build the local version. On macOS: `dmg`, on Windows: `zip`, on Linux: `tar.gz`.
|
||||
Termora is developed using [**Kotlin/JVM**](https://kotlinlang.org/) and partially implements the [**XTerm control sequence protocol**](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html). Its long-term goal is to achieve **full platform support** (including Android, iOS, and iPadOS) via [**Kotlin Multiplatform**](https://kotlinlang.org/docs/multiplatform.html).
|
||||
|
||||
|
||||
## LICENSE
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🧬 Cross-platform support
|
||||
- 🔐 Built-in key manager
|
||||
- 🖼️ X11 forwarding
|
||||
- 🧑💻 SSH-Agent integration
|
||||
- 💻 System information display
|
||||
- 📁 GUI-based SFTP file management
|
||||
- 📊 Nvidia GPU usage monitoring
|
||||
- ⚡ Quick command shortcuts
|
||||
|
||||
|
||||
## 🚀 File Transfer
|
||||
|
||||
- Direct transfers between server A ↔ B
|
||||
- Recursive folder support
|
||||
- Up to **6 concurrent transfer tasks**
|
||||
|
||||
<div align="center">
|
||||
<img src="docs/transfer.png" alt="Transfer" />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
## 📝 File Editing
|
||||
|
||||
- Auto-upload after editing and saving
|
||||
- Rename files and folders
|
||||
- Quick deletion of large folders (`rm -rf` supported)
|
||||
- Visual permission editing
|
||||
- Create new files and folders
|
||||
|
||||
<div align="center">
|
||||
<img src="docs/transfer-edit.png" alt="Transfer Edit" />
|
||||
</div>
|
||||
|
||||
## 💻 Hosts
|
||||
|
||||
- Tree-like hierarchical structure, similar to folders
|
||||
- Assign tags to individual hosts
|
||||
- Import hosts from other tools
|
||||
- Open with the transfer tool
|
||||
|
||||
<div align="center">
|
||||
<img src="docs/host.png" alt="Transfer Edit" />
|
||||
</div>
|
||||
|
||||
## 🧩 Plugins
|
||||
|
||||
- 🌍 Geo: Display geolocation of hosts
|
||||
- 🔄 Sync: Sync settings to Gist or WebDAV
|
||||
- 🗂️ WebDAV: Connect to WebDAV storage
|
||||
- 📝 Editor: Built-in SFTP file editor
|
||||
- 📡 SMB: Connect to [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
|
||||
- ☁️ S3: Connect to S3 object storage
|
||||
- ☁️ Huawei OBS: Connect to Huawei Cloud OBS
|
||||
- ☁️ Tencent COS: Connect to Tencent Cloud COS
|
||||
- ☁️ Alibaba OSS: Connect to Alibaba Cloud OSS
|
||||
- 👉 [View all plugins...](https://www.termora.app/plugins)
|
||||
|
||||
|
||||
|
||||
|
||||
## 📦 Download
|
||||
|
||||
- 🧾 [Latest Release](https://github.com/TermoraDev/termora/releases/latest)
|
||||
- 🍺 **Homebrew**: `brew install --cask termora`
|
||||
- 🔨 **WinGet**: `winget install termora`
|
||||
|
||||
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
We recommend using the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK for development.
|
||||
|
||||
- Run locally: `./gradlew :run`
|
||||
|
||||
|
||||
## 📄 License
|
||||
|
||||
This software is distributed under a dual-license model. You may choose one of the following options:
|
||||
|
||||
- AGPL-3.0: Use, distribute, and modify the software under the terms of the [AGPL-3.0](https://opensource.org/license/agpl-v3).
|
||||
- Proprietary License: For closed-source or proprietary use, please contact the author to obtain a commercial license.
|
||||
- **AGPL-3.0**: Use, distribute, and modify the software under the terms of the [AGPL-3.0](https://opensource.org/license/agpl-v3).
|
||||
- **Proprietary License**: For closed-source or proprietary use, please contact the author to obtain a commercial license.
|
||||
|
||||
114
README.zh_CN.md
@@ -1,48 +1,98 @@
|
||||
# Termora
|
||||
|
||||
**Termora** 是一个终端模拟器和 SSH 客户端,支持 Windows,macOS 和 Linux。
|
||||
**Termora** 是一款跨平台终端模拟器和 SSH 客户端,支持 **Windows、macOS、Linux**。
|
||||
|
||||
<div align="center">
|
||||
<img src="./docs/readme-zh_CN.png" alt="termora" />
|
||||
<img src="docs/readme-zh_CN.png" alt="Readme" />
|
||||
</div>
|
||||
|
||||
**Termora** 采用 [Kotlin/JVM](https://kotlinlang.org/) 开发并实现了 [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) 协议(尚未完全实现),它的最终目标是通过 [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) 实现全平台(含 Android、iOS、iPadOS 等)。
|
||||
Termora 使用 [**Kotlin/JVM**](https://kotlinlang.org/) 开发,支持(正在实现中) [**XTerm 控制序列协议**](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)。未来目标是借助 [**Kotlin Multiplatform**](https://kotlinlang.org/docs/multiplatform.html) 实现 **全平台支持**,包括 Android、iOS、iPadOS 等。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 支持 SSH 和本地终端
|
||||
- 支持串口协议
|
||||
- 支持 [SFTP](./docs/sftp-zh_CN.png?raw=1) & [命令行](./docs/sftp-command.png?raw=1) 文件传输
|
||||
- 支持 Windows、macOS、Linux 平台
|
||||
- 支持 Zmodem 协议
|
||||
- 支持 SSH 端口转发和跳板机
|
||||
- 支持 X11 和 SSH-Agent
|
||||
- 终端日志记录
|
||||
- 支持配置同步到 [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
||||
- 支持宏(录制脚本并回放)
|
||||
- 支持关键词高亮
|
||||
- 支持密钥管理器
|
||||
- 支持将命令发送到多个会话
|
||||
- 支持 [Find Everywhere](./docs/findeverywhere-zh_CN.png?raw=1) 快速跳转
|
||||
- 支持数据加密
|
||||
- 支持[插件](https://www.termora.app/plugins)
|
||||
- ...
|
||||
|
||||
## 下载
|
||||
## ✨ 功能特性
|
||||
|
||||
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
||||
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
||||
- 🧬 跨平台运行
|
||||
- 🔐 内建密钥管理器
|
||||
- 🖼️ 支持 X11 转发
|
||||
- 🧑💻 SSH-Agent 集成
|
||||
- 💻 系统信息展示
|
||||
- 📁 图形化 SFTP 文件管理
|
||||
- 📊 Nvidia 显卡使用率查看
|
||||
- ⚡ 快捷指令支持
|
||||
|
||||
## 开发
|
||||
|
||||
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) 的 JDK 版本,通过 `./gradlew :run` 即可运行程序。
|
||||
## 🚀 文件传输
|
||||
|
||||
通过 `./gradlew dist` 可以自动构建适用于本机的版本。在 macOS 上是:`dmg`,在 Windows 上是:`zip`,在 Linux 上是:`tar.gz`。
|
||||
- 支持 A ↔ B 服务器间直接传输
|
||||
- 文件夹递归复制支持
|
||||
- 最多可同时运行 **6 个传输任务**
|
||||
|
||||
## 协议
|
||||
<div align="center">
|
||||
<img src="docs/transfer-zh_CN.png" alt="Transfer" />
|
||||
</div>
|
||||
|
||||
本软件采用双重许可模式,您可以选择以下任意一种许可方式:
|
||||
|
||||
- AGPL-3.0:根据 [AGPL-3.0](https://opensource.org/license/agpl-v3) 的条款,您可以自由使用、分发和修改本软件。
|
||||
- 专有许可:如果希望在闭源或专有环境中使用,请联系作者获取许可。
|
||||
## 📝 文件编辑功能
|
||||
|
||||
- 保存后自动上传修改内容
|
||||
- 文件 / 文件夹 重命名
|
||||
- 快速删除大文件夹:`rm -rf` 支持
|
||||
- 可视化更改权限
|
||||
- 支持新建文件 / 文件夹
|
||||
|
||||
<div align="center">
|
||||
<img src="docs/transfer-edit-zh_CN.png" alt="Transfer Edit" />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
## 💻 主机
|
||||
|
||||
- 类似文件夹树形结构
|
||||
- 给主机添加标签
|
||||
- 从其它软件导入
|
||||
- 使用传输工具打开
|
||||
|
||||
<div align="center">
|
||||
<img src="docs/host-zh_CN.png" alt="Transfer Edit" />
|
||||
</div>
|
||||
|
||||
|
||||
## 🧩 插件
|
||||
|
||||
- 🌍 Geo:显示主机位置信息
|
||||
- 🔄 Sync:将配置同步至 Gist 或 WebDAV
|
||||
- 🗂️ WebDAV:连接 WebDAV 对象存储
|
||||
- 📝 Editor:内置 SFTP 文件编辑器
|
||||
- 📡 SMB: 连接 [SMB](https://baike.baidu.com/item/smb/4750512) 文件共享协议
|
||||
- ☁️ S3:连接 S3 对象存储
|
||||
- ☁️ Huawei OBS:连接华为云对象存储
|
||||
- ☁️ Tencent COS:连接腾讯云 COS
|
||||
- ☁️ Alibaba OSS:连接阿里云 OSS
|
||||
- 👉 [查看所有插件...](https://www.termora.cn/plugins)
|
||||
|
||||
|
||||
|
||||
|
||||
## 📦 下载
|
||||
|
||||
- 🧾 [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||
- 🍺 **Homebrew**:`brew install --cask termora`
|
||||
- 🪟 **WinGet**:`winget install termora`
|
||||
|
||||
|
||||
|
||||
## 🛠️ 开发指南
|
||||
|
||||
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK 运行环境。
|
||||
|
||||
- 本地运行:`./gradlew :run`
|
||||
|
||||
|
||||
## 📄 授权协议
|
||||
|
||||
Termora 采用双重许可方式,您可以选择:
|
||||
|
||||
- **AGPL-3.0**:自由使用、修改、分发(遵循 [AGPL 条款](https://opensource.org/license/agpl-v3))
|
||||
- **专有许可**:如需闭源或商业用途,请联系作者获取授权
|
||||
|
||||
@@ -28,7 +28,7 @@ version = rootProject.projectDir.resolve("VERSION").readText().trim()
|
||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||
val appVersion = project.version.toString().split("-")[0]
|
||||
val isDeb = os.isLinux && System.getProperty("type") == "deb"
|
||||
val isDeb = os.isLinux && System.getenv("TERMORA_TYPE") == "deb"
|
||||
|
||||
// macOS 签名信息
|
||||
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
||||
@@ -62,9 +62,6 @@ dependencies {
|
||||
testImplementation(libs.h2)
|
||||
testImplementation(libs.exposed.migration)
|
||||
|
||||
// implementation(platform(libs.koin.bom))
|
||||
// implementation(libs.koin.core)
|
||||
|
||||
api(kotlin("reflect"))
|
||||
api(libs.slf4j.api)
|
||||
api(libs.pty4j)
|
||||
@@ -105,7 +102,6 @@ dependencies {
|
||||
|
||||
api(libs.colorpicker)
|
||||
api(libs.mixpanel)
|
||||
api(libs.jSerialComm)
|
||||
api(libs.ini4j)
|
||||
api(libs.restart4j)
|
||||
api(libs.exposed.core)
|
||||
@@ -137,10 +133,6 @@ application {
|
||||
|
||||
args.add("-DTERMORA_PLUGIN_DIRECTORY=${layout.buildDirectory.get().asFile.absolutePath}${File.separator}plugins")
|
||||
|
||||
if (os.isLinux) {
|
||||
args.add("-Dsun.java2d.opengl=true")
|
||||
}
|
||||
|
||||
applicationDefaultJvmArgs = args
|
||||
mainClass = "app.termora.MainKt"
|
||||
}
|
||||
@@ -193,7 +185,7 @@ tasks.register<Copy>("copy-dependencies") {
|
||||
|
||||
// 对 JNA 和 PTY4J 的本地库提取
|
||||
// 提取出来是为了单独签名,不然无法通过公证
|
||||
if (os.isMacOsX && macOSSign) {
|
||||
if (os.isMacOsX) {
|
||||
doLast {
|
||||
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
||||
val dylib = dir.get().dir("dylib").asFile
|
||||
@@ -441,6 +433,7 @@ tasks.register<Exec>("jpackage") {
|
||||
// NSWindow
|
||||
options.add("-Dapple.awt.application.appearance=system")
|
||||
options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||
options.add("--add-opens java.desktop/sun.font=ALL-UNNAMED")
|
||||
options.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||
@@ -448,7 +441,6 @@ tasks.register<Exec>("jpackage") {
|
||||
}
|
||||
|
||||
if (os.isLinux) {
|
||||
options.add("-Dsun.java2d.opengl=true")
|
||||
if (isDeb) {
|
||||
options.add("-Djpackage.app-layout=deb")
|
||||
}
|
||||
@@ -488,10 +480,6 @@ tasks.register<Exec>("jpackage") {
|
||||
}
|
||||
|
||||
if (os.isWindows) {
|
||||
arguments.add("--win-dir-chooser")
|
||||
arguments.add("--win-shortcut")
|
||||
arguments.add("--win-shortcut-prompt")
|
||||
arguments.addAll(listOf("--win-upgrade-uuid", "E1D93CAD-5BF8-442E-93BA-6E90DE601E4C"))
|
||||
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
|
||||
}
|
||||
|
||||
@@ -504,7 +492,7 @@ tasks.register<Exec>("jpackage") {
|
||||
if (os.isMacOsX) {
|
||||
arguments.add("dmg")
|
||||
} else if (os.isWindows) {
|
||||
arguments.add("msi")
|
||||
arguments.add("app-image")
|
||||
} else if (os.isLinux) {
|
||||
arguments.add(if (isDeb) "deb" else "app-image")
|
||||
if (isDeb) {
|
||||
@@ -527,31 +515,20 @@ tasks.register<Exec>("jpackage") {
|
||||
|
||||
tasks.register("dist") {
|
||||
doLast {
|
||||
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
|
||||
val distributionDir = layout.buildDirectory.dir("distributions").get()
|
||||
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
|
||||
val projectName = project.name.uppercaseFirstChar()
|
||||
|
||||
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
|
||||
|
||||
// 清空目录
|
||||
exec { commandLine(gradlew, "clean") }
|
||||
|
||||
// 构建自带的插件
|
||||
exec { commandLine(gradlew, ":plugins:migration:build") }
|
||||
|
||||
// 打包并复制依赖
|
||||
exec {
|
||||
commandLine(gradlew, ":jar", ":copy-dependencies")
|
||||
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")
|
||||
}
|
||||
|
||||
// 检查依赖的开源协议
|
||||
exec { commandLine(gradlew, ":check-license") }
|
||||
|
||||
// jlink
|
||||
exec { commandLine(gradlew, ":jlink") }
|
||||
|
||||
// 打包
|
||||
exec { commandLine(gradlew, ":jpackage", "-Dtype=${System.getProperty("type")}") }
|
||||
|
||||
// 根据不同的系统构建不同的二进制包
|
||||
pack()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,32 +559,12 @@ 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
|
||||
*/
|
||||
fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
||||
val dir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
|
||||
val dir = layout.buildDirectory.dir("distributions").get().asFile
|
||||
val cfg = FileUtils.getFile(dir, projectName, "app", "${projectName}.cfg")
|
||||
val configText = cfg.readText()
|
||||
|
||||
@@ -632,21 +589,12 @@ fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: Str
|
||||
"/DMyAppVersion=${appVersion}",
|
||||
"/DMyOutputDir=${distributionDir.asFile.absolutePath}",
|
||||
"/DMySetupIconFile=${FileUtils.getFile(projectDir, "src", "main", "resources", "icons", "termora.ico")}",
|
||||
"/DMySourceDir=${layout.buildDirectory.dir("jpackage/images/win-msi.image/${projectName}").get().asFile}",
|
||||
"/DMySourceDir=${FileUtils.getFile(dir, projectName).absolutePath}",
|
||||
"/F${finalFilenameWithoutExtension}",
|
||||
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
|
||||
)
|
||||
}
|
||||
|
||||
// msi
|
||||
exec {
|
||||
commandLine(
|
||||
"cmd", "/c", "move",
|
||||
"${projectName}-${appVersion}.msi",
|
||||
"${finalFilenameWithoutExtension}.msi"
|
||||
)
|
||||
workingDir = distributionDir.asFile
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -662,7 +610,7 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
|
||||
// @formatter:on
|
||||
|
||||
// sign dmg
|
||||
if (macOSSign) signMacOSLocalFile(dmgFile)
|
||||
signMacOSLocalFile(dmgFile)
|
||||
|
||||
// 找到 .app
|
||||
val imageFile = layout.buildDirectory.dir("jpackage/images/").get().asFile
|
||||
@@ -675,7 +623,7 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
|
||||
// @formatter:on
|
||||
|
||||
// sign zip
|
||||
if (macOSSign) signMacOSLocalFile(zipFile)
|
||||
signMacOSLocalFile(zipFile)
|
||||
|
||||
// 公证
|
||||
if (macOSNotary) {
|
||||
|
||||
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 61 KiB |
BIN
docs/host-zh_CN.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/host.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
docs/plugins-zh_CN.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/plugins.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 166 KiB |
BIN
docs/readme.png
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 55 KiB |
BIN
docs/sftp.png
|
Before Width: | Height: | Size: 49 KiB |
BIN
docs/tags-zh_CN.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/tags.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
docs/transfer-edit-zh_CN.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
docs/transfer-edit.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/transfer-zh_CN.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
docs/transfer.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
@@ -4,10 +4,10 @@ slf4j = "2.0.17"
|
||||
pty4j = "0.13.6"
|
||||
tinylog = "2.7.0"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
flatlaf = "3.6"
|
||||
kotlinx-serialization-json = "1.8.1"
|
||||
flatlaf = "3.6.1-SNAPSHOT"
|
||||
kotlinx-serialization-json = "1.9.0"
|
||||
commons-codec = "1.18.0"
|
||||
commons-lang3 = "3.17.0"
|
||||
commons-lang3 = "3.18.0"
|
||||
commons-csv = "1.14.0"
|
||||
commons-net = "3.11.1"
|
||||
commons-text = "1.13.1"
|
||||
@@ -22,9 +22,9 @@ jna = "5.17.0"
|
||||
jSystemThemeDetector = "3.9.1"
|
||||
commons-io = "2.19.0"
|
||||
jbr-api = "17.1.10.1"
|
||||
hutool = "5.8.37"
|
||||
jsch = "0.2.26"
|
||||
okhttp = "4.12.0"
|
||||
hutool = "5.8.39"
|
||||
jsch = "2.27.2"
|
||||
okhttp = "5.1.0"
|
||||
sshj = "0.39.0"
|
||||
sshd-core = "2.15.0"
|
||||
jgit = "7.2.0.202503040940-r"
|
||||
@@ -37,17 +37,17 @@ rhino = "1.8.0"
|
||||
delight-rhino-sandbox = "0.0.17"
|
||||
testcontainers = "1.21.3"
|
||||
mixpanel = "1.5.3"
|
||||
jSerialComm = "2.11.0"
|
||||
jSerialComm = "2.11.2"
|
||||
ini4j = "0.5.5-2"
|
||||
restart4j = "0.0.1"
|
||||
eddsa = "0.3.0"
|
||||
exposed = "1.0.0-beta-2"
|
||||
exposed = "1.0.0-beta-4"
|
||||
h2 = "2.3.232"
|
||||
sqlite = "3.50.2.0"
|
||||
jug = "5.1.0"
|
||||
semver4j = "6.0.0"
|
||||
jsvg = "1.4.0"
|
||||
dom4j = "2.1.4"
|
||||
jsvg = "2.0.0"
|
||||
dom4j = "2.2.0"
|
||||
|
||||
[libraries]
|
||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.4"
|
||||
project.version = "0.0.5"
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.termora.plugins.bg
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.database.DatabaseManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
@@ -96,9 +95,7 @@ internal class BackgroundManager private constructor() : Disposable, GlassPaneAw
|
||||
return
|
||||
}
|
||||
val body = response.body
|
||||
if (body != null) {
|
||||
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
|
||||
}
|
||||
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
|
||||
IOUtils.closeQuietly(body)
|
||||
return@use tempFile
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.2"
|
||||
project.version = "0.0.3"
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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
|
||||
@@ -13,7 +14,7 @@ class COSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
|
||||
return COSProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return COSProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,13 @@ plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.1"
|
||||
|
||||
project.version = "0.0.2"
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
implementation("org.apache.commons:commons-pool2:2.12.1")
|
||||
testImplementation(project(":"))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import org.apache.commons.vfs2.Capability
|
||||
import org.apache.commons.vfs2.FileName
|
||||
import org.apache.commons.vfs2.FileSystem
|
||||
import org.apache.commons.vfs2.FileSystemOptions
|
||||
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
|
||||
|
||||
class FTPFileProvider private constructor() : AbstractOriginatingFileProvider() {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { FTPFileProvider() }
|
||||
val capabilities = listOf(
|
||||
Capability.CREATE,
|
||||
Capability.DELETE,
|
||||
Capability.RENAME,
|
||||
Capability.GET_TYPE,
|
||||
Capability.LIST_CHILDREN,
|
||||
Capability.READ_CONTENT,
|
||||
Capability.URI,
|
||||
Capability.WRITE_CONTENT,
|
||||
Capability.GET_LAST_MODIFIED,
|
||||
Capability.SET_LAST_MODIFIED_FILE,
|
||||
Capability.RANDOM_ACCESS_READ,
|
||||
Capability.APPEND_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCapabilities(): Collection<Capability> {
|
||||
return FTPFileProvider.capabilities
|
||||
}
|
||||
|
||||
override fun doCreateFileSystem(
|
||||
rootFileName: FileName,
|
||||
fileSystemOptions: FileSystemOptions
|
||||
): FileSystem? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.transfer.s3.S3FileSystem
|
||||
import app.termora.transfer.s3.S3Path
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.net.ftp.FTPClient
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||
|
||||
class FTPFileSystem(private val pool: GenericObjectPool<FTPClient>) : S3FileSystem(FTPSystemProvider(pool)) {
|
||||
|
||||
override fun create(root: String?, names: List<String>): S3Path {
|
||||
val path = FTPPath(this, root, names)
|
||||
if (names.isEmpty()) {
|
||||
path.attributes = path.attributes.copy(directory = true)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(pool)
|
||||
super.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.keymgr.KeyManager
|
||||
import app.termora.plugin.internal.BasicProxyOption
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.awt.event.ItemEvent
|
||||
import java.nio.charset.Charset
|
||||
import javax.swing.*
|
||||
|
||||
class FTPHostOptionsPane : OptionsPane() {
|
||||
private val generalOption = GeneralOption()
|
||||
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
|
||||
private val sftpOption = SFTPOption()
|
||||
|
||||
init {
|
||||
addOption(generalOption)
|
||||
addOption(proxyOption)
|
||||
addOption(sftpOption)
|
||||
|
||||
}
|
||||
|
||||
fun getHost(): Host {
|
||||
val name = generalOption.nameTextField.text
|
||||
val protocol = FTPProtocolProvider.PROTOCOL
|
||||
val port = generalOption.portTextField.value as Int
|
||||
var authentication = Authentication.Companion.No
|
||||
var proxy = Proxy.Companion.No
|
||||
val authenticationType = AuthenticationType.Password
|
||||
|
||||
authentication = authentication.copy(
|
||||
type = authenticationType,
|
||||
password = String(generalOption.passwordTextField.password)
|
||||
)
|
||||
|
||||
|
||||
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||
proxy = proxy.copy(
|
||||
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
|
||||
host = proxyOption.proxyHostTextField.text,
|
||||
username = proxyOption.proxyUsernameTextField.text,
|
||||
password = String(proxyOption.proxyPasswordTextField.password),
|
||||
port = proxyOption.proxyPortTextField.value as Int,
|
||||
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val options = Options.Default.copy(
|
||||
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||
encoding = sftpOption.charsetComboBox.selectedItem as String,
|
||||
extras = mutableMapOf("passive" to (sftpOption.passiveComboBox.selectedItem as PassiveMode).name)
|
||||
)
|
||||
|
||||
return Host(
|
||||
name = name,
|
||||
protocol = protocol,
|
||||
port = port,
|
||||
host = generalOption.hostTextField.text,
|
||||
username = generalOption.usernameTextField.text,
|
||||
authentication = authentication,
|
||||
proxy = proxy,
|
||||
sort = System.currentTimeMillis(),
|
||||
remark = generalOption.remarkTextArea.text,
|
||||
options = options,
|
||||
)
|
||||
}
|
||||
|
||||
fun setHost(host: Host) {
|
||||
generalOption.nameTextField.text = host.name
|
||||
generalOption.usernameTextField.text = host.username
|
||||
generalOption.remarkTextArea.text = host.remark
|
||||
generalOption.passwordTextField.text = host.authentication.password
|
||||
generalOption.hostTextField.text = host.host
|
||||
generalOption.portTextField.value = host.port
|
||||
|
||||
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||
proxyOption.proxyUsernameTextField.text = host.proxy.username
|
||||
proxyOption.proxyPortTextField.value = host.proxy.port
|
||||
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
|
||||
|
||||
|
||||
val passive = host.options.extras["passive"] ?: PassiveMode.Local.name
|
||||
sftpOption.charsetComboBox.selectedItem = host.options.encoding
|
||||
sftpOption.passiveComboBox.selectedItem = runCatching { PassiveMode.valueOf(passive) }
|
||||
.getOrNull() ?: PassiveMode.Local
|
||||
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||
}
|
||||
|
||||
fun validateFields(): Boolean {
|
||||
val host = getHost()
|
||||
|
||||
// general
|
||||
if (validateField(generalOption.nameTextField)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if (validateField(generalOption.hostTextField)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(generalOption.usernameTextField.text) || generalOption.passwordTextField.password.isNotEmpty()) {
|
||||
if (validateField(generalOption.usernameTextField)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (validateField(generalOption.passwordTextField)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// proxy
|
||||
if (host.proxy.type != ProxyType.No) {
|
||||
if (validateField(proxyOption.proxyHostTextField)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (host.proxy.authenticationType != AuthenticationType.No) {
|
||||
if (validateField(proxyOption.proxyUsernameTextField)
|
||||
|| validateField(proxyOption.proxyPasswordTextField)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 true 表示有错误
|
||||
*/
|
||||
private fun validateField(textField: JTextField): Boolean {
|
||||
if (textField.isEnabled && (if (textField is JPasswordField) textField.password.isEmpty() else textField.text.isBlank())) {
|
||||
setOutlineError(textField)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun setOutlineError(c: JComponent) {
|
||||
selectOptionJComponent(c)
|
||||
c.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||
c.requestFocusInWindow()
|
||||
}
|
||||
|
||||
|
||||
inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||
val portTextField = PortSpinner(21)
|
||||
val nameTextField = OutlineTextField(128)
|
||||
val usernameTextField = OutlineTextField(128)
|
||||
val hostTextField = OutlineTextField(255)
|
||||
val passwordTextField = OutlinePasswordField(255)
|
||||
val publicKeyComboBox = OutlineComboBox<String>()
|
||||
val remarkTextArea = FixedLengthTextArea(512)
|
||||
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
|
||||
publicKeyComboBox.isEditable = false
|
||||
|
||||
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = StringUtils.EMPTY
|
||||
if (value is String) {
|
||||
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
|
||||
}
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
text,
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = value?.toString() ?: ""
|
||||
when (value) {
|
||||
AuthenticationType.Password -> {
|
||||
text = "Password"
|
||||
}
|
||||
|
||||
AuthenticationType.PublicKey -> {
|
||||
text = "Public Key"
|
||||
}
|
||||
|
||||
AuthenticationType.KeyboardInteractive -> {
|
||||
text = "Keyboard Interactive"
|
||||
}
|
||||
}
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
text,
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||
|
||||
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
|
||||
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentResized(e: ComponentEvent) {
|
||||
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
|
||||
removeComponentListener(this)
|
||||
}
|
||||
})
|
||||
|
||||
authenticationTypeComboBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
passwordTextField.isEnabled = authenticationTypeComboBox.selectedItem == AuthenticationType.Password
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.settings
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.new-host.general")
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
|
||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
remarkTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
remarkTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
|
||||
remarkTextArea.rows = 8
|
||||
remarkTextArea.lineWrap = true
|
||||
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
||||
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||
.add(hostTextField).xy(3, rows)
|
||||
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||
.add(portTextField).xy(7, rows).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
|
||||
.add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||
.xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.build()
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||
val defaultDirectoryField = OutlineTextField(255)
|
||||
val charsetComboBox = JComboBox<String>()
|
||||
val passiveComboBox = JComboBox<PassiveMode>()
|
||||
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
|
||||
for (e in Charset.availableCharsets()) {
|
||||
charsetComboBox.addItem(e.key)
|
||||
}
|
||||
|
||||
charsetComboBox.selectedItem = "UTF-8"
|
||||
|
||||
passiveComboBox.addItem(PassiveMode.Local)
|
||||
passiveComboBox.addItem(PassiveMode.Remote)
|
||||
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.folder
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.transport.sftp")
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow",
|
||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
||||
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${FTPI18n.getString("termora.plugins.ftp.passive")}:").xy(1, rows)
|
||||
.add(passiveComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
|
||||
.build()
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
enum class PassiveMode {
|
||||
Local,
|
||||
Remote,
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.I18n
|
||||
import app.termora.NamedI18n
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
|
||||
object FTPI18n : NamedI18n("i18n/messages") {
|
||||
private val log = LoggerFactory.getLogger(FTPI18n::class.java)
|
||||
|
||||
override fun getLogger(): Logger {
|
||||
return log
|
||||
}
|
||||
|
||||
override fun getString(key: String): String {
|
||||
return try {
|
||||
substitutor.replace(getBundle().getString(key))
|
||||
} catch (_: MissingResourceException) {
|
||||
I18n.getString(key)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.transfer.s3.S3Path
|
||||
|
||||
class FTPPath(fileSystem: FTPFileSystem, root: String?, names: List<String>) : S3Path(fileSystem, root, names) {
|
||||
override val isBucket: Boolean
|
||||
get() = false
|
||||
|
||||
override val bucketName: String
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override val objectName: String
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override fun getCustomType(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.PaidPlugin
|
||||
@@ -27,6 +24,7 @@ class FTPPlugin : PaidPlugin {
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.Disposer
|
||||
import app.termora.Host
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
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 Host(
|
||||
name = StringUtils.EMPTY,
|
||||
protocol = FTPProtocolProvider.PROTOCOL
|
||||
)
|
||||
return pane.getHost()
|
||||
}
|
||||
|
||||
override fun setHost(host: Host) {
|
||||
|
||||
pane.setHost(host)
|
||||
}
|
||||
|
||||
override fun validateFields(): Boolean {
|
||||
return true
|
||||
return pane.validateFields()
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
|
||||
class FTPProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolHostPanelExtension() }
|
||||
val instance = FTPProtocolHostPanelExtension()
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return FTPProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return FTPProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,33 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.AuthenticationType
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.Icons
|
||||
import app.termora.protocol.FileObjectHandler
|
||||
import app.termora.protocol.FileObjectRequest
|
||||
import app.termora.ProxyType
|
||||
import app.termora.protocol.PathHandler
|
||||
import app.termora.protocol.PathHandlerRequest
|
||||
import app.termora.protocol.TransferProtocolProvider
|
||||
import org.apache.commons.vfs2.provider.FileProvider
|
||||
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 {
|
||||
val instance by lazy { FTPProtocolProvider() }
|
||||
private val log = LoggerFactory.getLogger(FTPProtocolProvider::class.java)
|
||||
|
||||
val instance = FTPProtocolProvider()
|
||||
const val PROTOCOL = "FTP"
|
||||
}
|
||||
|
||||
@@ -22,12 +39,82 @@ class FTPProtocolProvider private constructor() : TransferProtocolProvider {
|
||||
return Icons.ftp
|
||||
}
|
||||
|
||||
override fun getFileProvider(): FileProvider {
|
||||
return FTPFileProvider.instance
|
||||
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))
|
||||
}
|
||||
|
||||
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolProviderExtension() }
|
||||
val instance = FTPProtocolProviderExtension()
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return FTPProtocolProvider.Companion.instance
|
||||
return FTPProtocolProvider.instance
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.transfer.s3.S3FileSystemProvider
|
||||
import app.termora.transfer.s3.S3Path
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.net.ftp.FTPClient
|
||||
import org.apache.commons.net.ftp.FTPFile
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.AccessMode
|
||||
import java.nio.file.CopyOption
|
||||
import java.nio.file.NoSuchFileException
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.attribute.FileAttribute
|
||||
import java.nio.file.attribute.PosixFilePermission
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.exists
|
||||
|
||||
class FTPSystemProvider(private val pool: GenericObjectPool<FTPClient>) : S3FileSystemProvider() {
|
||||
|
||||
|
||||
override fun getScheme(): String? {
|
||||
return "ftp"
|
||||
}
|
||||
|
||||
override fun getOutputStream(path: S3Path): OutputStream {
|
||||
return createStreamer(path)
|
||||
}
|
||||
|
||||
override fun getInputStream(path: S3Path): InputStream {
|
||||
val ftp = pool.borrowObject()
|
||||
val fs = ftp.retrieveFileStream(path.absolutePathString())
|
||||
return object : InputStream() {
|
||||
override fun read(): Int {
|
||||
return fs.read()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(fs)
|
||||
ftp.completePendingCommand()
|
||||
pool.returnObject(ftp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createStreamer(path: S3Path): OutputStream {
|
||||
val ftp = pool.borrowObject()
|
||||
val os = ftp.storeFileStream(path.absolutePathString())
|
||||
return object : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
os.write(b)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(os)
|
||||
ftp.completePendingCommand()
|
||||
pool.returnObject(ftp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
|
||||
val paths = mutableListOf<S3Path>()
|
||||
if (path.exists().not()) {
|
||||
throw NoSuchFileException(path.absolutePathString())
|
||||
}
|
||||
|
||||
withFtpClient {
|
||||
val files = it.listFiles(path.absolutePathString())
|
||||
for (file in files) {
|
||||
val p = path.resolve(file.name)
|
||||
p.attributes = p.attributes.copy(
|
||||
directory = file.isDirectory,
|
||||
regularFile = file.isFile,
|
||||
size = file.size,
|
||||
lastModifiedTime = file.timestamp.timeInMillis,
|
||||
)
|
||||
p.attributes.permissions = ftpPermissionsToPosix(file)
|
||||
paths.add(p)
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun ftpPermissionsToPosix(file: FTPFile): Set<PosixFilePermission> {
|
||||
val perms = mutableSetOf<PosixFilePermission>()
|
||||
|
||||
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION))
|
||||
perms.add(PosixFilePermission.OWNER_READ)
|
||||
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OWNER_WRITE)
|
||||
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OWNER_EXECUTE)
|
||||
|
||||
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.READ_PERMISSION))
|
||||
perms.add(PosixFilePermission.GROUP_READ)
|
||||
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||
perms.add(PosixFilePermission.GROUP_WRITE)
|
||||
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||
perms.add(PosixFilePermission.GROUP_EXECUTE)
|
||||
|
||||
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.READ_PERMISSION))
|
||||
perms.add(PosixFilePermission.OTHERS_READ)
|
||||
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OTHERS_WRITE)
|
||||
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OTHERS_EXECUTE)
|
||||
|
||||
return perms
|
||||
}
|
||||
|
||||
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
|
||||
withFtpClient { it.mkd(dir.absolutePathString()) }
|
||||
}
|
||||
|
||||
override fun move(source: Path?, target: Path?, vararg options: CopyOption?) {
|
||||
if (source != null && target != null) {
|
||||
withFtpClient {
|
||||
it.rename(source.absolutePathString(), target.absolutePathString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(path: S3Path, isDirectory: Boolean) {
|
||||
withFtpClient {
|
||||
if (isDirectory) {
|
||||
it.rmd(path.absolutePathString())
|
||||
} else {
|
||||
it.deleteFile(path.absolutePathString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
|
||||
withFtpClient {
|
||||
if (it.cwd(path.absolutePathString()) == 250) {
|
||||
return
|
||||
}
|
||||
if (it.listFiles(path.absolutePathString()).isNotEmpty()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
throw NoSuchFileException(path.absolutePathString())
|
||||
}
|
||||
|
||||
private inline fun <T> withFtpClient(block: (FTPClient) -> T): T {
|
||||
val client = pool.borrowObject()
|
||||
return try {
|
||||
block(client)
|
||||
} finally {
|
||||
pool.returnObject(client)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<descriptions>
|
||||
<description>Connecting to FTP</description>
|
||||
<description language="zh_CN">支持连接到到 FTP</description>
|
||||
<description language="zh_CN">支持连接到 FTP</description>
|
||||
<description language="zh_TW">支援連接到 FTP</description>
|
||||
</descriptions>
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#6C707E"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#6C707E"></path></svg>
|
||||
<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.3 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1 +1 @@
|
||||
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#CED0D6"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#CED0D6"></path></svg>
|
||||
<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.3 KiB After Width: | Height: | Size: 1.2 KiB |
1
plugins/ftp/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1 @@
|
||||
termora.plugins.ftp.passive=Passive Mode
|
||||
@@ -0,0 +1 @@
|
||||
termora.plugins.ftp.passive=被动模式
|
||||
@@ -0,0 +1 @@
|
||||
termora.plugins.ftp.passive=被動模式
|
||||
@@ -2,14 +2,14 @@ plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.5"
|
||||
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-202506280327")
|
||||
implementation("com.github.hstyi:geolite2:v1.0-202507070058")
|
||||
}
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
|
||||
@@ -33,7 +34,7 @@ class GeoSimpleTreeCellRendererExtension private constructor() : SimpleTreeCellR
|
||||
if (GeoHostTreeShowMoreEnableExtension.instance.isShowMore().not()) return emptyList()
|
||||
val country = geo.country(node.data.host) ?: return emptyList()
|
||||
|
||||
val text = "${countryCodeToFlagEmoji(country.isoCode)}${country.name}"
|
||||
val text = if (SystemInfo.isMacOS) "${countryCodeToFlagEmoji(country.isoCode)}${country.name}" else country.name
|
||||
return listOf(
|
||||
MarkerSimpleTreeCellAnnotation(
|
||||
text,
|
||||
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.2"
|
||||
project.version = "0.0.3"
|
||||
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -46,7 +46,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
controlsVisible = false
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
title = I18n.getString("termora.doorman.safe")
|
||||
title = MigrationI18n.getString("termora.doorman.safe")
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||
}
|
||||
|
||||
@@ -65,8 +65,8 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
label.text = I18n.getString("termora.doorman.safe")
|
||||
tip.text = I18n.getString("termora.doorman.unlock-data")
|
||||
label.text = MigrationI18n.getString("termora.doorman.safe")
|
||||
tip.text = MigrationI18n.getString("termora.doorman.unlock-data")
|
||||
icon.icon = FlatSVGIcon(Icons.role.name, 80, 80)
|
||||
safeBtn.icon = Icons.unlocked
|
||||
|
||||
@@ -95,24 +95,24 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
.add(passwordTextField).xy(2, rows)
|
||||
.add(safeBtn).xy(4, rows).apply { rows += step }
|
||||
.add(tip).xyw(2, rows, 4, "center, fill").apply { rows += step }
|
||||
.add(JXHyperlink(object : AnAction(I18n.getString("termora.doorman.forget-password")) {
|
||||
.add(JXHyperlink(object : AnAction(MigrationI18n.getString("termora.doorman.forget-password")) {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val option = OptionPane.showConfirmDialog(
|
||||
this@DoormanDialog, I18n.getString("termora.doorman.forget-password-message"),
|
||||
this@DoormanDialog, MigrationI18n.getString("termora.doorman.forget-password-message"),
|
||||
options = arrayOf(
|
||||
I18n.getString("termora.doorman.have-a-mnemonic"),
|
||||
I18n.getString("termora.doorman.dont-have-a-mnemonic"),
|
||||
MigrationI18n.getString("termora.doorman.have-a-mnemonic"),
|
||||
MigrationI18n.getString("termora.doorman.dont-have-a-mnemonic"),
|
||||
),
|
||||
optionType = JOptionPane.YES_NO_OPTION,
|
||||
messageType = JOptionPane.INFORMATION_MESSAGE,
|
||||
initialValue = I18n.getString("termora.doorman.have-a-mnemonic")
|
||||
initialValue = MigrationI18n.getString("termora.doorman.have-a-mnemonic")
|
||||
)
|
||||
if (option == JOptionPane.YES_OPTION) {
|
||||
showMnemonicsDialog()
|
||||
} else if (option == JOptionPane.NO_OPTION) {
|
||||
OptionPane.showMessageDialog(
|
||||
this@DoormanDialog,
|
||||
I18n.getString("termora.doorman.delete-data"),
|
||||
MigrationI18n.getString("termora.doorman.delete-data"),
|
||||
messageType = JOptionPane.WARNING_MESSAGE
|
||||
)
|
||||
Application.browse(MigrationApplicationRunnerExtension.instance.getDatabaseFile().toURI())
|
||||
@@ -141,7 +141,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
OptionPane.showMessageDialog(
|
||||
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
||||
this, MigrationI18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
passwordTextField.outline = "error"
|
||||
@@ -166,7 +166,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
} catch (e: Exception) {
|
||||
if (e is PasswordWrongException) {
|
||||
OptionPane.showMessageDialog(
|
||||
this, I18n.getString("termora.doorman.password-wrong"),
|
||||
this, MigrationI18n.getString("termora.doorman.password-wrong"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
@@ -197,7 +197,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
isModal = true
|
||||
isResizable = true
|
||||
controlsVisible = false
|
||||
title = I18n.getString("termora.doorman.mnemonic.title")
|
||||
title = MigrationI18n.getString("termora.doorman.mnemonic.title")
|
||||
init()
|
||||
pack()
|
||||
size = Dimension(max(size.width, UIManager.getInt("Dialog.width") - 250), size.height)
|
||||
@@ -251,7 +251,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
} catch (e: Exception) {
|
||||
OptionPane.showMessageDialog(
|
||||
this,
|
||||
I18n.getString("termora.doorman.mnemonic.incorrect"),
|
||||
MigrationI18n.getString("termora.doorman.mnemonic.incorrect"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
return
|
||||
|
||||
@@ -7,3 +7,18 @@ termora.plugins.migration.message=<html> \
|
||||
<h3 align="center">📎 For more information, please see: <a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||
</html>
|
||||
termora.plugins.migration.migrate=Migrate
|
||||
|
||||
# Doorman
|
||||
termora.doorman.safe=Data is encrypted
|
||||
termora.doorman.unlock-data=Enter password to unlock data
|
||||
termora.doorman.password-wrong=Wrong password
|
||||
termora.doorman.forget-password=Forgot password?
|
||||
termora.doorman.delete-data=Delete the data catalog and restart, This will lose all data
|
||||
termora.doorman.forget-password-message=Unlock data with a mnemonic. Without it, data cannot be accessed
|
||||
termora.doorman.have-a-mnemonic=I have a mnemonic
|
||||
termora.doorman.dont-have-a-mnemonic=I don't have a mnemonic
|
||||
termora.doorman.mnemonic-data-corrupted=Unable to decrypt data with the mnemonic, the data maybe corrupted
|
||||
|
||||
termora.doorman.mnemonic.title=Enter 12 mnemonic words
|
||||
termora.doorman.mnemonic.incorrect=Incorrect mnemonic
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
# Doorman
|
||||
termora.doorman.safe=Данные защифрованы
|
||||
termora.doorman.unlock-data=Введите пароль для разблокировки данных
|
||||
termora.doorman.verify-password=Введите пароль для проверки
|
||||
termora.doorman.password-wrong=Неверный пароль
|
||||
termora.doorman.password-correct=Пароль верный
|
||||
termora.doorman.unsafe=Данные не зашифрованы
|
||||
termora.doorman.lock-data=Спрашивать пароль при запуске
|
||||
termora.doorman.forget-password=Забыли пароль?
|
||||
termora.doorman.delete-data=Удалить данные и перезапустить, это приведет к потере всех данных
|
||||
termora.doorman.forget-password-message=Разблокировать данные с помощью мнемоники. Без него доступ к данным невозможен.
|
||||
termora.doorman.have-a-mnemonic=У меня есть мнемоники
|
||||
termora.doorman.dont-have-a-mnemonic=У меня нет мнемоники
|
||||
termora.doorman.mnemonic-data-corrupted=Невозможно расшифровать данные с помощью мнемоники, возможно, данные повреждены.
|
||||
|
||||
termora.doorman.mnemonic.title=Введите 12 слов мнемоники
|
||||
termora.doorman.mnemonic.incorrect=Неверные мнемоники
|
||||
@@ -7,3 +7,17 @@ termora.plugins.migration.message=<html> \
|
||||
<h3 align="center">📎 更多信息请查看:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||
</html>
|
||||
termora.plugins.migration.migrate=迁移
|
||||
|
||||
# Doorman
|
||||
termora.doorman.safe=数据已加密
|
||||
termora.doorman.unlock-data=输入密码解锁数据
|
||||
termora.doorman.password-wrong=密码错误
|
||||
termora.doorman.forget-password=忘记密码?
|
||||
termora.doorman.delete-data=删除数据目录后重新启动程序,这样会丢失所有数据
|
||||
termora.doorman.forget-password-message=通过助记词解锁数据,没有助记词则无法解锁
|
||||
termora.doorman.have-a-mnemonic=我有助记词
|
||||
termora.doorman.dont-have-a-mnemonic=我没有助记词
|
||||
termora.doorman.mnemonic-data-corrupted=无法从助记词解密数据,数据可能已经损坏
|
||||
|
||||
termora.doorman.mnemonic.title=输入 12 个助记词
|
||||
termora.doorman.mnemonic.incorrect=助记词错误
|
||||
|
||||
@@ -7,3 +7,18 @@ termora.plugins.migration.message=<html> \
|
||||
<h3 align="center">📎 更多資訊請參見:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||
</html>
|
||||
termora.plugins.migration.migrate=遷移
|
||||
|
||||
|
||||
# Doorman
|
||||
termora.doorman.safe=資料已加密
|
||||
termora.doorman.unlock-data=輸入密碼解鎖資料
|
||||
termora.doorman.password-wrong=密碼錯誤
|
||||
termora.doorman.forget-password=忘記密碼?
|
||||
termora.doorman.delete-data=刪除資料目錄後重新啟動程序,這樣會遺失所有數據
|
||||
termora.doorman.forget-password-message=透過助記詞解鎖數據,沒有助記詞則無法解鎖
|
||||
termora.doorman.have-a-mnemonic=我有助記詞
|
||||
termora.doorman.dont-have-a-mnemonic=我沒有助記詞
|
||||
termora.doorman.mnemonic-data-corrupted=無法從助記詞解密數據,資料可能已損壞
|
||||
termora.doorman.mnemonic.title=輸入 12 個助記詞
|
||||
termora.doorman.mnemonic.incorrect=助記詞錯誤
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.1"
|
||||
project.version = "0.0.2"
|
||||
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora.plugins.obs
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
@@ -13,7 +14,7 @@ class OBSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
|
||||
return OBSProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return OBSProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@ plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.2"
|
||||
project.version = "0.0.3"
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
implementation("com.aliyun.oss:aliyun-sdk-oss:3.18.2")
|
||||
implementation("com.aliyun.oss:aliyun-sdk-oss:3.18.3")
|
||||
implementation("javax.xml.bind:jaxb-api:2.3.1")
|
||||
implementation("javax.activation:activation:1.1.1")
|
||||
implementation("org.glassfish.jaxb:jaxb-runtime:2.3.3")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora.plugins.oss
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
@@ -13,7 +14,7 @@ class OSSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
|
||||
return OSSProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return OSSProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.5"
|
||||
project.version = "0.0.6"
|
||||
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora.plugins.s3
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
@@ -13,7 +14,7 @@ class S3ProtocolHostPanelExtension private constructor() : ProtocolHostPanelExte
|
||||
return S3ProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return S3ProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
17
plugins/serial/build.gradle.kts
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
|
||||
|
||||
project.version = "0.0.2"
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
implementation("com.fazecast:jSerialComm:2.11.2")
|
||||
}
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.plugin.internal.AltKeyModifier
|
||||
import app.termora.plugin.internal.BasicGeneralOption
|
||||
import app.termora.plugin.internal.BasicTerminalOption
|
||||
import com.fazecast.jSerialComm.SerialPort
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
@@ -15,12 +17,15 @@ import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.nio.charset.Charset
|
||||
import javax.swing.*
|
||||
|
||||
class SerialHostOptionsPane : OptionsPane() {
|
||||
private val generalOption = BasicGeneralOption()
|
||||
private val terminalOption = TerminalOption()
|
||||
private val terminalOption = BasicTerminalOption().apply {
|
||||
showCharsetComboBox = true
|
||||
showStartupCommandTextField = true
|
||||
init()
|
||||
}
|
||||
private val serialCommOption = SerialCommOption()
|
||||
|
||||
init {
|
||||
@@ -48,6 +53,10 @@ class SerialHostOptionsPane : OptionsPane() {
|
||||
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
||||
startupCommand = terminalOption.startupCommandTextField.text,
|
||||
serialComm = serialComm,
|
||||
extras = mutableMapOf(
|
||||
"altModifier" to (terminalOption.altModifierComboBox.selectedItem?.toString()
|
||||
?: AltKeyModifier.EightBit.name),
|
||||
)
|
||||
)
|
||||
|
||||
return Host(
|
||||
@@ -128,67 +137,6 @@ class SerialHostOptionsPane : OptionsPane() {
|
||||
}
|
||||
|
||||
|
||||
protected inner class TerminalOption : JPanel(BorderLayout()), Option {
|
||||
val charsetComboBox = JComboBox<String>()
|
||||
val startupCommandTextField = OutlineTextField()
|
||||
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
|
||||
|
||||
for (e in Charset.availableCharsets()) {
|
||||
charsetComboBox.addItem(e.key)
|
||||
}
|
||||
|
||||
charsetComboBox.selectedItem = "UTF-8"
|
||||
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.terminal
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.new-host.terminal")
|
||||
}
|
||||
|
||||
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("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows)
|
||||
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
|
||||
.apply { rows += step }
|
||||
.build()
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected inner class SerialCommOption : JPanel(BorderLayout()), Option {
|
||||
val serialPortComboBox = OutlineComboBox<String>()
|
||||
val baudRateComboBox = OutlineComboBox<Int>()
|
||||
@@ -0,0 +1,32 @@
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.Plugin
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
internal class SerialPlugin : Plugin {
|
||||
private val support = ExtensionSupport()
|
||||
|
||||
override fun getAuthor(): String {
|
||||
return "TermoraDev"
|
||||
}
|
||||
|
||||
init {
|
||||
support.addExtension(ProtocolProviderExtension::class.java) { SerialProtocolProviderExtension.instance }
|
||||
support.addExtension(ProtocolHostPanelExtension::class.java) { SerialProtocolHostPanelExtension.instance }
|
||||
}
|
||||
|
||||
|
||||
override fun getName(): String {
|
||||
return "Serial Comm"
|
||||
}
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.termora
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.terminal.PtyConnector
|
||||
import com.fazecast.jSerialComm.SerialPort
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.Disposer
|
||||
import app.termora.Host
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
@@ -14,8 +15,11 @@ internal class SerialProtocolHostPanelExtension private constructor() : Protocol
|
||||
return SerialProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return SerialProtocolHostPanel()
|
||||
}
|
||||
|
||||
override fun ordered(): Long {
|
||||
return 5
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.actions.DataProvider
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
@@ -1,6 +1,9 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.Host
|
||||
import app.termora.Icons
|
||||
import app.termora.PtyHostTerminalTab
|
||||
import app.termora.WindowScope
|
||||
import app.termora.terminal.PtyConnector
|
||||
import org.apache.commons.io.Charsets
|
||||
import java.nio.charset.StandardCharsets
|
||||
@@ -1,5 +1,8 @@
|
||||
package app.termora
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.Host
|
||||
import app.termora.SerialCommFlowControl
|
||||
import app.termora.SerialCommParity
|
||||
import com.fazecast.jSerialComm.SerialPort
|
||||
|
||||
object Serials {
|
||||
22
plugins/serial/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<termora-plugin>
|
||||
|
||||
<id>serial</id>
|
||||
|
||||
<name>Serial Comm</name>
|
||||
|
||||
<version>${projectVersion}</version>
|
||||
|
||||
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||
|
||||
<entry>app.termora.plugins.serial.SerialPlugin</entry>
|
||||
|
||||
<descriptions>
|
||||
<description>Supports access to serial ports</description>
|
||||
<description language="zh_CN">支持访问串口</description>
|
||||
<description language="zh_TW">支援訪問串口</description>
|
||||
</descriptions>
|
||||
|
||||
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||
|
||||
|
||||
</termora-plugin>
|
||||
@@ -0,0 +1 @@
|
||||
<svg t="1747210120200" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1169" width="16" height="16"><path d="M806.11718723 531.44140652l-78.2578125 78.50390571L410.22265625 291.21874973l82.08984402-78.53906223a231.46874973 231.46874973 0 0 1 162.49218723-68.66015625 206.54296902 206.54296902 0 0 1 131.02734402 44.296875L856.98828125 117.125a35.15625027 35.15625027 0 0 1 49.67578125-0.03515652h0.03515652a35.15625027 35.15625027 0 0 1 0 49.74609429l-74.35546875 74.35546875a225.21093777 225.21093777 0 0 1-26.22656304 290.25z m-191.63671875 27.07031223a24.57421875 24.57421875 0 0 1-1.51171821 33.08203098l-57.72656277 57.76171929 63.98437473 63.73828071-83.42578125 83.46093777a230.62499973 230.62499973 0 0 1-161.71874946 68.66015625 204.92578125 204.92578125 0 0 1-136.75781304-49.21874973l-95.83593723 95.80078071a24.18750027 24.18750027 0 0 1-34.24218723-0.07031223 24.890625 24.890625 0 0 1-0.35156277-34.76953098l95.94140598-100.12500027a225.98437473 225.98437473 0 0 1 21.09375-299.28515598L305.98437473 394.0859375l68.66015625 68.66015625L427.13281277 411.10156277a24.39843777 24.39843777 0 0 1 34.34765598 34.59375L409.94140652 497.234375l117.9140625 117.63281277 56.53125-57.5859375a20.28515652 20.28515652 0 0 1 30.09374946 1.23046848z m-112.32421875 199.19531223l44.05078179-44.05078098-240.22265679-240.46875-44.296875 44.57812473a169.91015625 169.91015625 0 0 0-4.67578071 240.18750027l4.640625 4.92187473a151.875 151.875 0 0 0 117.9140625 48.97265652 175.35937473 175.35937473 0 0 0 122.58984321-54.70312473v0.56249946zM933.875 512c0 232.98046875-188.89453125 421.875-421.875 421.875-7.87499973 0-15.71484375-0.2109375-23.48437473-0.6328125l73.12499946-73.16015598a351.77343777 351.77343777 0 0 0 298.44140679-298.40625027l73.12499946-73.16015598c0.45703152 7.76953098 0.66796902 15.609375 0.66796902 23.48437473zM535.48437473 90.7578125L462.35937527 163.953125a351.77343777 351.77343777 0 0 0-298.44140679 298.40625027l-73.12499946 73.16015598A428.625 428.625 0 0 1 90.125 512C90.125 279.01953125 279.01953125 90.125 512 90.125c7.87499973 0 15.71484375 0.2109375 23.48437473 0.6328125z" fill="#6C707E" p-id="1170"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg t="1747210120200" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1169"
|
||||
width="16" height="16">
|
||||
<path d="M806.11718723 531.44140652l-78.2578125 78.50390571L410.22265625 291.21874973l82.08984402-78.53906223a231.46874973 231.46874973 0 0 1 162.49218723-68.66015625 206.54296902 206.54296902 0 0 1 131.02734402 44.296875L856.98828125 117.125a35.15625027 35.15625027 0 0 1 49.67578125-0.03515652h0.03515652a35.15625027 35.15625027 0 0 1 0 49.74609429l-74.35546875 74.35546875a225.21093777 225.21093777 0 0 1-26.22656304 290.25z m-191.63671875 27.07031223a24.57421875 24.57421875 0 0 1-1.51171821 33.08203098l-57.72656277 57.76171929 63.98437473 63.73828071-83.42578125 83.46093777a230.62499973 230.62499973 0 0 1-161.71874946 68.66015625 204.92578125 204.92578125 0 0 1-136.75781304-49.21874973l-95.83593723 95.80078071a24.18750027 24.18750027 0 0 1-34.24218723-0.07031223 24.890625 24.890625 0 0 1-0.35156277-34.76953098l95.94140598-100.12500027a225.98437473 225.98437473 0 0 1 21.09375-299.28515598L305.98437473 394.0859375l68.66015625 68.66015625L427.13281277 411.10156277a24.39843777 24.39843777 0 0 1 34.34765598 34.59375L409.94140652 497.234375l117.9140625 117.63281277 56.53125-57.5859375a20.28515652 20.28515652 0 0 1 30.09374946 1.23046848z m-112.32421875 199.19531223l44.05078179-44.05078098-240.22265679-240.46875-44.296875 44.57812473a169.91015625 169.91015625 0 0 0-4.67578071 240.18750027l4.640625 4.92187473a151.875 151.875 0 0 0 117.9140625 48.97265652 175.35937473 175.35937473 0 0 0 122.58984321-54.70312473v0.56249946zM933.875 512c0 232.98046875-188.89453125 421.875-421.875 421.875-7.87499973 0-15.71484375-0.2109375-23.48437473-0.6328125l73.12499946-73.16015598a351.77343777 351.77343777 0 0 0 298.44140679-298.40625027l73.12499946-73.16015598c0.45703152 7.76953098 0.66796902 15.609375 0.66796902 23.48437473zM535.48437473 90.7578125L462.35937527 163.953125a351.77343777 351.77343777 0 0 0-298.44140679 298.40625027l-73.12499946 73.16015598A428.625 428.625 0 0 1 90.125 512C90.125 279.01953125 279.01953125 90.125 512 90.125c7.87499973 0 15.71484375 0.2109375 23.48437473 0.6328125z"
|
||||
fill="#CED0D6" p-id="1170"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -2,7 +2,7 @@ plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.1"
|
||||
project.version = "0.0.3"
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.transfer.s3.S3FileSystem
|
||||
import app.termora.transfer.s3.S3Path
|
||||
import com.hierynomus.smbj.session.Session
|
||||
import com.hierynomus.smbj.share.DiskShare
|
||||
|
||||
class SMBFileSystem(private val share: DiskShare, session: Session) :
|
||||
S3FileSystem(SMBFileSystemProvider(share, session)) {
|
||||
|
||||
override fun create(root: String?, names: List<String>): S3Path {
|
||||
val path = SMBPath(this, root, names)
|
||||
if (names.isEmpty()) {
|
||||
path.attributes = path.attributes.copy(directory = true)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
|
||||
override fun close() {
|
||||
share.close()
|
||||
super.close()
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.transfer.s3.S3Path
|
||||
|
||||
class SMBPath(fileSystem: SMBFileSystem, 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,5 +1,6 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
@@ -13,7 +14,7 @@ class SMBProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
|
||||
return SMBProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return SMBProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.2"
|
||||
project.version = "0.0.3"
|
||||
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -2,7 +2,7 @@ plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.1"
|
||||
project.version = "0.0.2"
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora.plugins.webdav
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
@@ -13,7 +14,7 @@ class WebDAVProtocolHostPanelExtension private constructor() : ProtocolHostPanel
|
||||
return WebDAVProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return WebDAVProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ include("plugins:s3")
|
||||
include("plugins:oss")
|
||||
include("plugins:cos")
|
||||
include("plugins:obs")
|
||||
//include("plugins:ftp")
|
||||
include("plugins:ftp")
|
||||
include("plugins:bg")
|
||||
include("plugins:sync")
|
||||
include("plugins:migration")
|
||||
@@ -15,3 +15,4 @@ include("plugins:editor")
|
||||
include("plugins:geo")
|
||||
include("plugins:webdav")
|
||||
include("plugins:smb")
|
||||
include("plugins:serial")
|
||||
|
||||
@@ -37,7 +37,7 @@ record ExtensionProxy(Plugin plugin, Extension extension) implements InvocationH
|
||||
}
|
||||
throw new IllegalCallerException(target.getMessage(), target);
|
||||
}
|
||||
throw e;
|
||||
throw target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,6 @@ object Actions {
|
||||
*/
|
||||
const val KEY_MANAGER = "KeyManagerAction"
|
||||
|
||||
/**
|
||||
* 更新
|
||||
*/
|
||||
const val APP_UPDATE = "AppUpdateAction"
|
||||
|
||||
|
||||
/**
|
||||
* 宏
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.actions.ActionManager
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.keymap.KeymapManager
|
||||
import app.termora.plugin.ExtensionManager
|
||||
import app.termora.plugin.PluginManager
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
@@ -22,19 +20,17 @@ import org.apache.commons.lang3.LocaleUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import org.json.JSONObject
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.MenuItem
|
||||
import java.awt.PopupMenu
|
||||
import java.awt.SystemTray
|
||||
import java.awt.TrayIcon
|
||||
import java.awt.*
|
||||
import java.awt.desktop.AppReopenedEvent
|
||||
import java.awt.desktop.AppReopenedListener
|
||||
import java.awt.desktop.SystemEventListener
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.WindowEvent
|
||||
import java.awt.event.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.*
|
||||
import javax.swing.event.PopupMenuEvent
|
||||
import javax.swing.event.PopupMenuListener
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class ApplicationRunner {
|
||||
@@ -57,12 +53,6 @@ class ApplicationRunner {
|
||||
// 统计
|
||||
enableAnalytics()
|
||||
|
||||
// init ActionManager、KeymapManager、VFS
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
ActionManager.getInstance()
|
||||
KeymapManager.getInstance()
|
||||
}
|
||||
|
||||
// 设置 LAF
|
||||
setupLaf()
|
||||
|
||||
@@ -123,16 +113,63 @@ class ApplicationRunner {
|
||||
if (!SystemInfo.isWindows || !SystemTray.isSupported()) return
|
||||
|
||||
val tray = SystemTray.getSystemTray()
|
||||
val image = ImageIO.read(TermoraFrame::class.java.getResourceAsStream("/icons/termora_16x16.png"))
|
||||
val image = ImageIO.read(TermoraFrame::class.java.getResourceAsStream("/icons/termora_32x32.png"))
|
||||
val trayIcon = TrayIcon(image)
|
||||
val popupMenu = PopupMenu()
|
||||
trayIcon.popupMenu = popupMenu
|
||||
val dialog = JDialog()
|
||||
val trayPopup = JPopupMenu()
|
||||
|
||||
dialog.isUndecorated = true
|
||||
dialog.isModal = false
|
||||
dialog.size = Dimension(0, 0)
|
||||
|
||||
trayIcon.isImageAutoSize = true
|
||||
trayIcon.toolTip = Application.getName()
|
||||
|
||||
// PopupMenu 不支持中文
|
||||
val exitMenu = MenuItem("Exit")
|
||||
exitMenu.addActionListener { SwingUtilities.invokeLater { quitHandler() } }
|
||||
popupMenu.add(exitMenu)
|
||||
trayPopup.add(I18n.getString("termora.exit")).addActionListener { quitHandler() }
|
||||
trayPopup.addPopupMenuListener(object : PopupMenuListener {
|
||||
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent?) {
|
||||
|
||||
}
|
||||
|
||||
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent?) {
|
||||
SwingUtilities.invokeLater {
|
||||
if (dialog.isVisible) {
|
||||
dialog.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun popupMenuCanceled(e: PopupMenuEvent?) {
|
||||
popupMenuWillBecomeInvisible(e)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
trayIcon.addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseReleased(e: MouseEvent) {
|
||||
maybeShowPopup(e)
|
||||
}
|
||||
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
maybeShowPopup(e)
|
||||
}
|
||||
|
||||
private fun maybeShowPopup(e: MouseEvent) {
|
||||
if (e.isPopupTrigger) {
|
||||
val mouseLocation = MouseInfo.getPointerInfo().location
|
||||
trayPopup.setLocation(mouseLocation.x, mouseLocation.y)
|
||||
trayPopup.setInvoker(dialog)
|
||||
dialog.isVisible = true
|
||||
trayPopup.isVisible = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
dialog.addWindowFocusListener(object : WindowAdapter() {
|
||||
override fun windowLostFocus(e: WindowEvent) {
|
||||
dialog.isVisible = false
|
||||
}
|
||||
})
|
||||
|
||||
// double click
|
||||
trayIcon.addActionListener(object : AbstractAction() {
|
||||
@@ -173,7 +210,6 @@ class ApplicationRunner {
|
||||
private fun setupLaf() {
|
||||
|
||||
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux || SystemInfo.isWindows}")
|
||||
System.setProperty(FlatSystemProperties.USE_ROUNDED_POPUP_BORDER, "false")
|
||||
|
||||
if (SystemInfo.isLinux) {
|
||||
JFrame.setDefaultLookAndFeelDecorated(true)
|
||||
@@ -197,12 +233,13 @@ class ApplicationRunner {
|
||||
|
||||
themeManager.change(theme, true)
|
||||
|
||||
|
||||
FlatInspector.install("ctrl shift X")
|
||||
if (Application.isBetaVersion()) {
|
||||
FlatInspector.install("ctrl shift X")
|
||||
}
|
||||
|
||||
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
|
||||
UIManager.put("TitlePane.useWindowDecorations", false)
|
||||
UIManager.put(FlatClientProperties.POPUP_FORCE_HEAVY_WEIGHT, true)
|
||||
|
||||
UIManager.put("Component.arc", 5)
|
||||
UIManager.put("TextComponent.arc", UIManager.getInt("Component.arc"))
|
||||
@@ -213,7 +250,6 @@ class ApplicationRunner {
|
||||
UIManager.put("Dialog.width", 650)
|
||||
UIManager.put("Dialog.height", 550)
|
||||
|
||||
|
||||
if (SystemInfo.isMacOS) {
|
||||
UIManager.put("TabbedPane.tabHeight", UIManager.getInt("TitleBar.height"))
|
||||
} else if (SystemInfo.isLinux) {
|
||||
@@ -231,15 +267,33 @@ class ApplicationRunner {
|
||||
UIManager.put("Table.rowHeight", 24)
|
||||
UIManager.put("Table.focusCellHighlightBorder", FlatTableCellBorder.Default())
|
||||
UIManager.put("Table.focusSelectedCellHighlightBorder", FlatTableCellBorder.Default())
|
||||
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
||||
|
||||
UIManager.put("Tree.rowHeight", 24)
|
||||
UIManager.put("Tree.background", DynamicColor("window"))
|
||||
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("Tree.showCellFocusIndicator", false)
|
||||
UIManager.put("Tree.repaintWholeRow", true)
|
||||
|
||||
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
||||
// Linux 更多的是尖锐风格
|
||||
if (SystemInfo.isMacOS || SystemInfo.isWindows) {
|
||||
val selectionInsets = Insets(0, 2, 0, 2)
|
||||
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("Tree.selectionInsets", selectionInsets)
|
||||
|
||||
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("List.selectionInsets", selectionInsets)
|
||||
|
||||
UIManager.put("ComboBox.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("ComboBox.selectionInsets", selectionInsets)
|
||||
|
||||
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("Table.selectionInsets", selectionInsets)
|
||||
|
||||
UIManager.put("MenuBar.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("MenuBar.selectionInsets", selectionInsets)
|
||||
|
||||
UIManager.put("MenuItem.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("MenuItem.selectionInsets", selectionInsets)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class BannerPanel(fontSize: Int = 11, val beautiful: Boolean = false) : JCompone
|
||||
size = preferredSize
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
public override fun paintComponent(g: Graphics) {
|
||||
if (g is Graphics2D) {
|
||||
g.setRenderingHints(
|
||||
RenderingHints(
|
||||
@@ -33,7 +33,7 @@ class BannerPanel(fontSize: Int = 11, val beautiful: Boolean = false) : JCompone
|
||||
}
|
||||
|
||||
g.font = font
|
||||
g.color = UIManager.getColor("TextField.placeholderForeground")
|
||||
g.color = foreground ?: UIManager.getColor("TextField.placeholderForeground")
|
||||
|
||||
val height = g.fontMetrics.height
|
||||
val descent = g.fontMetrics.descent
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.actions.MultipleAction
|
||||
import app.termora.database.DatabaseManager
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
@@ -18,10 +15,10 @@ import javax.swing.event.ListDataListener
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class CustomizeToolBarDialog(
|
||||
internal class CustomizeToolBarDialog(
|
||||
owner: Window,
|
||||
private val windowScope: WindowScope,
|
||||
private val toolbar: TermoraToolBar
|
||||
private val model: TermoraToolbarModel,
|
||||
) : DialogWrapper(owner) {
|
||||
|
||||
private val moveTopBtn = JButton(Icons.moveUp)
|
||||
@@ -40,6 +37,7 @@ class CustomizeToolBarDialog(
|
||||
private val actionManager get() = ActionManager.getInstance()
|
||||
|
||||
private var isOk = false
|
||||
private val actions = mutableListOf<ToolBarAction>()
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 150, UIManager.getInt("Dialog.height") - 100)
|
||||
@@ -147,7 +145,7 @@ class CustomizeToolBarDialog(
|
||||
resetBtn.addActionListener {
|
||||
leftList.model.removeAllElements()
|
||||
rightList.model.removeAllElements()
|
||||
for (action in toolbar.getAllActions()) {
|
||||
for (action in model.getAllActions()) {
|
||||
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
|
||||
}
|
||||
}
|
||||
@@ -258,7 +256,7 @@ class CustomizeToolBarDialog(
|
||||
override fun windowOpened(e: WindowEvent) {
|
||||
removeWindowListener(this)
|
||||
|
||||
for (action in toolbar.getActions()) {
|
||||
for (action in model.getActions()) {
|
||||
if (action.visible) {
|
||||
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
|
||||
} else {
|
||||
@@ -271,12 +269,7 @@ class CustomizeToolBarDialog(
|
||||
}
|
||||
|
||||
private fun getActionHolder(actionId: String): ActionHolder? {
|
||||
var action = actionManager.getAction(actionId)
|
||||
if (action == null) {
|
||||
if (actionId == MultipleAction.MULTIPLE) {
|
||||
action = MultipleAction.getInstance(windowScope)
|
||||
}
|
||||
}
|
||||
val action = actionManager.getAction(actionId)
|
||||
if (action == null) return null
|
||||
return ActionHolder(actionId, action)
|
||||
}
|
||||
@@ -365,12 +358,14 @@ class CustomizeToolBarDialog(
|
||||
actions.add(ToolBarAction(leftList.model.getElementAt(i).id, false))
|
||||
}
|
||||
|
||||
DatabaseManager.getInstance()
|
||||
.properties.putString("Termora.ToolBar.Actions", ohMyJson.encodeToString(actions))
|
||||
this.actions.clear()
|
||||
this.actions.addAll(actions)
|
||||
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
fun getActions()=actions
|
||||
|
||||
fun open(): Boolean {
|
||||
isModal = true
|
||||
isVisible = true
|
||||
|
||||
71
src/main/kotlin/app/termora/FontComboBox.kt
Normal file
@@ -0,0 +1,71 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||
import com.formdev.flatlaf.util.FontUtils
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import javax.swing.DefaultListCellRenderer
|
||||
import javax.swing.JList
|
||||
import javax.swing.event.PopupMenuEvent
|
||||
import javax.swing.event.PopupMenuListener
|
||||
|
||||
class FontComboBox : FlatComboBox<String>() {
|
||||
private var fontsLoaded = false
|
||||
|
||||
init {
|
||||
val fontComboBox = this
|
||||
fontComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
init {
|
||||
preferredSize = Dimension(preferredSize.width, fontComboBox.preferredSize.height - 2)
|
||||
maximumSize = Dimension(preferredSize.width, preferredSize.height)
|
||||
}
|
||||
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = value
|
||||
if (text is String) {
|
||||
if (text.isBlank()) {
|
||||
text = "<None>"
|
||||
}
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
"<html><font face='$text'>$text</font></html>",
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
|
||||
}
|
||||
}
|
||||
fontComboBox.maximumSize = fontComboBox.preferredSize
|
||||
|
||||
fontComboBox.addPopupMenuListener(object : PopupMenuListener {
|
||||
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
||||
if (fontsLoaded) return
|
||||
val selectedItem = fontComboBox.selectedItem
|
||||
val families = getItems()
|
||||
for (family in FontUtils.getAvailableFontFamilyNames()) {
|
||||
if (families.contains(family).not()) fontComboBox.addItem(family)
|
||||
}
|
||||
fontComboBox.selectedItem = selectedItem
|
||||
fontsLoaded = true
|
||||
}
|
||||
|
||||
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {}
|
||||
override fun popupMenuCanceled(e: PopupMenuEvent) {}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fun getItems(): Set<String> {
|
||||
val families = mutableSetOf<String>()
|
||||
for (i in 0 until itemCount) families.add(getItemAt(i))
|
||||
return families
|
||||
}
|
||||
}
|
||||
21
src/main/kotlin/app/termora/FramePlugin.kt
Normal file
@@ -0,0 +1,21 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.database.DatabaseChangedExtension
|
||||
import app.termora.database.DatabasePropertiesChangedExtension
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.InternalPlugin
|
||||
|
||||
internal class FramePlugin : InternalPlugin() {
|
||||
init {
|
||||
support.addExtension(DatabasePropertiesChangedExtension::class.java) { KeymapRefresher.getInstance() }
|
||||
support.addExtension(DatabaseChangedExtension::class.java) { KeymapRefresher.getInstance() }
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
return "Frame"
|
||||
}
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
}
|
||||
@@ -344,6 +344,11 @@ data class Host(
|
||||
|
||||
val isFolder get() = StringUtils.equalsIgnoreCase(protocol, "Folder")
|
||||
|
||||
/**
|
||||
* 临时的 SSH 不可以保存
|
||||
*/
|
||||
val isTemporary get() = options.extras["Temporary"] != null
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
@@ -21,9 +21,13 @@ class HostManager private constructor() : Disposable {
|
||||
*/
|
||||
fun addHost(host: Host, source: DatabaseChangedExtension.Source = DatabaseChangedExtension.Source.User) {
|
||||
assertEventDispatchThread()
|
||||
if (host.ownerType.isBlank()) {
|
||||
|
||||
if (host.isTemporary)
|
||||
throw IllegalArgumentException("Temporary host")
|
||||
|
||||
if (host.ownerType.isBlank())
|
||||
throw IllegalArgumentException("Owner type cannot be null")
|
||||
}
|
||||
|
||||
databaseManager.saveAndIncrementVersion(
|
||||
Data(
|
||||
id = host.id,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.terminal.*
|
||||
@@ -8,7 +9,9 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.beans.PropertyChangeEvent
|
||||
import java.util.*
|
||||
import javax.swing.Icon
|
||||
|
||||
abstract class HostTerminalTab(
|
||||
@@ -20,6 +23,10 @@ abstract class HostTerminalTab(
|
||||
val Host = DataKey(app.termora.Host::class)
|
||||
}
|
||||
|
||||
|
||||
protected val terminalTabbedManager
|
||||
get() = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent()))
|
||||
.getData(DataProviders.TerminalTabbedManager)
|
||||
protected val coroutineScope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.Swing) }
|
||||
protected val terminalModel get() = terminal.getTerminalModel()
|
||||
protected var unread = false
|
||||
@@ -42,7 +49,10 @@ abstract class HostTerminalTab(
|
||||
if (hasFocus || unread) {
|
||||
return
|
||||
}
|
||||
unread = true
|
||||
// 如果当前选中的不是这个 Tab,那么设置成未读
|
||||
if (terminalTabbedManager?.getSelectedTerminalTab() != this@HostTerminalTab) {
|
||||
unread = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@ object I18n : AbstractI18n() {
|
||||
"en_US" to "English",
|
||||
"zh_CN" to "简体中文",
|
||||
"zh_TW" to "繁體中文",
|
||||
"ru_RU" to "Русский",
|
||||
)
|
||||
|
||||
fun containsLanguage(locale: Locale): String? {
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.termora
|
||||
|
||||
object Icons {
|
||||
val bulletList by lazy { DynamicIcon("icons/bulletList.svg", "icons/bulletList_dark.svg") }
|
||||
val dataColumn by lazy { DynamicIcon("icons/dataColumn.svg", "icons/dataColumn_dark.svg") }
|
||||
val dbms by lazy { DynamicIcon("icons/dbms.svg", "icons/dbms_dark.svg") }
|
||||
val newUI by lazy { DynamicIcon("icons/newUI.svg", "icons/newUI.svg") }
|
||||
val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") }
|
||||
@@ -33,6 +34,7 @@ object Icons {
|
||||
val empty by lazy { DynamicIcon("icons/empty.svg") }
|
||||
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") }
|
||||
val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") }
|
||||
val breakpoint by lazy { DynamicIcon("icons/breakpoint.svg", "icons/breakpoint_dark.svg") }
|
||||
val softWrap by lazy { DynamicIcon("icons/softWrap.svg", "icons/softWrap_dark.svg") }
|
||||
val scrollUp by lazy { DynamicIcon("icons/scrollUp.svg", "icons/scrollUp_dark.svg") }
|
||||
val reformatCode by lazy { DynamicIcon("icons/reformatCode.svg", "icons/reformatCode_dark.svg") }
|
||||
@@ -77,6 +79,7 @@ object Icons {
|
||||
val import by lazy { DynamicIcon("icons/import.svg", "icons/import_dark.svg") }
|
||||
val export by lazy { DynamicIcon("icons/export.svg", "icons/export_dark.svg") }
|
||||
val terminal by lazy { DynamicIcon("icons/terminal.svg", "icons/terminal_dark.svg") }
|
||||
val telnet by lazy { DynamicIcon("icons/telnet.svg", "icons/telnet_dark.svg") }
|
||||
val ssh by lazy { DynamicIcon("icons/ssh.svg", "icons/ssh_dark.svg") }
|
||||
val ftp by lazy { DynamicIcon("icons/ftp.svg", "icons/ftp_dark.svg") }
|
||||
val minio by lazy { DynamicIcon("icons/minio.svg", "icons/minio_dark.svg") }
|
||||
|
||||
126
src/main/kotlin/app/termora/JSplitPaneWithZeroSizeDivider.kt
Normal file
@@ -0,0 +1,126 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.ui.FlatSplitPaneUI
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Cursor
|
||||
import java.awt.Graphics
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.MouseMotionAdapter
|
||||
import java.util.function.Supplier
|
||||
import javax.swing.*
|
||||
|
||||
class SplitPaneUI : FlatSplitPaneUI() {
|
||||
public override fun startDragging() {
|
||||
super.startDragging()
|
||||
}
|
||||
|
||||
public override fun dragDividerTo(location: Int) {
|
||||
super.dragDividerTo(location)
|
||||
}
|
||||
|
||||
public override fun finishDraggingTo(location: Int) {
|
||||
super.finishDraggingTo(location)
|
||||
}
|
||||
}
|
||||
|
||||
class JSplitPaneWithZeroSizeDivider(
|
||||
private val splitPane: JSplitPane,
|
||||
private val topOffset: Supplier<Int>,
|
||||
) : JPanel(BorderLayout()) {
|
||||
|
||||
private val dividerDragSize = 7
|
||||
private val layeredPane = LayeredPane()
|
||||
private val divider = Divider()
|
||||
|
||||
init {
|
||||
layeredPane.add(splitPane, JLayeredPane.DEFAULT_LAYER as Any)
|
||||
layeredPane.add(divider, JLayeredPane.PALETTE_LAYER as Any)
|
||||
add(layeredPane, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
private inner class Divider : JComponent() {
|
||||
private var dragging = false
|
||||
private var dragStartX = 0
|
||||
private var initialDividerLocation = 0
|
||||
private val splitPaneUI get() = splitPane.ui as SplitPaneUI
|
||||
|
||||
init {
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR)
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (SwingUtilities.isLeftMouseButton(e)) {
|
||||
dragging = true
|
||||
dragStartX = e.xOnScreen
|
||||
initialDividerLocation = splitPane.dividerLocation
|
||||
splitPaneUI.startDragging()
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseReleased(e: MouseEvent) {
|
||||
if (SwingUtilities.isLeftMouseButton(e)) {
|
||||
if (dragging) {
|
||||
val deltaX = e.xOnScreen - dragStartX
|
||||
val newLocation = initialDividerLocation + deltaX
|
||||
splitPaneUI.finishDraggingTo(newLocation)
|
||||
}
|
||||
dragging = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addMouseMotionListener(object : MouseMotionAdapter() {
|
||||
override fun mouseDragged(e: MouseEvent) {
|
||||
if (dragging) {
|
||||
val deltaX = e.xOnScreen - dragStartX
|
||||
val newLocation = initialDividerLocation + deltaX
|
||||
splitPaneUI.dragDividerTo(newLocation)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun paint(g: Graphics) {
|
||||
g.color = UIManager.getColor("controlShadow")
|
||||
g.fillRect(width / 2, 0, 1, height)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private inner class LayeredPane : JLayeredPane() {
|
||||
private val w get() = (dividerDragSize - 1) / 2
|
||||
|
||||
override fun doLayout() {
|
||||
synchronized(treeLock) {
|
||||
for (c in components) {
|
||||
if (c == divider) {
|
||||
c.isVisible = splitPane.leftComponent.isVisible
|
||||
c.setBounds(
|
||||
splitPane.dividerLocation - w,
|
||||
topOffset.get(),
|
||||
dividerDragSize,
|
||||
height - topOffset.get()
|
||||
)
|
||||
} else {
|
||||
c.setBounds(0, 0, width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun paint(g: Graphics) {
|
||||
super.paint(g)
|
||||
if (divider.isVisible) {
|
||||
g.color = UIManager.getColor("controlShadow")
|
||||
g.fillRect(splitPane.dividerLocation, 0, 1, topOffset.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun doLayout() {
|
||||
super.doLayout()
|
||||
layeredPane.doLayout()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||