mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
42 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 |
52
.github/workflows/linux-aarch64.yml
vendored
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
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
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
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
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
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
|
||||
@@ -90,8 +90,6 @@ Termora is developed using [**Kotlin/JVM**](https://kotlinlang.org/) and partial
|
||||
We recommend using the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK for development.
|
||||
|
||||
- Run locally: `./gradlew :run`
|
||||
- Build for current OS: `./gradlew :dist`
|
||||
|
||||
|
||||
|
||||
## 📄 License
|
||||
|
||||
@@ -88,8 +88,6 @@ Termora 使用 [**Kotlin/JVM**](https://kotlinlang.org/) 开发,支持(正
|
||||
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK 运行环境。
|
||||
|
||||
- 本地运行:`./gradlew :run`
|
||||
- 构建当前系统安装包:`./gradlew :dist`
|
||||
|
||||
|
||||
|
||||
## 📄 授权协议
|
||||
|
||||
@@ -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)
|
||||
@@ -189,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
|
||||
@@ -484,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"))
|
||||
}
|
||||
|
||||
@@ -500,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) {
|
||||
@@ -523,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,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()
|
||||
|
||||
@@ -628,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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -658,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
|
||||
@@ -671,7 +623,7 @@ fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String,
|
||||
// @formatter:on
|
||||
|
||||
// sign zip
|
||||
if (macOSSign) signMacOSLocalFile(zipFile)
|
||||
signMacOSLocalFile(zipFile)
|
||||
|
||||
// 公证
|
||||
if (macOSNotary) {
|
||||
|
||||
@@ -4,10 +4,10 @@ slf4j = "2.0.17"
|
||||
pty4j = "0.13.6"
|
||||
tinylog = "2.7.0"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
flatlaf = "3.6"
|
||||
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"
|
||||
@@ -41,12 +41,12 @@ jSerialComm = "2.11.2"
|
||||
ini4j = "0.5.5-2"
|
||||
restart4j = "0.0.1"
|
||||
eddsa = "0.3.0"
|
||||
exposed = "1.0.0-beta-3"
|
||||
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"
|
||||
jsvg = "2.0.0"
|
||||
dom4j = "2.2.0"
|
||||
|
||||
[libraries]
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
IOUtils.closeQuietly(body)
|
||||
return@use tempFile
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
|
||||
TODO("Not yet implemented")
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
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.6"
|
||||
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")
|
||||
|
||||
@@ -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=助記詞錯誤
|
||||
|
||||
|
||||
17
plugins/serial/build.gradle.kts
Normal file
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>()
|
||||
@@ -1,11 +1,18 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.InternalPlugin
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.Plugin
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
internal class SerialInternalPlugin : InternalPlugin() {
|
||||
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 }
|
||||
@@ -13,7 +20,7 @@ internal class SerialInternalPlugin : InternalPlugin() {
|
||||
|
||||
|
||||
override fun getName(): String {
|
||||
return "Serial Protocol"
|
||||
return "Serial Comm"
|
||||
}
|
||||
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
@@ -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
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.2"
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -26,12 +24,13 @@ 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 {
|
||||
@@ -54,12 +53,6 @@ class ApplicationRunner {
|
||||
// 统计
|
||||
enableAnalytics()
|
||||
|
||||
// init ActionManager、KeymapManager、VFS
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
ActionManager.getInstance()
|
||||
KeymapManager.getInstance()
|
||||
}
|
||||
|
||||
// 设置 LAF
|
||||
setupLaf()
|
||||
|
||||
@@ -120,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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
21
src/main/kotlin/app/termora/FramePlugin.kt
Normal file
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,
|
||||
|
||||
@@ -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? {
|
||||
|
||||
65
src/main/kotlin/app/termora/KeymapRefresher.kt
Normal file
65
src/main/kotlin/app/termora/KeymapRefresher.kt
Normal file
@@ -0,0 +1,65 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.database.DatabaseChangedExtension
|
||||
import app.termora.database.DatabasePropertiesChangedExtension
|
||||
import app.termora.keymap.KeymapManager
|
||||
|
||||
internal class KeymapRefresher private constructor() : DatabasePropertiesChangedExtension, DatabaseChangedExtension {
|
||||
companion object {
|
||||
fun getInstance(): KeymapRefresher {
|
||||
return ApplicationScope.forApplicationScope()
|
||||
.getOrCreate(KeymapRefresher::class) { KeymapRefresher() }
|
||||
}
|
||||
}
|
||||
|
||||
private val listeners = mutableListOf<() -> Unit>()
|
||||
private var currentKeymap: String? = null
|
||||
private val keymapManager get() = KeymapManager.getInstance()
|
||||
private val activeKeymapName get() = keymapManager.getActiveKeymap().name
|
||||
|
||||
override fun onDataChanged(
|
||||
id: String,
|
||||
type: String,
|
||||
action: DatabaseChangedExtension.Action,
|
||||
source: DatabaseChangedExtension.Source
|
||||
) {
|
||||
if (type != "Keymap") return
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun onPropertyChanged(name: String, key: String, value: String) {
|
||||
if (name != "Setting.Properties") return
|
||||
if (key != "Keymap.Active") return
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
synchronized(this) {
|
||||
if (currentKeymap == activeKeymapName) {
|
||||
return
|
||||
}
|
||||
|
||||
currentKeymap = activeKeymapName
|
||||
|
||||
for (function in listeners) {
|
||||
function.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addRefreshListener(listener: () -> Unit): Disposable {
|
||||
synchronized(this) {
|
||||
listeners.add(listener)
|
||||
return object : Disposable {
|
||||
override fun dispose() {
|
||||
removeRefreshListener(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeRefreshListener(listener: () -> Unit) {
|
||||
synchronized(this) { listeners.remove(listener) }
|
||||
}
|
||||
|
||||
}
|
||||
215
src/main/kotlin/app/termora/LoginScriptPanel.kt
Normal file
215
src/main/kotlin/app/termora/LoginScriptPanel.kt
Normal file
@@ -0,0 +1,215 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatTable
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.awt.event.ActionEvent
|
||||
import javax.swing.*
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
import javax.swing.table.DefaultTableModel
|
||||
import kotlin.math.max
|
||||
|
||||
internal class LoginScriptPanel(private val loginScripts: MutableList<LoginScript>) : JPanel(BorderLayout()) {
|
||||
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||
|
||||
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
|
||||
private val editBtn = JButton(I18n.getString("termora.new-host.tunneling.edit"))
|
||||
private val deleteBtn = JButton(I18n.getString("termora.new-host.tunneling.delete"))
|
||||
private val table = FlatTable()
|
||||
private val model = object : DefaultTableModel() {
|
||||
override fun getRowCount(): Int {
|
||||
return loginScripts.size
|
||||
}
|
||||
|
||||
override fun isCellEditable(row: Int, column: Int): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
fun addRow(loginScript: LoginScript) {
|
||||
val rowCount = super.getRowCount()
|
||||
loginScripts.add(loginScript)
|
||||
super.fireTableRowsInserted(rowCount, rowCount + 1)
|
||||
}
|
||||
|
||||
override fun getValueAt(row: Int, column: Int): Any {
|
||||
val loginScript = loginScripts[row]
|
||||
return when (column) {
|
||||
0 -> loginScript.expect
|
||||
1 -> loginScript.send
|
||||
else -> super.getValueAt(row, column)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
|
||||
addBtn.isFocusable = false
|
||||
editBtn.isFocusable = false
|
||||
deleteBtn.isFocusable = false
|
||||
|
||||
deleteBtn.isEnabled = false
|
||||
editBtn.isEnabled = false
|
||||
|
||||
val scrollPane = JScrollPane(table)
|
||||
|
||||
model.addColumn(I18n.getString("termora.new-host.terminal.expect"))
|
||||
model.addColumn(I18n.getString("termora.new-host.terminal.send"))
|
||||
|
||||
table.putClientProperty(
|
||||
FlatClientProperties.STYLE, mapOf(
|
||||
"showHorizontalLines" to true,
|
||||
"showVerticalLines" to true,
|
||||
)
|
||||
)
|
||||
table.model = model
|
||||
table.autoResizeMode = JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS
|
||||
table.setDefaultRenderer(
|
||||
Any::class.java,
|
||||
DefaultTableCellRenderer().apply { horizontalAlignment = SwingConstants.CENTER })
|
||||
table.fillsViewportHeight = true
|
||||
scrollPane.border = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createEmptyBorder(4, 0, 4, 0),
|
||||
BorderFactory.createMatteBorder(1, 1, 1, 1, DynamicColor.Companion.BorderColor)
|
||||
)
|
||||
table.border = BorderFactory.createEmptyBorder()
|
||||
|
||||
|
||||
val box = Box.createHorizontalBox()
|
||||
box.add(addBtn)
|
||||
box.add(Box.createHorizontalStrut(4))
|
||||
box.add(editBtn)
|
||||
box.add(Box.createHorizontalStrut(4))
|
||||
box.add(deleteBtn)
|
||||
|
||||
add(scrollPane, BorderLayout.CENTER)
|
||||
add(box, BorderLayout.SOUTH)
|
||||
border = BorderFactory.createEmptyBorder(6, 8, 6, 8)
|
||||
}
|
||||
|
||||
|
||||
private fun initEvents() {
|
||||
addBtn.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val dialog = LoginScriptDialog(owner)
|
||||
dialog.isVisible = true
|
||||
model.addRow(dialog.loginScript ?: return)
|
||||
}
|
||||
})
|
||||
|
||||
editBtn.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val dialog = LoginScriptDialog(owner, loginScripts[table.selectedRow])
|
||||
dialog.isVisible = true
|
||||
loginScripts[table.selectedRow] = dialog.loginScript ?: return
|
||||
model.fireTableRowsUpdated(table.selectedRow, table.selectedRow)
|
||||
}
|
||||
})
|
||||
|
||||
deleteBtn.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val rows = table.selectedRows
|
||||
if (rows.isEmpty()) return
|
||||
rows.sortDescending()
|
||||
for (row in rows) {
|
||||
loginScripts.removeAt(row)
|
||||
model.fireTableRowsDeleted(row, row)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
table.selectionModel.addListSelectionListener {
|
||||
deleteBtn.isEnabled = table.selectedRowCount > 0
|
||||
editBtn.isEnabled = deleteBtn.isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
private inner class LoginScriptDialog(
|
||||
owner: Window,
|
||||
var loginScript: LoginScript? = null
|
||||
) : DialogWrapper(owner) {
|
||||
private val formMargin = "4dlu"
|
||||
private val expectTextField = OutlineTextField()
|
||||
private val sendTextField = OutlineTextField()
|
||||
private val regexToggleBtn = JToggleButton(Icons.regex)
|
||||
.apply { toolTipText = I18n.getString("termora.regex") }
|
||||
private val matchCaseToggleBtn = JToggleButton(Icons.matchCase)
|
||||
.apply { toolTipText = I18n.getString("termora.match-case") }
|
||||
|
||||
init {
|
||||
isModal = true
|
||||
title = I18n.getString("termora.new-host.terminal.login-scripts")
|
||||
controlsVisible = false
|
||||
|
||||
init()
|
||||
pack()
|
||||
size = Dimension(max(UIManager.getInt("Dialog.width") - 300, 250), preferredSize.height)
|
||||
setLocationRelativeTo(owner)
|
||||
|
||||
val toolbar = FlatToolBar().apply { isFloatable = false }
|
||||
toolbar.add(regexToggleBtn)
|
||||
toolbar.add(matchCaseToggleBtn)
|
||||
expectTextField.trailingComponent = toolbar
|
||||
expectTextField.placeholderText = I18n.getString("termora.optional")
|
||||
|
||||
val script = loginScript
|
||||
if (script != null) {
|
||||
expectTextField.text = script.expect
|
||||
sendTextField.text = script.send
|
||||
matchCaseToggleBtn.isSelected = script.matchCase
|
||||
regexToggleBtn.isSelected = script.regex
|
||||
}
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
if (sendTextField.text.isBlank()) {
|
||||
sendTextField.outline = "error"
|
||||
sendTextField.requestFocusInWindow()
|
||||
return
|
||||
}
|
||||
|
||||
loginScript = LoginScript(
|
||||
expect = expectTextField.text,
|
||||
send = sendTextField.text,
|
||||
matchCase = matchCaseToggleBtn.isSelected,
|
||||
regex = regexToggleBtn.isSelected,
|
||||
)
|
||||
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
override fun doCancelAction() {
|
||||
loginScript = null
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow",
|
||||
"pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
return FormBuilder.create().layout(layout).padding("0dlu, $formMargin, $formMargin, $formMargin")
|
||||
.add("${I18n.getString("termora.new-host.terminal.expect")}:").xy(1, rows)
|
||||
.add(expectTextField).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.new-host.terminal.send")}:").xy(1, rows)
|
||||
.add(sendTextField).xy(3, rows).apply { rows += step }
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,7 +27,7 @@ class MultipleTerminalListener : TerminalPaintListener {
|
||||
) {
|
||||
val windowScope = AnActionEvent(terminalPanel, StringUtils.EMPTY, EventObject(terminalPanel))
|
||||
.getData(DataProviders.WindowScope) ?: return
|
||||
if (!MultipleAction.getInstance(windowScope).isSelected) return
|
||||
if (MultipleAction.getInstance().isSelected(windowScope).not()) return
|
||||
|
||||
val oldFont = g.font
|
||||
val colorPalette = terminal.getTerminalModel().getColorPalette()
|
||||
|
||||
218
src/main/kotlin/app/termora/MyTermoraToolbar.kt
Normal file
218
src/main/kotlin/app/termora/MyTermoraToolbar.kt
Normal file
@@ -0,0 +1,218 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.actions.StateAction
|
||||
import app.termora.findeverywhere.FindEverywhereAction
|
||||
import app.termora.plugin.internal.update.AppUpdateAction
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import java.awt.AWTEvent
|
||||
import java.awt.Rectangle
|
||||
import java.awt.event.*
|
||||
import java.beans.PropertyChangeEvent
|
||||
import java.beans.PropertyChangeListener
|
||||
import javax.swing.*
|
||||
|
||||
internal class MyTermoraToolbar(private val windowScope: WindowScope, private val frame: TermoraFrame) : FlatToolBar() {
|
||||
|
||||
|
||||
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
|
||||
private val model get() = TermoraToolbarModel.getInstance()
|
||||
private val actionManager get() = model.getActionManager()
|
||||
private val toolbar get() = this
|
||||
|
||||
/**
|
||||
* 一次性生命周期 每次刷新都会重置
|
||||
*/
|
||||
private var disposable = Disposer.newDisposable()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
refreshActions()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
isFloatable = false
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
Disposer.register(windowScope, object : Disposable {
|
||||
override fun dispose() {
|
||||
Disposer.dispose(disposable)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// 监听全局事件
|
||||
toolkit.addAWTEventListener(customizeToolBarAWTEventListener, AWTEvent.MOUSE_EVENT_MASK)
|
||||
Disposer.register(windowScope, customizeToolBarAWTEventListener)
|
||||
|
||||
// 监听变化
|
||||
model.addTermoraToolbarModelListener(object : TermoraToolbarModel.TermoraToolbarModelListener {
|
||||
override fun onChanged() {
|
||||
refreshActions()
|
||||
}
|
||||
}).let { Disposer.register(windowScope, it) }
|
||||
|
||||
// 监听窗口大小变动,然后修改边距避开控制按钮
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentResized(e: ComponentEvent) {
|
||||
adjust()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshActions() {
|
||||
Disposer.dispose(disposable)
|
||||
disposable = Disposer.newDisposable()
|
||||
|
||||
removeAll()
|
||||
|
||||
add(JButton(object : AbstractAction() {
|
||||
init {
|
||||
putValue(SMALL_ICON, Icons.add)
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: ActionEvent) {
|
||||
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
|
||||
}
|
||||
}))
|
||||
|
||||
add(Box.createHorizontalGlue())
|
||||
|
||||
// update
|
||||
add(redirectUpdateAction(disposable))
|
||||
|
||||
for (action in model.getActions()) {
|
||||
if (action.visible.not()) continue
|
||||
val action = actionManager.getAction(action.id) ?: continue
|
||||
add(redirectAction(action, disposable))
|
||||
}
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
adjust()
|
||||
}
|
||||
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun adjust() {
|
||||
val rectangle = frame.rootPane.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
||||
as? Rectangle ?: return
|
||||
val right = rectangle.width
|
||||
|
||||
for (i in 0 until toolbar.componentCount) {
|
||||
val c = toolbar.getComponent(i)
|
||||
if (c.name == "spacing") {
|
||||
if (c.width == right) {
|
||||
return
|
||||
}
|
||||
toolbar.remove(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
val spacing = Box.createHorizontalStrut(right)
|
||||
spacing.name = "spacing"
|
||||
toolbar.add(spacing)
|
||||
}
|
||||
|
||||
private fun redirectUpdateAction(disposable: Disposable): AbstractButton {
|
||||
val action = AppUpdateAction.getInstance()
|
||||
val button = JButton(action.smallIcon)
|
||||
button.isVisible = action.isEnabled
|
||||
button.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
action.actionPerformed(e)
|
||||
}
|
||||
})
|
||||
|
||||
val listener = object : PropertyChangeListener, Disposable {
|
||||
override fun propertyChange(evt: PropertyChangeEvent) {
|
||||
button.isVisible = action.isEnabled
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
action.removePropertyChangeListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
action.addPropertyChangeListener(listener)
|
||||
Disposer.register(disposable, listener)
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
private fun redirectAction(action: Action, disposable: Disposable): AbstractButton {
|
||||
val button = if (action is StateAction) JToggleButton() else JButton()
|
||||
button.toolTipText = action.getValue(Action.SHORT_DESCRIPTION) as? String
|
||||
button.icon = action.getValue(Action.SMALL_ICON) as? Icon
|
||||
button.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
action.actionPerformed(e)
|
||||
if (action is StateAction) {
|
||||
button.isSelected = action.isSelected(windowScope)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val listener = object : PropertyChangeListener, Disposable {
|
||||
override fun propertyChange(evt: PropertyChangeEvent) {
|
||||
if (action is StateAction) {
|
||||
button.isSelected = action.isSelected(windowScope)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
action.removePropertyChangeListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
action.addPropertyChangeListener(listener)
|
||||
Disposer.register(disposable, listener)
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
/**
|
||||
* 对着 ToolBar 右键
|
||||
*/
|
||||
private inner class CustomizeToolBarAWTEventListener : AWTEventListener, Disposable {
|
||||
override fun eventDispatched(event: AWTEvent) {
|
||||
if (event !is MouseEvent || event.id != MouseEvent.MOUSE_CLICKED
|
||||
|| SwingUtilities.isRightMouseButton(event).not()
|
||||
) return
|
||||
|
||||
// 如果 ToolBar 没有显示
|
||||
if (toolbar.isShowing.not()) return
|
||||
|
||||
// 如果不是作用于在 ToolBar 上面
|
||||
if (Rectangle(toolbar.locationOnScreen, toolbar.size).contains(event.locationOnScreen).not()) return
|
||||
|
||||
// 显示右键菜单
|
||||
showContextMenu(event)
|
||||
}
|
||||
|
||||
private fun showContextMenu(event: MouseEvent) {
|
||||
val popupMenu = FlatPopupMenu()
|
||||
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
|
||||
val owner = windowScope.window
|
||||
val dialog = CustomizeToolBarDialog(owner, windowScope, model)
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
if (dialog.open()) {
|
||||
model.setActions(dialog.getActions())
|
||||
}
|
||||
}
|
||||
popupMenu.show(event.component, event.x, event.y)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
toolkit.removeAWTEventListener(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.plugin.internal.AltKeyModifier
|
||||
import app.termora.terminal.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
@@ -46,6 +47,9 @@ abstract class PtyHostTerminalTab(
|
||||
// 开启 reader
|
||||
startPtyConnectorReader()
|
||||
|
||||
// 修饰
|
||||
terminalKeyModifiers()
|
||||
|
||||
// 启动命令
|
||||
if (host.options.startupCommand.isNotBlank()) {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
@@ -155,6 +159,15 @@ abstract class PtyHostTerminalTab(
|
||||
ptyConnector.write(bytes)
|
||||
}
|
||||
|
||||
open fun terminalKeyModifiers() {
|
||||
val altModifier = host.options.extras["altModifier"]
|
||||
if (altModifier == AltKeyModifier.CharactersPrecededByESC.name) {
|
||||
terminalModel.setData(DataKey.AltModifier, AltKeyModifier.CharactersPrecededByESC)
|
||||
} else {
|
||||
terminalModel.setData(DataKey.AltModifier, AltKeyModifier.EightBit)
|
||||
}
|
||||
}
|
||||
|
||||
override fun canReconnect(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(own
|
||||
preferredSize = size
|
||||
minimumSize = size
|
||||
|
||||
rememberCheckBox.isVisible = host.isTemporary.not()
|
||||
|
||||
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
@@ -84,7 +86,7 @@ class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(own
|
||||
|
||||
switchPasswordComponent()
|
||||
|
||||
return FormBuilder.create().padding("$formMargin, $formMargin, $formMargin, $formMargin")
|
||||
return FormBuilder.create().padding("1dlu, $formMargin, $formMargin, $formMargin")
|
||||
.layout(layout)
|
||||
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, 1)
|
||||
.add(authenticationTypeComboBox).xy(3, 1)
|
||||
|
||||
@@ -9,7 +9,7 @@ import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.UIManager
|
||||
|
||||
class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
||||
internal class SettingsDialog(owner: Window) : DialogWrapper(owner) {
|
||||
private val optionsPane = SettingsOptionsPane()
|
||||
private val properties get() = DatabaseManager.getInstance().properties
|
||||
|
||||
|
||||
@@ -839,7 +839,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private fun p(): JPanel {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow",
|
||||
"pref, 20dlu, pref, 4dlu, pref, 4dlu, pref, 4dlu, pref"
|
||||
"pref, 20dlu, pref, 4dlu, pref, 4dlu, pref, 4dlu, pref, 4dlu, pref"
|
||||
)
|
||||
|
||||
|
||||
@@ -848,7 +848,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
|
||||
val branch = if (Application.isUnknownVersion()) "main" else Application.getVersion()
|
||||
|
||||
return FormBuilder.create().padding("$FORM_MARGIN, $FORM_MARGIN, $FORM_MARGIN, $FORM_MARGIN")
|
||||
val builder = FormBuilder.create().padding("$FORM_MARGIN, $FORM_MARGIN, $FORM_MARGIN, $FORM_MARGIN")
|
||||
.layout(layout).debug(false)
|
||||
.add(I18n.getString("termora.settings.about.termora", Application.getVersion()))
|
||||
.xyw(1, rows, 3, "center, fill").apply { rows += step }
|
||||
@@ -870,8 +870,14 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
"Open-source software"
|
||||
)
|
||||
).xy(3, rows).apply { rows += step }
|
||||
.build()
|
||||
|
||||
if (I18n.isChinaMainland()) {
|
||||
builder.add("交流群:").xy(1, rows)
|
||||
.add(createHyperlink("https://www.termora.cn/muted/discussion-group", "Discussion Group"))
|
||||
.xy(3, rows).apply { rows += step }
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.actions.TerminalFocusModeAction
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.terminal.*
|
||||
import app.termora.terminal.panel.TerminalPanel
|
||||
import app.termora.tlog.TerminalLoggerDataListener
|
||||
import java.awt.Color
|
||||
import javax.swing.UIManager
|
||||
import kotlin.reflect.cast
|
||||
|
||||
class TerminalFactory private constructor() : Disposable {
|
||||
private val terminals = mutableListOf<Terminal>()
|
||||
@@ -75,6 +77,8 @@ class TerminalFactory private constructor() : Disposable {
|
||||
override fun <T : Any> getData(key: DataKey<T>, defaultValue: T): T {
|
||||
if (key == TerminalPanel.SelectCopy) {
|
||||
return config.selectCopy as T
|
||||
} else if (key == TerminalPanel.FocusMode) {
|
||||
return key.clazz.cast(TerminalFocusModeAction.getInstance().isSelected)
|
||||
}
|
||||
return super.getData(key, defaultValue)
|
||||
}
|
||||
|
||||
@@ -133,8 +133,8 @@ class TerminalPanelFactory : Disposable {
|
||||
return
|
||||
}
|
||||
|
||||
val multipleAction = MultipleAction.getInstance(windowScope)
|
||||
if (!multipleAction.isSelected) {
|
||||
val multipleAction = MultipleAction.getInstance()
|
||||
if (multipleAction.isSelected(windowScope).not()) {
|
||||
ptyConnector.write(request.buffer)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.*
|
||||
import java.awt.event.AWTEventListener
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
@@ -28,13 +27,10 @@ import kotlin.math.min
|
||||
|
||||
class TerminalTabbed(
|
||||
private val windowScope: WindowScope,
|
||||
private val termoraToolBar: TermoraToolBar,
|
||||
private val tabbedPane: FlatTabbedPane,
|
||||
private val layout: TermoraLayout,
|
||||
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider {
|
||||
private val tabs = mutableListOf<TerminalTab>()
|
||||
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
|
||||
private val toolbar = termoraToolBar.getJToolBar()
|
||||
private val actionManager = ActionManager.getInstance()
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private val appearance get() = DatabaseManager.getInstance().appearance
|
||||
@@ -60,8 +56,6 @@ class TerminalTabbed(
|
||||
tabbedPane.isTabsClosable = true
|
||||
tabbedPane.tabType = FlatTabbedPane.TabType.card
|
||||
|
||||
tabbedPane.trailingComponent = toolbar
|
||||
|
||||
add(tabbedPane, BorderLayout.CENTER)
|
||||
|
||||
windowScope.getOrCreate(TerminalTabbedManager::class) { this }
|
||||
@@ -72,7 +66,6 @@ class TerminalTabbed(
|
||||
|
||||
|
||||
private fun initEvents() {
|
||||
Disposer.register(this, customizeToolBarAWTEventListener)
|
||||
|
||||
// 关闭 tab
|
||||
tabbedPane.setTabCloseCallback { _, i -> removeTabAt(i, true) }
|
||||
@@ -146,9 +139,6 @@ class TerminalTabbed(
|
||||
}
|
||||
}).let { Disposer.register(this, it) }
|
||||
|
||||
// 监听全局事件
|
||||
toolkit.addAWTEventListener(customizeToolBarAWTEventListener, AWTEvent.MOUSE_EVENT_MASK)
|
||||
|
||||
}
|
||||
|
||||
private fun removeTabAt(index: Int, disposable: Boolean = true) {
|
||||
@@ -301,9 +291,7 @@ class TerminalTabbed(
|
||||
|
||||
// 关闭
|
||||
val close = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close"))
|
||||
close.addActionListener {
|
||||
tabbedPane.tabCloseCallback?.accept(tabbedPane, tabIndex)
|
||||
}
|
||||
close.addActionListener { tabbedPane.tabCloseCallback?.accept(tabbedPane, tabIndex) }
|
||||
|
||||
// 关闭其他标签页
|
||||
popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close-other-tabs")).addActionListener {
|
||||
@@ -326,7 +314,7 @@ class TerminalTabbed(
|
||||
close.isEnabled = tab.canClose()
|
||||
rename.isEnabled = close.isEnabled
|
||||
clone.isEnabled = close.isEnabled
|
||||
edit.isEnabled = tab is HostTerminalTab && tab.host.id != "local"
|
||||
edit.isEnabled = tab is HostTerminalTab && tab.host.id != "local" && tab.host.isTemporary.not()
|
||||
openInNewWindow.isEnabled = close.isEnabled
|
||||
|
||||
// 如果不允许克隆
|
||||
@@ -337,12 +325,7 @@ class TerminalTabbed(
|
||||
if (close.isEnabled) {
|
||||
popupMenu.addSeparator()
|
||||
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
||||
reconnect.addActionListener {
|
||||
if (tabIndex > 0) {
|
||||
tabs[tabIndex].reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
reconnect.addActionListener { tabs[tabIndex].reconnect() }
|
||||
reconnect.isEnabled = tabs[tabIndex].canReconnect()
|
||||
}
|
||||
|
||||
@@ -384,60 +367,6 @@ class TerminalTabbed(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 对着 ToolBar 右键
|
||||
*/
|
||||
private inner class CustomizeToolBarAWTEventListener : AWTEventListener, Disposable {
|
||||
override fun eventDispatched(event: AWTEvent) {
|
||||
if (event !is MouseEvent || event.id != MouseEvent.MOUSE_CLICKED || !SwingUtilities.isRightMouseButton(event)) return
|
||||
// 如果 ToolBar 没有显示
|
||||
if (!toolbar.isShowing) return
|
||||
// 如果不是作用于在 ToolBar 上面
|
||||
if (!Rectangle(toolbar.locationOnScreen, toolbar.size).contains(event.locationOnScreen)) return
|
||||
|
||||
// 显示右键菜单
|
||||
showContextMenu(event)
|
||||
}
|
||||
|
||||
private fun showContextMenu(event: MouseEvent) {
|
||||
val popupMenu = FlatPopupMenu()
|
||||
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
|
||||
val owner = SwingUtilities.getWindowAncestor(this@TerminalTabbed)
|
||||
val dialog = CustomizeToolBarDialog(owner, windowScope, termoraToolBar)
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
if (dialog.open()) {
|
||||
TermoraToolBar.rebuild()
|
||||
}
|
||||
}
|
||||
popupMenu.show(event.component, event.x, event.y)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
toolkit.removeAWTEventListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
/*private inner class CustomizeToolBarDialog(owner: Window) : DialogWrapper(owner) {
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||
isModal = true
|
||||
title = I18n.getString("termora.setting")
|
||||
setLocationRelativeTo(null)
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val model = DefaultListModel<String>()
|
||||
val checkBoxList = CheckBoxList(model)
|
||||
checkBoxList.fixedCellHeight = UIManager.getInt("Tree.rowHeight")
|
||||
model.addElement("Test")
|
||||
return checkBoxList
|
||||
}
|
||||
|
||||
}*/
|
||||
|
||||
private inner class SwitchFindEverywhereResult(
|
||||
private val title: String,
|
||||
private val icon: Icon?,
|
||||
|
||||
@@ -107,7 +107,7 @@ class TermoraFencePanel(
|
||||
label.foreground = UIManager.getColor("textInactiveText")
|
||||
label.font = label.font.deriveFont(Font.BOLD)
|
||||
// 与最后一个按钮对冲,使其宽度和谐
|
||||
box.add(JButton(Icons.empty))
|
||||
box.add(Box.createHorizontalStrut(24))
|
||||
box.add(Box.createHorizontalGlue())
|
||||
if (SystemInfo.isMacOS.not()) {
|
||||
box.add(label)
|
||||
@@ -153,6 +153,7 @@ class TermoraFencePanel(
|
||||
|
||||
|
||||
override fun dispose() {
|
||||
if (leftTreePanel.isVisible)
|
||||
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package app.termora
|
||||
|
||||
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviderSupport
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.actions.OpenHostAction
|
||||
import app.termora.actions.*
|
||||
import app.termora.findeverywhere.FindEverywhereProvider
|
||||
import app.termora.findeverywhere.FindEverywhereProviderExtension
|
||||
import app.termora.findeverywhere.FindEverywhereResult
|
||||
import app.termora.keymap.KeyShortcut
|
||||
import app.termora.keymap.KeymapManager
|
||||
import app.termora.plugin.ExtensionManager
|
||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||
import app.termora.plugin.internal.ssh.SSHProtocolProvider
|
||||
@@ -21,16 +20,12 @@ import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jetbrains.JBR
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import java.awt.*
|
||||
import java.awt.event.*
|
||||
import java.util.*
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JFrame
|
||||
import javax.swing.*
|
||||
import javax.swing.SwingUtilities.isEventDispatchThread
|
||||
import javax.swing.UIManager
|
||||
|
||||
|
||||
fun assertEventDispatchThread() {
|
||||
@@ -45,22 +40,25 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
private val id = UUID.randomUUID().toString()
|
||||
private val windowScope = ApplicationScope.forWindowScope(this)
|
||||
private val tabbedPane = MyTabbedPane().apply { tabHeight = titleBarHeight }
|
||||
private val toolbar = TermoraToolBar(windowScope, this)
|
||||
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane, layout)
|
||||
private val toolbar = MyTermoraToolbar(windowScope, this)
|
||||
private val terminalTabbed = TerminalTabbed(windowScope, tabbedPane, layout)
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private var notifyListeners = emptyArray<NotifyListener>()
|
||||
private val moveMouseAdapter = createMoveMouseAdaptor()
|
||||
|
||||
private val keymapManager get() = KeymapManager.getInstance()
|
||||
private val actionManager get() = ActionManager.getInstance()
|
||||
private val dynamicExtensionHandler get() = DynamicExtensionHandler.getInstance()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
initKeymap()
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
if (SystemInfo.isLinux) {
|
||||
toolbar.getJToolBar().addMouseListener(moveMouseAdapter)
|
||||
toolbar.getJToolBar().addMouseMotionListener(moveMouseAdapter)
|
||||
toolbar.addMouseListener(moveMouseAdapter)
|
||||
toolbar.addMouseMotionListener(moveMouseAdapter)
|
||||
} else if (SystemInfo.isMacOS) {
|
||||
terminalTabbed.addMouseListener(moveMouseAdapter)
|
||||
terminalTabbed.addMouseMotionListener(moveMouseAdapter)
|
||||
@@ -68,12 +66,16 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
tabbedPane.addMouseListener(moveMouseAdapter)
|
||||
tabbedPane.addMouseMotionListener(moveMouseAdapter)
|
||||
|
||||
toolbar.getJToolBar().addMouseListener(moveMouseAdapter)
|
||||
toolbar.getJToolBar().addMouseMotionListener(moveMouseAdapter)
|
||||
toolbar.addMouseListener(moveMouseAdapter)
|
||||
toolbar.addMouseMotionListener(moveMouseAdapter)
|
||||
}
|
||||
|
||||
// 快捷键变动时重新监听
|
||||
KeymapRefresher.getInstance().addRefreshListener { initKeymap() }
|
||||
.let { Disposer.register(windowScope, it) }
|
||||
|
||||
// FindEverywhere
|
||||
DynamicExtensionHandler.getInstance()
|
||||
dynamicExtensionHandler
|
||||
.register(FindEverywhereProviderExtension::class.java, object : FindEverywhereProviderExtension {
|
||||
private val hostTreeModel get() = NewHostTreeModel.getInstance()
|
||||
|
||||
@@ -115,8 +117,7 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
private val showMoreInfo get() = EnableManager.getInstance().isShowMoreInfo()
|
||||
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
ActionManager.getInstance()
|
||||
.getAction(OpenHostAction.OPEN_HOST)
|
||||
actionManager.getAction(OpenHostAction.OPEN_HOST)
|
||||
?.actionPerformed(OpenHostActionEvent(e.source, host, e))
|
||||
}
|
||||
|
||||
@@ -149,7 +150,6 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun initView() {
|
||||
|
||||
// macOS 要避开左边的控制栏
|
||||
@@ -162,6 +162,8 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
|
||||
}
|
||||
|
||||
tabbedPane.trailingComponent = toolbar
|
||||
|
||||
val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
@@ -209,6 +211,61 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
|
||||
}
|
||||
|
||||
private fun initKeymap() {
|
||||
assertEventDispatchThread()
|
||||
|
||||
val keymap = keymapManager.getActiveKeymap()
|
||||
val inputMap = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
|
||||
val actionMap = rootPane.actionMap
|
||||
|
||||
// 移除之前所有的快捷键
|
||||
inputMap.clear()
|
||||
actionMap.clear()
|
||||
|
||||
for ((shortcut, actionIds) in keymap.getShortcuts()) {
|
||||
if (shortcut !is KeyShortcut) continue
|
||||
if (actionIds.contains(SwitchTabAction.SWITCH_TAB)) continue
|
||||
registerKeyStroke(actionMap, inputMap, shortcut.keyStroke, actionIds)
|
||||
}
|
||||
|
||||
for (shortcut in keymap.getShortcut(SwitchTabAction.SWITCH_TAB)) {
|
||||
if (shortcut !is KeyShortcut) continue
|
||||
registerKeyStroke(actionMap, inputMap, shortcut.keyStroke, listOf(SwitchTabAction.SWITCH_TAB))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun registerKeyStroke(
|
||||
actionMap: ActionMap,
|
||||
inputMap: InputMap,
|
||||
keyStroke: KeyStroke,
|
||||
actionIds: List<String>
|
||||
) {
|
||||
val keyShortcutActionId = "KeyShortcutAction_${randomUUID()}"
|
||||
actionMap.put(keyShortcutActionId, redirectAction(actionIds))
|
||||
inputMap.put(keyStroke, keyShortcutActionId)
|
||||
}
|
||||
|
||||
private fun redirectAction(actionIds: List<String>): Action {
|
||||
return object : AbstractAction() {
|
||||
private val keyboardFocusManager get() = KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
var source = e.source
|
||||
if (source == rootPane) {
|
||||
val focusOwner = keyboardFocusManager.focusOwner
|
||||
if (focusOwner is JComponent) {
|
||||
source = focusOwner
|
||||
}
|
||||
}
|
||||
|
||||
for (actionId in actionIds) {
|
||||
val action = actionManager.getAction(actionId) ?: continue
|
||||
action.actionPerformed(RedirectAnActionEvent(source, e.actionCommand, EventQueue.getCurrentEvent()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||
return dataProviderSupport.getData(dataKey) ?: terminalTabbed.getData(dataKey)
|
||||
}
|
||||
@@ -356,6 +413,12 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
}
|
||||
|
||||
|
||||
private inner class RedirectAnActionEvent(
|
||||
source: Any,
|
||||
command: String,
|
||||
event: EventObject
|
||||
) : AnActionEvent(source, command, event)
|
||||
|
||||
private inner class GlassPane : JComponent() {
|
||||
|
||||
init {
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.actions.*
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.findeverywhere.FindEverywhereAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionContainerFactory
|
||||
import java.awt.Rectangle
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import javax.swing.Box
|
||||
import javax.swing.JToolBar
|
||||
|
||||
|
||||
@Serializable
|
||||
data class ToolBarAction(
|
||||
val id: String,
|
||||
val visible: Boolean,
|
||||
)
|
||||
|
||||
class TermoraToolBar(
|
||||
private val windowScope: WindowScope,
|
||||
private val frame: TermoraFrame,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun rebuild() {
|
||||
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
||||
val toolbars = SwingUtils.getDescendantsOfClass(MyToolBar::class.java, frame)
|
||||
for (toolbar in toolbars) {
|
||||
toolbar.rebuild()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val properties get() = DatabaseManager.getInstance().properties
|
||||
private val toolbar by lazy { MyToolBar().apply { rebuild() } }
|
||||
|
||||
|
||||
fun getJToolBar(): JToolBar {
|
||||
return toolbar
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取到所有的 Action
|
||||
*/
|
||||
fun getAllActions(): List<ToolBarAction> {
|
||||
return listOf(
|
||||
ToolBarAction(SnippetAction.SNIPPET, true),
|
||||
ToolBarAction(Actions.SFTP, true),
|
||||
ToolBarAction(Actions.TERMINAL_LOGGER, true),
|
||||
ToolBarAction(Actions.MACRO, true),
|
||||
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
|
||||
ToolBarAction(Actions.KEY_MANAGER, true),
|
||||
ToolBarAction(MultipleAction.MULTIPLE, true),
|
||||
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
|
||||
ToolBarAction(SettingsAction.SETTING, true),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取到所有 Action,会根据用户个性化排序/显示
|
||||
*/
|
||||
fun getActions(): List<ToolBarAction> {
|
||||
val text = properties.getString(
|
||||
"Termora.ToolBar.Actions",
|
||||
StringUtils.EMPTY
|
||||
)
|
||||
|
||||
val actions = getAllActions()
|
||||
|
||||
if (text.isBlank()) {
|
||||
return actions
|
||||
}
|
||||
|
||||
// 存储的 action
|
||||
val storageActions = (ohMyJson.runCatching {
|
||||
ohMyJson.decodeFromString<List<ToolBarAction>>(text)
|
||||
}.getOrNull() ?: return actions).toMutableList()
|
||||
|
||||
for (action in actions) {
|
||||
// 如果存储的 action 不包含这个,那么这个可能是新增的,新增的默认显示出来
|
||||
if (storageActions.none { it.id == action.id }) {
|
||||
storageActions.addFirst(ToolBarAction(action.id, true))
|
||||
}
|
||||
}
|
||||
|
||||
// 如果存储的 Action 在所有 Action 里没有,那么移除
|
||||
storageActions.removeIf { e -> actions.none { e.id == it.id } }
|
||||
|
||||
return storageActions
|
||||
}
|
||||
|
||||
private inner class MyToolBar : JToolBar() {
|
||||
init {
|
||||
// 监听窗口大小变动,然后修改边距避开控制按钮
|
||||
addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentResized(e: ComponentEvent) {
|
||||
adjust()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun adjust() {
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
val rectangle =
|
||||
frame.rootPane.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
||||
as? Rectangle ?: return
|
||||
val right = rectangle.width
|
||||
val toolbar = this@MyToolBar
|
||||
for (i in 0 until toolbar.componentCount) {
|
||||
val c = toolbar.getComponent(i)
|
||||
if (c.name == "spacing") {
|
||||
if (c.width == right) {
|
||||
return
|
||||
}
|
||||
toolbar.remove(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (right > 0) {
|
||||
val spacing = Box.createHorizontalStrut(right)
|
||||
spacing.name = "spacing"
|
||||
toolbar.add(spacing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun rebuild() {
|
||||
val toolbar: JToolBar = this
|
||||
val actionManager = ActionManager.getInstance()
|
||||
val actionContainerFactory = ActionContainerFactory(actionManager)
|
||||
|
||||
toolbar.removeAll()
|
||||
|
||||
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
|
||||
}
|
||||
|
||||
override fun isEnabled(): Boolean {
|
||||
return actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.isEnabled ?: false
|
||||
}
|
||||
}))
|
||||
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
|
||||
if (SystemInfo.isLinux || SystemInfo.isWindows) {
|
||||
toolbar.add(Box.createHorizontalStrut(16))
|
||||
}
|
||||
|
||||
|
||||
// update btn
|
||||
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
||||
updateBtn.isVisible = updateBtn.isEnabled
|
||||
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
|
||||
toolbar.add(updateBtn)
|
||||
|
||||
|
||||
// 获取显示的Action,如果不是 false 那么就是显示出来
|
||||
for (action in getActions()) {
|
||||
if (action.visible) {
|
||||
val ac = actionManager.getAction(action.id)
|
||||
if (ac == null) {
|
||||
if (action.id == MultipleAction.MULTIPLE) {
|
||||
toolbar.add(actionContainerFactory.createButton(MultipleAction.getInstance(windowScope)))
|
||||
}
|
||||
} else {
|
||||
toolbar.add(actionContainerFactory.createButton(ac))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (toolbar is MyToolBar) {
|
||||
toolbar.adjust()
|
||||
}
|
||||
|
||||
toolbar.revalidate()
|
||||
toolbar.repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
103
src/main/kotlin/app/termora/TermoraToolbarModel.kt
Normal file
103
src/main/kotlin/app/termora/TermoraToolbarModel.kt
Normal file
@@ -0,0 +1,103 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.actions.ActionManager
|
||||
import app.termora.actions.MultipleAction
|
||||
import app.termora.actions.SettingsAction
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.findeverywhere.FindEverywhereAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.util.*
|
||||
import javax.swing.event.EventListenerList
|
||||
|
||||
internal class TermoraToolbarModel private constructor() {
|
||||
companion object {
|
||||
fun getInstance(): TermoraToolbarModel {
|
||||
return ApplicationScope.forApplicationScope()
|
||||
.getOrCreate(TermoraToolbarModel::class) { TermoraToolbarModel() }
|
||||
}
|
||||
}
|
||||
|
||||
private val properties get() = DatabaseManager.getInstance().properties
|
||||
private val eventListener = EventListenerList()
|
||||
|
||||
|
||||
fun getActionManager() = ActionManager.getInstance()
|
||||
|
||||
/**
|
||||
* 获取到所有的 Action
|
||||
*/
|
||||
fun getAllActions(): List<ToolBarAction> {
|
||||
return listOf(
|
||||
ToolBarAction(SnippetAction.SNIPPET, true),
|
||||
ToolBarAction(Actions.SFTP, true),
|
||||
ToolBarAction(Actions.TERMINAL_LOGGER, true),
|
||||
ToolBarAction(Actions.MACRO, true),
|
||||
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
|
||||
ToolBarAction(Actions.KEY_MANAGER, true),
|
||||
ToolBarAction(MultipleAction.MULTIPLE, true),
|
||||
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
|
||||
ToolBarAction(SettingsAction.SETTING, true),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取到所有 Action,会根据用户个性化排序/显示
|
||||
*/
|
||||
fun getActions(): List<ToolBarAction> {
|
||||
val text = properties.getString(
|
||||
"Termora.ToolBar.Actions",
|
||||
StringUtils.EMPTY
|
||||
)
|
||||
|
||||
val actions = getAllActions()
|
||||
|
||||
if (text.isBlank()) {
|
||||
return actions
|
||||
}
|
||||
|
||||
// 存储的 action
|
||||
val storageActions = (ohMyJson.runCatching {
|
||||
ohMyJson.decodeFromString<List<ToolBarAction>>(text)
|
||||
}.getOrNull() ?: return actions).toMutableList()
|
||||
|
||||
for (action in actions) {
|
||||
// 如果存储的 action 不包含这个,那么这个可能是新增的,新增的默认显示出来
|
||||
if (storageActions.none { it.id == action.id }) {
|
||||
storageActions.addFirst(ToolBarAction(action.id, true))
|
||||
}
|
||||
}
|
||||
|
||||
// 如果存储的 Action 在所有 Action 里没有,那么移除
|
||||
storageActions.removeIf { e -> actions.none { e.id == it.id } }
|
||||
|
||||
return storageActions
|
||||
}
|
||||
|
||||
fun setActions(actions: List<ToolBarAction>) {
|
||||
assertEventDispatchThread()
|
||||
properties.putString("Termora.ToolBar.Actions", ohMyJson.encodeToString(actions))
|
||||
for (listener in eventListener.getListeners(TermoraToolbarModelListener::class.java)) {
|
||||
listener.onChanged()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun addTermoraToolbarModelListener(listener: TermoraToolbarModelListener): Disposable {
|
||||
eventListener.add(TermoraToolbarModelListener::class.java, listener)
|
||||
return object : Disposable {
|
||||
override fun dispose() {
|
||||
removeTermoraToolbarModelListener(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeTermoraToolbarModelListener(listener: TermoraToolbarModelListener) {
|
||||
eventListener.remove(TermoraToolbarModelListener::class.java, listener)
|
||||
}
|
||||
|
||||
interface TermoraToolbarModelListener : EventListener {
|
||||
fun onChanged()
|
||||
}
|
||||
}
|
||||
9
src/main/kotlin/app/termora/ToolBarAction.kt
Normal file
9
src/main/kotlin/app/termora/ToolBarAction.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package app.termora
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ToolBarAction(
|
||||
val id: String,
|
||||
val visible: Boolean,
|
||||
)
|
||||
@@ -86,8 +86,8 @@ class UpdaterManager private constructor() {
|
||||
return LatestVersion.self
|
||||
}
|
||||
|
||||
val text = response.use { resp -> resp.body?.use { it.string() } }
|
||||
if (text.isNullOrBlank()) {
|
||||
val text = response.use { resp -> resp.body.use { it.string() } }
|
||||
if (text.isBlank()) {
|
||||
return LatestVersion.self
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package app.termora.account
|
||||
import app.termora.AES
|
||||
import app.termora.database.*
|
||||
import app.termora.database.Data.Companion.toData
|
||||
import okhttp3.internal.EMPTY_BYTE_ARRAY
|
||||
import org.apache.commons.codec.binary.Base64
|
||||
import org.apache.commons.codec.digest.DigestUtils
|
||||
import org.apache.commons.lang3.ObjectUtils
|
||||
@@ -88,7 +87,7 @@ abstract class SyncService {
|
||||
return accountManager.getSecretKey()
|
||||
}
|
||||
val team = accountManager.getTeams().firstOrNull { it.id == ownerId }
|
||||
return team?.secretKey ?: EMPTY_BYTE_ARRAY
|
||||
return team?.secretKey ?: byteArrayOf()
|
||||
}
|
||||
|
||||
protected fun decryptData(id: String, data: String, ownerId: String): String {
|
||||
|
||||
@@ -28,19 +28,20 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
||||
private fun registerActions() {
|
||||
addAction(NewWindowAction.NEW_WINDOW, NewWindowAction())
|
||||
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
|
||||
addAction(QuickConnectAction.QUICK_CONNECT, QuickConnectAction.instance)
|
||||
|
||||
addAction(Actions.APP_UPDATE, AppUpdateAction.getInstance())
|
||||
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
|
||||
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
|
||||
addAction(Actions.SFTP, TransferAnAction())
|
||||
addAction(SFTPCommandAction.SFTP_COMMAND, SFTPCommandAction())
|
||||
addAction(MultipleAction.MULTIPLE, MultipleAction.getInstance())
|
||||
addAction(SnippetAction.SNIPPET, SnippetAction.getInstance())
|
||||
addAction(Actions.MACRO, MacroAction())
|
||||
addAction(Actions.KEY_MANAGER, KeyManagerAction())
|
||||
|
||||
addAction(SwitchTabAction.SWITCH_TAB, SwitchTabAction())
|
||||
addAction(TabReconnectAction.RECONNECT_TAB, TabReconnectAction())
|
||||
addAction(SettingsAction.SETTING, SettingsAction())
|
||||
addAction(SettingsAction.SETTING, SettingsAction.getInstance())
|
||||
|
||||
addAction(NewHostAction.NEW_HOST, NewHostAction())
|
||||
addAction(OpenHostAction.OPEN_HOST, OpenHostAction())
|
||||
@@ -52,6 +53,7 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
||||
addAction(TerminalClearScreenAction.CLEAR_SCREEN, TerminalClearScreenAction())
|
||||
addAction(OpenLocalTerminalAction.LOCAL_TERMINAL, OpenLocalTerminalAction())
|
||||
addAction(TerminalSelectAllAction.SELECT_ALL, TerminalSelectAllAction())
|
||||
addAction(TerminalFocusModeAction.FocusMode, TerminalFocusModeAction.getInstance())
|
||||
|
||||
addAction(TerminalZoomInAction.ZOOM_IN, TerminalZoomInAction())
|
||||
addAction(TerminalZoomOutAction.ZOOM_OUT, TerminalZoomOutAction())
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.Application.httpClient
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.sun.jna.platform.win32.Advapi32
|
||||
import com.sun.jna.platform.win32.WinError
|
||||
import com.sun.jna.platform.win32.WinNT
|
||||
import com.sun.jna.platform.win32.WinReg
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import okhttp3.Request
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.JXEditorPane
|
||||
import org.semver4j.Semver
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Dimension
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.io.File
|
||||
import java.net.ProxySelector
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.JScrollPane
|
||||
import javax.swing.UIManager
|
||||
import javax.swing.event.HyperlinkEvent
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class AppUpdateAction private constructor() : AnAction(
|
||||
StringUtils.EMPTY,
|
||||
Icons.ideUpdate
|
||||
) {
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Swing)
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(AppUpdateAction::class.java)
|
||||
private const val PKG_FILE_KEY = "pkgFile"
|
||||
|
||||
fun getInstance(): AppUpdateAction {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(AppUpdateAction::class) { AppUpdateAction() }
|
||||
}
|
||||
}
|
||||
|
||||
private val updaterManager get() = UpdaterManager.getInstance()
|
||||
private var isRemindMeNextTime = false
|
||||
|
||||
init {
|
||||
isEnabled = false
|
||||
scheduleUpdate()
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
showUpdateDialog()
|
||||
}
|
||||
|
||||
|
||||
private fun scheduleUpdate() {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
// 启动 3 分钟后才是检查
|
||||
if (Application.isUnknownVersion().not()) {
|
||||
delay(3.minutes)
|
||||
}
|
||||
|
||||
while (coroutineScope.isActive) {
|
||||
// 下次提醒我
|
||||
if (isRemindMeNextTime) break
|
||||
|
||||
try {
|
||||
checkUpdate()
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
// 之后每 3 小时检查一次
|
||||
delay(3.hours.inWholeMilliseconds)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkUpdate() {
|
||||
|
||||
val latestVersion = updaterManager.fetchLatestVersion()
|
||||
if (latestVersion.isSelf) {
|
||||
return
|
||||
}
|
||||
|
||||
// 之所以放到后面检查是不是开发版本,是需要发起一次检测请求,以方便调试
|
||||
if (Application.isUnknownVersion()) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
val newVersion = Semver.parse(latestVersion.version) ?: return
|
||||
val version = Semver.parse(Application.getVersion()) ?: return
|
||||
if (newVersion <= version) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
downloadLatestPkg(latestVersion)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
withContext(Dispatchers.Swing) { isEnabled = true }
|
||||
|
||||
}
|
||||
|
||||
|
||||
private suspend fun downloadLatestPkg(latestVersion: UpdaterManager.LatestVersion) {
|
||||
if (SystemInfo.isLinux) return
|
||||
|
||||
super.putValue(PKG_FILE_KEY, null)
|
||||
val arch = if (SystemInfo.isAARCH64) "aarch64" else "x86-64"
|
||||
val osName = if (SystemInfo.isWindows) "windows" else "osx"
|
||||
val suffix = if (SystemInfo.isWindows) "exe" else "dmg"
|
||||
val filename = "termora-${latestVersion.version}-${osName}-${arch}.${suffix}"
|
||||
val asset = latestVersion.assets.find { it.name == filename } ?: return
|
||||
|
||||
val response = httpClient
|
||||
.newBuilder()
|
||||
.callTimeout(15, TimeUnit.MINUTES)
|
||||
.readTimeout(15, TimeUnit.MINUTES)
|
||||
.proxySelector(ProxySelector.getDefault())
|
||||
.build()
|
||||
.newCall(Request.Builder().url(asset.downloadUrl).build())
|
||||
.execute()
|
||||
if (response.isSuccessful.not()) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.warn("Failed to download latest version ${latestVersion.version}, response code ${response.code}")
|
||||
}
|
||||
IOUtils.closeQuietly(response)
|
||||
return
|
||||
}
|
||||
|
||||
val body = response.body
|
||||
val input = body?.byteStream()
|
||||
val file = FileUtils.getFile(Application.getTemporaryDir(), "${UUID.randomUUID()}-${filename}")
|
||||
val output = file.outputStream()
|
||||
|
||||
val downloaded = runCatching { IOUtils.copy(input, output) }.isSuccess
|
||||
IOUtils.closeQuietly(input, output, body, response)
|
||||
|
||||
if (!downloaded) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error("Failed to download latest version to $filename")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("Successfully downloaded latest version to $file")
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Swing) { setLatestPkgFile(file) }
|
||||
|
||||
}
|
||||
|
||||
private fun setLatestPkgFile(file: File) {
|
||||
putValue(PKG_FILE_KEY, file)
|
||||
}
|
||||
|
||||
private fun getLatestPkgFile(): File? {
|
||||
return getValue(PKG_FILE_KEY) as? File
|
||||
}
|
||||
|
||||
private fun showUpdateDialog() {
|
||||
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
|
||||
val lastVersion = updaterManager.lastVersion
|
||||
val editorPane = JXEditorPane()
|
||||
editorPane.contentType = "text/html"
|
||||
editorPane.text = lastVersion.htmlBody
|
||||
editorPane.isEditable = false
|
||||
editorPane.addHyperlinkListener {
|
||||
if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) {
|
||||
Application.browse(it.url.toURI())
|
||||
}
|
||||
}
|
||||
editorPane.background = DynamicColor("window")
|
||||
val scrollPane = JScrollPane(editorPane)
|
||||
scrollPane.border = BorderFactory.createEmptyBorder()
|
||||
scrollPane.preferredSize = Dimension(
|
||||
UIManager.getInt("Dialog.width") - 100,
|
||||
UIManager.getInt("Dialog.height") - 100
|
||||
)
|
||||
|
||||
val option = OptionPane.showConfirmDialog(
|
||||
owner,
|
||||
scrollPane,
|
||||
title = I18n.getString("termora.update.title"),
|
||||
messageType = JOptionPane.PLAIN_MESSAGE,
|
||||
optionType = JOptionPane.YES_NO_CANCEL_OPTION,
|
||||
options = arrayOf(
|
||||
I18n.getString("termora.update.update"),
|
||||
I18n.getString("termora.update.ignore"),
|
||||
I18n.getString("termora.cancel")
|
||||
),
|
||||
initialValue = I18n.getString("termora.update.update")
|
||||
)
|
||||
if (option == JOptionPane.CANCEL_OPTION) {
|
||||
return
|
||||
} else if (option == JOptionPane.NO_OPTION) {
|
||||
isEnabled = false
|
||||
isRemindMeNextTime = true
|
||||
} else if (option == JOptionPane.YES_OPTION) {
|
||||
updateSelf(lastVersion)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelf(latestVersion: UpdaterManager.LatestVersion) {
|
||||
val file = getLatestPkgFile()
|
||||
if (SystemInfo.isLinux || file == null) {
|
||||
isEnabled = false
|
||||
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${latestVersion.version}"))
|
||||
return
|
||||
}
|
||||
|
||||
val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||
val commands = if (SystemInfo.isMacOS) listOf("open", "-n", file.absolutePath)
|
||||
// 如果安装过,那么直接静默安装和自动启动
|
||||
else if (isAppInstalled()) listOf(
|
||||
file.absolutePath,
|
||||
"/SILENT",
|
||||
"/AUTOSTART",
|
||||
"/NORESTART",
|
||||
"/FORCECLOSEAPPLICATIONS"
|
||||
)
|
||||
// 没有安装过 则打开安装向导
|
||||
else listOf(file.absolutePath)
|
||||
|
||||
if (log.isInfoEnabled) {
|
||||
log.info("restart {}", commands.joinToString(StringUtils.SPACE))
|
||||
}
|
||||
|
||||
TermoraRestarter.getInstance().scheduleRestart(owner, true, commands)
|
||||
|
||||
}
|
||||
|
||||
private fun isAppInstalled(): Boolean {
|
||||
val keyPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${Application.getName()}_is1"
|
||||
val phkKey = WinReg.HKEYByReference()
|
||||
|
||||
// 尝试打开注册表键
|
||||
val result = Advapi32.INSTANCE.RegOpenKeyEx(
|
||||
WinReg.HKEY_LOCAL_MACHINE,
|
||||
keyPath,
|
||||
0,
|
||||
WinNT.KEY_READ,
|
||||
phkKey
|
||||
)
|
||||
|
||||
if (result == WinError.ERROR_SUCCESS) {
|
||||
// 键存在,关闭句柄
|
||||
Advapi32.INSTANCE.RegCloseKey(phkKey.getValue())
|
||||
return true
|
||||
} else {
|
||||
// 键不存在或无权限
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.TerminalPanelFactory
|
||||
import app.termora.WindowScope
|
||||
import app.termora.*
|
||||
|
||||
class MultipleAction private constructor() : AnAction(
|
||||
I18n.getString("termora.tools.multiple"),
|
||||
Icons.vcs
|
||||
) {
|
||||
), StateAction {
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -17,8 +14,8 @@ class MultipleAction private constructor() : AnAction(
|
||||
*/
|
||||
const val MULTIPLE = "MultipleAction"
|
||||
|
||||
fun getInstance(windowScope: WindowScope): MultipleAction {
|
||||
return windowScope.getOrCreate(MultipleAction::class) { MultipleAction() }
|
||||
fun getInstance(): MultipleAction {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(MultipleAction::class) { MultipleAction() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +24,24 @@ class MultipleAction private constructor() : AnAction(
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
super.setSelected(false)
|
||||
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
|
||||
setSelected(windowScope, isSelected(windowScope).not())
|
||||
TerminalPanelFactory.getInstance().repaintAll()
|
||||
}
|
||||
|
||||
override fun isSelected(): Boolean {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun isSelected(windowScope: WindowScope): Boolean {
|
||||
return windowScope.getBoolean("MultipleAction.isSelected", false)
|
||||
}
|
||||
|
||||
override fun setSelected(windowScope: WindowScope, selected: Boolean) {
|
||||
windowScope.putBoolean("MultipleAction.isSelected", selected)
|
||||
putValue("MultipleAction.isSelected", selected)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -38,7 +38,7 @@ class OpenHostAction : AnAction() {
|
||||
if (providers.first { StringUtils.equalsIgnoreCase(it.getProtocol(), host.protocol) }
|
||||
.isTransfer()) {
|
||||
ActionManager.getInstance().getAction(Actions.SFTP)
|
||||
.actionPerformed(TransferActionEvent(evt.source, evt.host.id, evt.event))
|
||||
.actionPerformed(TransferActionEvent(evt.source, host, evt.event))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
178
src/main/kotlin/app/termora/actions/QuickConnectAction.kt
Normal file
178
src/main/kotlin/app/termora/actions/QuickConnectAction.kt
Normal file
@@ -0,0 +1,178 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.OptionsPane.Companion.FORM_MARGIN
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import javax.swing.*
|
||||
|
||||
class QuickConnectAction private constructor() : AnAction(I18n.getString("termora.actions.quick-connect"), Icons.find) {
|
||||
companion object {
|
||||
const val QUICK_CONNECT = "QuickConnectAction"
|
||||
val instance = QuickConnectAction()
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.quick-connect"))
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val scope = evt.getData(DataProviders.WindowScope) ?: return
|
||||
val dialog = QuickConnectDialog(scope.window)
|
||||
dialog.isVisible = true
|
||||
}
|
||||
|
||||
private class QuickConnectDialog(owner: Window) : DialogWrapper(owner) {
|
||||
private val properties get() = DatabaseManager.getInstance().properties
|
||||
private val hostComboBox = OutlineComboBox<String>()
|
||||
private val usernameTextField = OutlineTextField(256)
|
||||
private val passwordTextField = OutlinePasswordField(256)
|
||||
|
||||
init {
|
||||
isModal = true
|
||||
title = I18n.getString("termora.actions.quick-connect")
|
||||
isResizable = false
|
||||
init()
|
||||
pack()
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 250, preferredSize.height)
|
||||
setLocationRelativeTo(owner)
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
hostComboBox.isEditable = true
|
||||
hostComboBox.placeholderText = "ssh://127.0.0.1:22"
|
||||
|
||||
val histories = getHistories()
|
||||
for (history in histories) {
|
||||
if (histories.first() == history) {
|
||||
usernameTextField.text = history.host.username
|
||||
passwordTextField.text = history.host.authentication.password
|
||||
}
|
||||
hostComboBox.addItem(history.url)
|
||||
}
|
||||
|
||||
usernameTextField.placeholderText = I18n.getString("termora.new-host.general.username")
|
||||
passwordTextField.placeholderText = I18n.getString("termora.new-host.general.password")
|
||||
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow",
|
||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
|
||||
return FormBuilder.create().layout(layout)
|
||||
.border(BorderFactory.createEmptyBorder(0, 8, 8, 8))
|
||||
.add("${I18n.getString("termora.new-host.general.protocol")}:").xy(1, 1)
|
||||
.add(hostComboBox).xy(3, 1)
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, 3)
|
||||
.add(usernameTextField).xy(3, 3)
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, 5)
|
||||
.add(passwordTextField).xy(3, 5)
|
||||
.build()
|
||||
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
val host = hostComboBox.selectedItem as? String
|
||||
if (host.isNullOrBlank()) {
|
||||
hostComboBox.requestFocusInWindow()
|
||||
return
|
||||
}
|
||||
|
||||
val historyHost: HistoryHost
|
||||
try {
|
||||
historyHost = getHistoryHost(host.trim())
|
||||
} catch (e: Exception) {
|
||||
hostComboBox.requestFocusInWindow()
|
||||
OptionPane.showMessageDialog(
|
||||
this,
|
||||
e.message ?: ExceptionUtils.getRootCauseMessage(e),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val action = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
||||
if (action is OpenHostAction) {
|
||||
SwingUtilities.invokeLater {
|
||||
action.actionPerformed(OpenHostActionEvent(this, historyHost.host, EventObject(this)))
|
||||
}
|
||||
}
|
||||
super.doOKAction()
|
||||
|
||||
|
||||
}
|
||||
|
||||
override fun createOkAction(): AbstractAction {
|
||||
return OkAction(I18n.getString("termora.welcome.contextmenu.connect"))
|
||||
}
|
||||
|
||||
private fun getHistoryHost(host: String): HistoryHost {
|
||||
|
||||
|
||||
val uri = URI.create(host)
|
||||
val protocolProvider = ProtocolProvider.valueOf(uri.scheme)
|
||||
if (protocolProvider == null) {
|
||||
throw UnsupportedOperationException(I18n.getString("termora.protocol.not-supported", uri.scheme))
|
||||
}
|
||||
|
||||
val historyHost = HistoryHost(
|
||||
host, Host(
|
||||
name = uri.host,
|
||||
protocol = uri.scheme,
|
||||
host = uri.host,
|
||||
port = uri.port,
|
||||
username = usernameTextField.text.trim(),
|
||||
authentication = Authentication.No.copy(
|
||||
type = AuthenticationType.Password,
|
||||
password = String(passwordTextField.password)
|
||||
),
|
||||
options = Options.Default.copy(
|
||||
extras = mutableMapOf("Temporary" to "true")
|
||||
)
|
||||
)
|
||||
)
|
||||
val histories = getHistories().toMutableList()
|
||||
histories.removeIf { it.url == host }
|
||||
histories.addFirst(historyHost)
|
||||
|
||||
if (histories.size > 20) {
|
||||
histories.removeLast()
|
||||
}
|
||||
|
||||
properties.putString("QuickConnect.historyHosts", ohMyJson.encodeToString(histories))
|
||||
|
||||
return historyHost
|
||||
}
|
||||
|
||||
private fun getHistories(): List<HistoryHost> {
|
||||
val text = properties.getString("QuickConnect.historyHosts", "[]")
|
||||
return ohMyJson.runCatching { ohMyJson.decodeFromString<List<HistoryHost>>(text) }
|
||||
.getOrNull() ?: emptyList()
|
||||
}
|
||||
|
||||
override fun addNotify() {
|
||||
super.addNotify()
|
||||
controlsVisible = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class HistoryHost(
|
||||
val url: String,
|
||||
val host: Host,
|
||||
)
|
||||
|
||||
}
|
||||
@@ -11,19 +11,25 @@ import java.awt.event.ActionEvent
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
class SettingsAction : AnAction(
|
||||
class SettingsAction private constructor() : AnAction(
|
||||
I18n.getString("termora.setting"),
|
||||
Icons.settings
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* 打开设置
|
||||
*/
|
||||
const val SETTING = "SettingAction"
|
||||
|
||||
fun getInstance(): SettingsAction {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(SettingsAction::class) { SettingsAction() }
|
||||
}
|
||||
}
|
||||
|
||||
private var isShowing = false
|
||||
private val action get() = this
|
||||
|
||||
init {
|
||||
FlatDesktop.setPreferencesHandler {
|
||||
@@ -36,20 +42,25 @@ class SettingsAction : AnAction(
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
if (isShowing) {
|
||||
return
|
||||
if (isShowing) return
|
||||
showSettingsDialog(evt)
|
||||
}
|
||||
|
||||
|
||||
private fun showSettingsDialog(evt: AnActionEvent) {
|
||||
|
||||
isShowing = true
|
||||
|
||||
val owner = evt.window
|
||||
val dialog = SettingsDialog(owner)
|
||||
dialog.addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
this@SettingsAction.isShowing = false
|
||||
action.isShowing = false
|
||||
}
|
||||
})
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
dialog.isVisible = true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
8
src/main/kotlin/app/termora/actions/StateAction.kt
Normal file
8
src/main/kotlin/app/termora/actions/StateAction.kt
Normal file
@@ -0,0 +1,8 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.WindowScope
|
||||
|
||||
interface StateAction {
|
||||
fun isSelected(windowScope: WindowScope): Boolean
|
||||
fun setSelected(windowScope: WindowScope, selected: Boolean)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.EnableManager
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
class TerminalFocusModeAction private constructor() : AnAction(
|
||||
I18n.getString("termora.actions.focus-mode"),
|
||||
Icons.eye
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val FocusMode = "TerminalFocusMode"
|
||||
private val log = LoggerFactory.getLogger(TerminalFocusModeAction::class.java)
|
||||
fun getInstance(): TerminalFocusModeAction {
|
||||
return ApplicationScope.forApplicationScope()
|
||||
.getOrCreate(TerminalFocusModeAction::class) { TerminalFocusModeAction() }
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.focus-mode"))
|
||||
putValue(ACTION_COMMAND_KEY, FocusMode)
|
||||
setStateAction()
|
||||
isSelected = enableManager.getFlag("Terminal.FocusMode", false)
|
||||
}
|
||||
|
||||
private val enableManager get() = EnableManager.getInstance()
|
||||
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
enableManager.setFlag("Terminal.FocusMode", isSelected)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -504,6 +504,8 @@ class DatabaseManager private constructor() : Disposable {
|
||||
|
||||
protected open fun putString(key: String, value: String) {
|
||||
databaseManager.setSetting("${name}.$key", value)
|
||||
// 触发变动
|
||||
DatabasePropertiesChangedExtension.onPropertyChanged(name, key, value)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package app.termora.database
|
||||
|
||||
import app.termora.database.DatabaseManager.Companion.log
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionManager
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
internal interface DatabasePropertiesChangedExtension : Extension {
|
||||
|
||||
companion object {
|
||||
fun onPropertyChanged(name: String, key: String, value: String) {
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
for (extension in ExtensionManager.getInstance()
|
||||
.getExtensions(DatabasePropertiesChangedExtension::class.java)) {
|
||||
try {
|
||||
extension.onPropertyChanged(name, key, value)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SwingUtilities.invokeLater { onPropertyChanged(name, key, value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 属性数据变动
|
||||
*
|
||||
* @param name 属性名
|
||||
* @param key key
|
||||
*/
|
||||
fun onPropertyChanged(name: String, key: String, value: String)
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import app.termora.I18n
|
||||
import app.termora.Scope
|
||||
import app.termora.WindowScope
|
||||
import app.termora.actions.MultipleAction
|
||||
import app.termora.actions.TerminalFocusModeAction
|
||||
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
|
||||
@@ -13,6 +14,7 @@ class QuickActionsFindEverywhereProvider(private val windowScope: WindowScope) :
|
||||
Actions.KEY_MANAGER,
|
||||
Actions.KEYWORD_HIGHLIGHT,
|
||||
MultipleAction.MULTIPLE,
|
||||
TerminalFocusModeAction.FocusMode,
|
||||
)
|
||||
|
||||
override fun find(pattern: String, scope: Scope): List<FindEverywhereResult> {
|
||||
@@ -22,9 +24,7 @@ class QuickActionsFindEverywhereProvider(private val windowScope: WindowScope) :
|
||||
for (action in actions) {
|
||||
val ac = actionManager.getAction(action)
|
||||
if (ac == null) {
|
||||
if (action == MultipleAction.MULTIPLE) {
|
||||
results.add(ActionFindEverywhereResult(MultipleAction.getInstance(windowScope)))
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
results.add(ActionFindEverywhereResult(ac))
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import app.termora.Icons
|
||||
import app.termora.Scope
|
||||
import app.termora.actions.NewHostAction
|
||||
import app.termora.actions.OpenLocalTerminalAction
|
||||
import app.termora.actions.QuickConnectAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
@@ -19,19 +20,13 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
|
||||
actionManager.let { list.add(CreateHostFindEverywhereResult()) }
|
||||
|
||||
// Local terminal
|
||||
actionManager.getAction(OpenLocalTerminalAction.LOCAL_TERMINAL)?.let {
|
||||
list.add(ActionFindEverywhereResult(it))
|
||||
}
|
||||
|
||||
actionManager.getAction(OpenLocalTerminalAction.LOCAL_TERMINAL)?.let { list.add(ActionFindEverywhereResult(it)) }
|
||||
// Snippet
|
||||
actionManager.getAction(SnippetAction.SNIPPET)?.let {
|
||||
list.add(ActionFindEverywhereResult(it))
|
||||
}
|
||||
|
||||
actionManager.getAction(SnippetAction.SNIPPET)?.let { list.add(ActionFindEverywhereResult(it)) }
|
||||
// SFTP
|
||||
actionManager.getAction(Actions.SFTP)?.let {
|
||||
list.add(ActionFindEverywhereResult(it))
|
||||
}
|
||||
actionManager.getAction(Actions.SFTP)?.let { list.add(ActionFindEverywhereResult(it)) }
|
||||
// quick connect
|
||||
actionManager.getAction(QuickConnectAction.QUICK_CONNECT)?.let { list.add(ActionFindEverywhereResult(it)) }
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
package app.termora.highlight
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.terminal.TerminalColor
|
||||
import com.formdev.flatlaf.extras.components.FlatTable
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Color
|
||||
import java.awt.Component
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import javax.swing.*
|
||||
import javax.swing.border.EmptyBorder
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
@@ -29,7 +34,8 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
|
||||
private val addBtn = JButton(I18n.getString("termora.new-host.tunneling.add"))
|
||||
private val editBtn = JButton(I18n.getString("termora.keymgr.edit"))
|
||||
private val deleteBtn = JButton(I18n.getString("termora.remove"))
|
||||
|
||||
private val importBtn = JButton(I18n.getString("termora.keymgr.import"))
|
||||
private val exportBtn = JButton(I18n.getString("termora.keymgr.export"))
|
||||
|
||||
init {
|
||||
initView()
|
||||
@@ -213,6 +219,29 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
|
||||
deleteBtn.isEnabled = editBtn.isEnabled
|
||||
}
|
||||
|
||||
exportBtn.addActionListener {
|
||||
val fileChooser = FileChooser()
|
||||
fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||
fileChooser.win32Filters.add(Pair("All files", listOf("*")))
|
||||
fileChooser.showSaveDialog(owner, "highlights.json").thenAccept { file ->
|
||||
file?.outputStream()?.use {
|
||||
val highlights = keywordHighlightManager.getKeywordHighlights(accountOwner.id)
|
||||
.map { e -> e.copy(id = randomUUID()) }
|
||||
IOUtils.write(ohMyJson.encodeToString(highlights), it, StandardCharsets.UTF_8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
importBtn.addActionListener {
|
||||
val chooser = FileChooser()
|
||||
chooser.osxAllowedFileTypes = listOf("json")
|
||||
chooser.allowsMultiSelection = false
|
||||
chooser.win32Filters.add(Pair("JSON files", listOf("json")))
|
||||
chooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||
chooser.showOpenDialog(owner)
|
||||
.thenAccept { if (it.isNotEmpty()) SwingUtilities.invokeLater { importKeywordHighlights(it.first()) } }
|
||||
}
|
||||
|
||||
Disposer.register(this, object : Disposable {
|
||||
override fun dispose() {
|
||||
terminal.close()
|
||||
@@ -220,6 +249,23 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
|
||||
})
|
||||
}
|
||||
|
||||
private fun importKeywordHighlights(file: File) {
|
||||
try {
|
||||
val highlights = ohMyJson.decodeFromString<List<KeywordHighlight>>(file.readText())
|
||||
.map { it.copy(id = randomUUID()) }
|
||||
for (highlight in highlights) {
|
||||
keywordHighlightManager.addKeywordHighlight(highlight, accountOwner)
|
||||
model.fireTableRowsInserted(model.rowCount - 1, model.rowCount)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
OptionPane.showMessageDialog(
|
||||
owner,
|
||||
message = e.message ?: ExceptionUtils.getRootCauseMessage(e),
|
||||
messageType = JOptionPane.ERROR_MESSAGE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCenterPanel(): JComponent {
|
||||
|
||||
val panel = JPanel(BorderLayout())
|
||||
@@ -232,13 +278,15 @@ class KeywordHighlightPanel(private val accountOwner: AccountOwner) : JPanel(Bor
|
||||
val formMargin = "4dlu"
|
||||
val layout = FormLayout(
|
||||
"default:grow",
|
||||
"pref, $formMargin, pref, $formMargin, pref"
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
panel.add(
|
||||
FormBuilder.create().layout(layout).padding(EmptyBorder(0, 12, 0, 0))
|
||||
.add(addBtn).xy(1, rows).apply { rows += step }
|
||||
.add(editBtn).xy(1, rows).apply { rows += step }
|
||||
.add(deleteBtn).xy(1, rows).apply { rows += step }
|
||||
.add(importBtn).xy(1, rows).apply { rows += step }
|
||||
.add(exportBtn).xy(1, rows).apply { rows += step }
|
||||
.build(),
|
||||
BorderLayout.EAST)
|
||||
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
package app.termora.keymap
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.DialogWrapper
|
||||
import app.termora.Disposable
|
||||
import app.termora.SwingUtils
|
||||
import app.termora.account.AccountManager
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.database.Data
|
||||
import app.termora.database.DataType
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.database.OwnerType
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Container
|
||||
import java.awt.KeyEventDispatcher
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.KeyStroke
|
||||
|
||||
class KeymapManager private constructor() : Disposable {
|
||||
|
||||
@@ -34,17 +21,13 @@ class KeymapManager private constructor() : Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
private val keymapKeyEventDispatcher = KeymapKeyEventDispatcher()
|
||||
private val database get() = DatabaseManager.getInstance()
|
||||
private val properties get() = DatabaseManager.getInstance().properties
|
||||
private val keymaps = linkedMapOf<String, Keymap>()
|
||||
private val accountManager get() = AccountManager.getInstance()
|
||||
private val activeKeymap get() = properties.getString("Keymap.Active")
|
||||
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
|
||||
|
||||
init {
|
||||
keyboardFocusManager.addKeyEventDispatcher(keymapKeyEventDispatcher)
|
||||
|
||||
try {
|
||||
for (data in database.rawData(DataType.Keymap)) {
|
||||
try {
|
||||
@@ -63,13 +46,8 @@ class KeymapManager private constructor() : Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
MacOSKeymap.getInstance().let {
|
||||
keymaps[it.name] = it
|
||||
}
|
||||
|
||||
WindowsKeymap.getInstance().let {
|
||||
keymaps[it.name] = it
|
||||
}
|
||||
MacOSKeymap.getInstance().let { keymaps[it.name] = it }
|
||||
WindowsKeymap.getInstance().let { keymaps[it.name] = it }
|
||||
|
||||
}
|
||||
|
||||
@@ -102,7 +80,7 @@ class KeymapManager private constructor() : Disposable {
|
||||
keymaps.putFirst(keymap.name, keymap)
|
||||
val accountId = accountManager.getAccountId()
|
||||
|
||||
database.save(
|
||||
database.saveAndIncrementVersion(
|
||||
Data(
|
||||
id = keymap.id,
|
||||
ownerId = accountId,
|
||||
@@ -122,84 +100,4 @@ class KeymapManager private constructor() : Disposable {
|
||||
database.delete(id, DataType.Keymap.name)
|
||||
}
|
||||
|
||||
private inner class KeymapKeyEventDispatcher : KeyEventDispatcher {
|
||||
|
||||
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
|
||||
if (e.isConsumed || e.id != KeyEvent.KEY_PRESSED || e.modifiersEx == 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
val keyStroke = KeyStroke.getKeyStrokeForEvent(e)
|
||||
val component = e.source
|
||||
|
||||
if (component is JComponent) {
|
||||
// 如果这个键已经被组件注册了,那么忽略
|
||||
if (getConditionForKeyStroke(component, keyStroke) != JComponent.UNDEFINED_CONDITION) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val shortcuts = getActiveKeymap()
|
||||
val actionIds = shortcuts.getActionIds(KeyShortcut(keyStroke))
|
||||
if (actionIds.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val focusedWindow = keyboardFocusManager.focusedWindow
|
||||
if (focusedWindow is DialogWrapper) {
|
||||
if (!focusedWindow.processGlobalKeymap) {
|
||||
return false
|
||||
}
|
||||
} else if (focusedWindow is JDialog) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果当前有 Popup ,那么不派发事件
|
||||
val c = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||
if (c is Container) {
|
||||
val popups: List<JPopupMenu> = SwingUtils.getDescendantsOfType(
|
||||
JPopupMenu::class.java,
|
||||
c, true
|
||||
)
|
||||
if (popups.isNotEmpty()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val evt = AnActionEvent(e.source, StringUtils.EMPTY, e)
|
||||
for (actionId in actionIds) {
|
||||
val action = ActionManager.getInstance().getAction(actionId) ?: continue
|
||||
if (!action.isEnabled) {
|
||||
continue
|
||||
}
|
||||
action.actionPerformed(evt)
|
||||
if (evt.isConsumed) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun getConditionForKeyStroke(c: JComponent, keyStroke: KeyStroke): Int {
|
||||
val condition = c.getConditionForKeyStroke(keyStroke)
|
||||
|
||||
// 如果这个键已经被组件注册了,那么忽略
|
||||
if (condition != JComponent.UNDEFINED_CONDITION) {
|
||||
return condition
|
||||
}
|
||||
|
||||
if (c.parent is JComponent) {
|
||||
return getConditionForKeyStroke(c.parent as JComponent, keyStroke)
|
||||
}
|
||||
|
||||
return JComponent.UNDEFINED_CONDITION
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
keyboardFocusManager.removeKeyEventDispatcher(keymapKeyEventDispatcher)
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,17 @@ package app.termora.plugin
|
||||
|
||||
import app.termora.Application
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.FramePlugin
|
||||
import app.termora.account.AccountPlugin
|
||||
import app.termora.plugin.internal.badge.BadgePlugin
|
||||
import app.termora.plugin.internal.extension.DynamicExtensionPlugin
|
||||
import app.termora.plugin.internal.local.LocalInternalPlugin
|
||||
import app.termora.plugin.internal.plugin.PluginInternalPlugin
|
||||
import app.termora.plugin.internal.rdp.RDPInternalPlugin
|
||||
import app.termora.plugin.internal.serial.SerialInternalPlugin
|
||||
import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin
|
||||
import app.termora.plugin.internal.ssh.SSHInternalPlugin
|
||||
import app.termora.plugin.internal.telnet.TelnetInternalPlugin
|
||||
import app.termora.plugin.internal.update.UpdatePlugin
|
||||
import app.termora.plugin.internal.wsl.WSLInternalPlugin
|
||||
import app.termora.swingCoroutineScope
|
||||
import app.termora.terminal.panel.vw.FloatingToolbarPlugin
|
||||
@@ -109,6 +110,10 @@ internal class PluginManager private constructor() {
|
||||
plugins.add(PluginDescriptor(AccountPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||
// badge plugin
|
||||
plugins.add(PluginDescriptor(BadgePlugin(), origin = PluginOrigin.Internal, version = version))
|
||||
// update plugin
|
||||
plugins.add(PluginDescriptor(UpdatePlugin(), origin = PluginOrigin.Internal, version = version))
|
||||
// frame plugin
|
||||
plugins.add(PluginDescriptor(FramePlugin(), origin = PluginOrigin.Internal, version = version))
|
||||
|
||||
// ssh plugin
|
||||
plugins.add(PluginDescriptor(SSHInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||
@@ -118,8 +123,6 @@ internal class PluginManager private constructor() {
|
||||
plugins.add(PluginDescriptor(RDPInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||
// telnet plugin
|
||||
plugins.add(PluginDescriptor(TelnetInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||
// serial plugin
|
||||
plugins.add(PluginDescriptor(SerialInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||
// wsl plugin
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
plugins.add(PluginDescriptor(WSLInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package app.termora.plugin.internal
|
||||
|
||||
enum class AltKeyModifier {
|
||||
EightBit,
|
||||
CharactersPrecededByESC,
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import javax.swing.*
|
||||
|
||||
internal class BasicGeneralOption : JPanel(BorderLayout()), OptionsPane.Option {
|
||||
class BasicGeneralOption : JPanel(BorderLayout()), OptionsPane.Option {
|
||||
val nameTextField = OutlineTextField(128)
|
||||
val remarkTextArea = FixedLengthTextArea(512)
|
||||
private val formMargin = "7dlu"
|
||||
|
||||
@@ -78,8 +78,6 @@ class BasicProxyOption(
|
||||
proxyAuthenticationTypeComboBox.addItem(type)
|
||||
}
|
||||
|
||||
proxyUsernameTextField.text = "root"
|
||||
|
||||
refreshStates()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package app.termora.plugin.internal
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.OptionsPane.Companion.FORM_MARGIN
|
||||
import app.termora.OptionsPane.Option
|
||||
import app.termora.plugin.internal.telnet.TelnetHostOptionsPane.Backspace
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.nio.charset.Charset
|
||||
import javax.swing.*
|
||||
|
||||
class BasicTerminalOption() : JPanel(BorderLayout()), Option {
|
||||
|
||||
var showCharsetComboBox: Boolean = false
|
||||
var showStartupCommandTextField: Boolean = false
|
||||
var showHeartbeatIntervalTextField: Boolean = false
|
||||
var showEnvironmentTextArea: Boolean = false
|
||||
var showLoginScripts: Boolean = false
|
||||
var showBackspaceComboBox: Boolean = false
|
||||
var showCharacterAtATimeTextField: Boolean = false
|
||||
var showAltModifierComboBox: Boolean = true
|
||||
|
||||
val charsetComboBox = JComboBox<String>()
|
||||
val startupCommandTextField = OutlineTextField()
|
||||
val heartbeatIntervalTextField = IntSpinner(30, minimum = 3, maximum = Int.MAX_VALUE)
|
||||
val environmentTextArea = FixedLengthTextArea(2048)
|
||||
val loginScripts = mutableListOf<LoginScript>()
|
||||
val backspaceComboBox = JComboBox<Backspace>()
|
||||
val altModifierComboBox = JComboBox<AltKeyModifier>()
|
||||
val characterAtATimeTextField = YesOrNoComboBox()
|
||||
|
||||
|
||||
private val loginScriptPanel = LoginScriptPanel(loginScripts)
|
||||
private val tabbed = FlatTabbedPane()
|
||||
|
||||
fun init() {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
|
||||
if (showLoginScripts) {
|
||||
tabbed.styleMap = mapOf(
|
||||
"focusColor" to DynamicColor("TabbedPane.background"),
|
||||
"hoverColor" to DynamicColor("TabbedPane.background"),
|
||||
)
|
||||
tabbed.tabHeight = UIManager.getInt("TabbedPane.tabHeight") - 4
|
||||
putClientProperty("ContentPanelBorder", BorderFactory.createEmptyBorder())
|
||||
tabbed.addTab(I18n.getString("termora.new-host.general"), getCenterComponent())
|
||||
tabbed.addTab(I18n.getString("termora.new-host.terminal.login-scripts"), loginScriptPanel)
|
||||
add(tabbed, BorderLayout.CENTER)
|
||||
} else {
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
if (showAltModifierComboBox) {
|
||||
altModifierComboBox.addItem(AltKeyModifier.EightBit)
|
||||
altModifierComboBox.addItem(AltKeyModifier.CharactersPrecededByESC)
|
||||
|
||||
altModifierComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component? {
|
||||
var text = value?.toString() ?: value
|
||||
if (value == AltKeyModifier.CharactersPrecededByESC) {
|
||||
text = I18n.getString("termora.new-host.terminal.alt-modifier.by-esc")
|
||||
} else if (value == AltKeyModifier.EightBit) {
|
||||
text = I18n.getString("termora.new-host.terminal.alt-modifier.eight-bit")
|
||||
}
|
||||
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (showBackspaceComboBox) {
|
||||
backspaceComboBox.addItem(Backspace.Delete)
|
||||
backspaceComboBox.addItem(Backspace.Backspace)
|
||||
backspaceComboBox.addItem(Backspace.VT220)
|
||||
}
|
||||
|
||||
if (showCharacterAtATimeTextField) {
|
||||
characterAtATimeTextField.selectedItem = false
|
||||
}
|
||||
|
||||
environmentTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
environmentTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
|
||||
environmentTextArea.rows = 8
|
||||
environmentTextArea.lineWrap = true
|
||||
environmentTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
|
||||
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, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val builder = FormBuilder.create().layout(layout)
|
||||
if (showLoginScripts) {
|
||||
builder.border(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
||||
}
|
||||
|
||||
if (showCharsetComboBox) {
|
||||
builder.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
||||
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
||||
}
|
||||
|
||||
if (showAltModifierComboBox) {
|
||||
builder.add("${I18n.getString("termora.new-host.terminal.alt-modifier")}:").xy(1, rows)
|
||||
.add(altModifierComboBox).xy(3, rows).apply { rows += step }
|
||||
}
|
||||
|
||||
if (showBackspaceComboBox) {
|
||||
builder.add("${I18n.getString("termora.new-host.terminal.backspace")}:").xy(1, rows)
|
||||
.add(backspaceComboBox).xy(3, rows).apply { rows += step }
|
||||
}
|
||||
|
||||
if (showCharacterAtATimeTextField) {
|
||||
builder
|
||||
.add("${I18n.getString("termora.new-host.terminal.character-mode")}:").xy(1, rows)
|
||||
.add(characterAtATimeTextField).xy(3, rows).apply { rows += step }
|
||||
}
|
||||
|
||||
if (showHeartbeatIntervalTextField) {
|
||||
builder.add("${I18n.getString("termora.new-host.terminal.heartbeat-interval")}:").xy(1, rows)
|
||||
.add(heartbeatIntervalTextField).xy(3, rows).apply { rows += step }
|
||||
}
|
||||
|
||||
if (showStartupCommandTextField) {
|
||||
builder.add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows)
|
||||
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
|
||||
}
|
||||
|
||||
|
||||
if (showEnvironmentTextArea) {
|
||||
builder.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
|
||||
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
|
||||
.apply { rows += step }
|
||||
}
|
||||
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user