mirror of https://github.com/ospab/ostp.git
feat: GUI v2 redesign + CI/CD speedup
GUI (ostp-gui): - Complete HTML rewrite: orbit rings, server badge, metrics bar, peek-key - CSS design system v2: ambient blobs, glassmorphism card, richer token set orbit animation (connected/connecting states), breathing power button, modern toggle component with thumb, toast variants (ok/error/default) - main.js: clean state machine, server badge, TUN/SOCKS5 mode label, peek-key toggle, toast variants, import link, uptime counter CI/CD (.github/workflows/release.yml): - Replaced swatinem/rust-cache with actions/cache@v4 (per-target key) - Cache cross binary: skip reinstall on cache hit (~3 min saved per job) - Cache tauri-cli binary: skip reinstall on cache hit (~2 min saved per GUI job) - Added npm cache (cache-dependency-path: ostp-gui/package-lock.json) - Removed redundant pre-flight cargo check step (duplicates build step) - Cleaned up packaging scripts (inline vars, smaller surface area)
This commit is contained in:
parent
3a16373a31
commit
ee14a60348
|
|
@ -9,6 +9,12 @@ on:
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
|
# ── Global defaults ─────────────────────────────────────────────────────────
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-release-matrix:
|
publish-release-matrix:
|
||||||
name: Release for ${{ matrix.target }}
|
name: Release for ${{ matrix.target }}
|
||||||
|
|
@ -17,21 +23,21 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
# ==========================================
|
# ── Windows ──────────────────────────────────────────────────────
|
||||||
# 🏁 WINDOWS ECOSYSTEM
|
|
||||||
# ==========================================
|
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
artifact_name: ostp.exe
|
artifact_name: ostp.exe
|
||||||
release_name: ostp-windows-amd64.zip
|
release_name: ostp-windows-amd64.zip
|
||||||
tun2socks_arch: windows-amd64
|
tun2socks_arch: windows-amd64
|
||||||
wintun_arch: amd64
|
wintun_arch: amd64
|
||||||
|
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
target: i686-pc-windows-msvc
|
target: i686-pc-windows-msvc
|
||||||
artifact_name: ostp.exe
|
artifact_name: ostp.exe
|
||||||
release_name: ostp-windows-386.zip
|
release_name: ostp-windows-386.zip
|
||||||
tun2socks_arch: windows-386
|
tun2socks_arch: windows-386
|
||||||
wintun_arch: x86
|
wintun_arch: x86
|
||||||
|
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
target: aarch64-pc-windows-msvc
|
target: aarch64-pc-windows-msvc
|
||||||
artifact_name: ostp.exe
|
artifact_name: ostp.exe
|
||||||
|
|
@ -39,45 +45,47 @@ jobs:
|
||||||
tun2socks_arch: windows-arm64
|
tun2socks_arch: windows-arm64
|
||||||
wintun_arch: arm64
|
wintun_arch: arm64
|
||||||
|
|
||||||
# ==========================================
|
# ── macOS ─────────────────────────────────────────────────────────
|
||||||
# 🍏 APPLE DARWIN (macOS)
|
|
||||||
# ==========================================
|
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
artifact_name: ostp
|
artifact_name: ostp
|
||||||
release_name: ostp-darwin-amd64.tar.gz
|
release_name: ostp-darwin-amd64.tar.gz
|
||||||
tun2socks_arch: darwin-amd64
|
tun2socks_arch: darwin-amd64
|
||||||
|
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
target: aarch64-apple-darwin
|
target: aarch64-apple-darwin
|
||||||
artifact_name: ostp
|
artifact_name: ostp
|
||||||
release_name: ostp-darwin-arm64.tar.gz
|
release_name: ostp-darwin-arm64.tar.gz
|
||||||
tun2socks_arch: darwin-arm64
|
tun2socks_arch: darwin-arm64
|
||||||
|
|
||||||
# ==========================================
|
# ── Linux native ──────────────────────────────────────────────────
|
||||||
# 🐧 LINUX & FreeBSD STANDARD
|
|
||||||
# ==========================================
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: x86_64-unknown-linux-musl
|
target: x86_64-unknown-linux-musl
|
||||||
artifact_name: ostp
|
artifact_name: ostp
|
||||||
release_name: ostp-linux-amd64.tar.gz
|
release_name: ostp-linux-amd64.tar.gz
|
||||||
tun2socks_arch: linux-amd64
|
tun2socks_arch: linux-amd64
|
||||||
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: i686-unknown-linux-musl
|
target: i686-unknown-linux-musl
|
||||||
artifact_name: ostp
|
artifact_name: ostp
|
||||||
release_name: ostp-linux-386.tar.gz
|
release_name: ostp-linux-386.tar.gz
|
||||||
tun2socks_arch: linux-386
|
tun2socks_arch: linux-386
|
||||||
|
|
||||||
|
# ── Linux cross ───────────────────────────────────────────────────
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: aarch64-unknown-linux-musl
|
target: aarch64-unknown-linux-musl
|
||||||
artifact_name: ostp
|
artifact_name: ostp
|
||||||
release_name: ostp-linux-arm64.tar.gz
|
release_name: ostp-linux-arm64.tar.gz
|
||||||
tun2socks_arch: linux-arm64
|
tun2socks_arch: linux-arm64
|
||||||
use_cross: true
|
use_cross: true
|
||||||
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: armv7-unknown-linux-musleabihf
|
target: armv7-unknown-linux-musleabihf
|
||||||
artifact_name: ostp
|
artifact_name: ostp
|
||||||
release_name: ostp-linux-armv7.tar.gz
|
release_name: ostp-linux-armv7.tar.gz
|
||||||
tun2socks_arch: linux-armv7
|
tun2socks_arch: linux-armv7
|
||||||
use_cross: true
|
use_cross: true
|
||||||
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: x86_64-unknown-freebsd
|
target: x86_64-unknown-freebsd
|
||||||
artifact_name: ostp
|
artifact_name: ostp
|
||||||
|
|
@ -85,9 +93,6 @@ jobs:
|
||||||
tun2socks_arch: freebsd-amd64
|
tun2socks_arch: freebsd-amd64
|
||||||
use_cross: true
|
use_cross: true
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# 🛰️ ROUTER & SPECIAL ARCHITECTURES (Cross)
|
|
||||||
# ==========================================
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: mipsel-unknown-linux-musl
|
target: mipsel-unknown-linux-musl
|
||||||
artifact_name: ostp
|
artifact_name: ostp
|
||||||
|
|
@ -95,6 +100,7 @@ jobs:
|
||||||
tun2socks_arch: linux-mipsle-softfloat
|
tun2socks_arch: linux-mipsle-softfloat
|
||||||
use_cross: true
|
use_cross: true
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: riscv64gc-unknown-linux-gnu
|
target: riscv64gc-unknown-linux-gnu
|
||||||
artifact_name: ostp
|
artifact_name: ostp
|
||||||
|
|
@ -102,9 +108,6 @@ jobs:
|
||||||
tun2socks_arch: linux-riscv64
|
tun2socks_arch: linux-riscv64
|
||||||
use_cross: true
|
use_cross: true
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# 🤖 MOBILE & EMBEDDED SUITE (Cross)
|
|
||||||
# ==========================================
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: aarch64-linux-android
|
target: aarch64-linux-android
|
||||||
artifact_name: ostp
|
artifact_name: ostp
|
||||||
|
|
@ -116,102 +119,102 @@ jobs:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Initialize Rust ecosystem (Native Host)
|
# ── Rust toolchain ─────────────────────────────────────────────────────
|
||||||
if: ${{ !matrix.use_cross }}
|
- name: Setup Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ matrix.toolchain || 'stable' }}
|
toolchain: ${{ matrix.toolchain || 'stable' }}
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ !matrix.use_cross && matrix.target || '' }}
|
||||||
|
|
||||||
- name: Initialize Rust ecosystem (Cross Container Host)
|
# ── Cargo cache (shared per target) ───────────────────────────────────
|
||||||
if: ${{ matrix.use_cross }}
|
- name: Restore Cargo cache
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ matrix.toolchain || 'stable' }}
|
path: |
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
target/
|
||||||
|
key: cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
cargo-${{ matrix.target }}-
|
||||||
|
|
||||||
- name: Activate rust compilation caching
|
# ── MUSL tools for native Linux musl builds ────────────────────────────
|
||||||
if: ${{ !matrix.use_cross }}
|
- name: Install musl-tools
|
||||||
uses: swatinem/rust-cache@v2
|
|
||||||
|
|
||||||
- name: Setup local MUSL linker dependencies
|
|
||||||
if: ${{ matrix.os == 'ubuntu-latest' && !matrix.use_cross }}
|
if: ${{ matrix.os == 'ubuntu-latest' && !matrix.use_cross }}
|
||||||
run: sudo apt-get update && sudo apt-get install -y musl-tools
|
run: sudo apt-get update && sudo apt-get install -y musl-tools
|
||||||
|
|
||||||
- name: Pre-flight compilation check
|
# ── Native build ───────────────────────────────────────────────────────
|
||||||
|
- name: Build (native)
|
||||||
if: ${{ !matrix.use_cross }}
|
if: ${{ !matrix.use_cross }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: cargo build --release --target ${{ matrix.target }} --bin ostp
|
||||||
cargo check --target ${{ matrix.target }} --bin ostp 2>&1
|
|
||||||
echo "Pre-flight check passed."
|
|
||||||
|
|
||||||
- name: Execute Standard Native Compilation (Windows)
|
# ── Cross build ────────────────────────────────────────────────────────
|
||||||
if: ${{ !matrix.use_cross && matrix.os == 'windows-latest' }}
|
- name: Restore cross binary cache
|
||||||
run: |
|
|
||||||
cargo build --release --target ${{ matrix.target }} --bin ostp
|
|
||||||
|
|
||||||
- name: Execute Standard Native Compilation (Unix)
|
|
||||||
if: ${{ !matrix.use_cross && matrix.os != 'windows-latest' }}
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
cargo build --release --target ${{ matrix.target }} --bin ostp
|
|
||||||
|
|
||||||
- name: Execute Specialized Cross-Compilation
|
|
||||||
if: ${{ matrix.use_cross }}
|
if: ${{ matrix.use_cross }}
|
||||||
run: |
|
id: cross-cache
|
||||||
cargo install cross --git https://github.com/cross-rs/cross.git
|
uses: actions/cache@v4
|
||||||
cross build --release --target ${{ matrix.target }} --bin ostp
|
with:
|
||||||
|
path: ~/.cargo/bin/cross
|
||||||
|
key: cross-bin-${{ runner.os }}-v1
|
||||||
|
|
||||||
- name: Inject Win32 Driver Dependencies (Windows)
|
- name: Install cross (if not cached)
|
||||||
|
if: ${{ matrix.use_cross && steps.cross-cache.outputs.cache-hit != 'true' }}
|
||||||
|
run: cargo install cross --git https://github.com/cross-rs/cross.git --locked
|
||||||
|
|
||||||
|
- name: Build (cross)
|
||||||
|
if: ${{ matrix.use_cross }}
|
||||||
|
run: cross build --release --target ${{ matrix.target }} --bin ostp
|
||||||
|
|
||||||
|
# ── Driver dependencies ────────────────────────────────────────────────
|
||||||
|
- name: Download tun2socks + wintun (Windows)
|
||||||
if: ${{ matrix.os == 'windows-latest' && matrix.tun2socks_arch }}
|
if: ${{ matrix.os == 'windows-latest' && matrix.tun2socks_arch }}
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
cd target/${{ matrix.target }}/release
|
|
||||||
$ProgressPreference = 'SilentlyContinue'
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
# 1. Acquire tun2socks
|
$dir = "target/${{ matrix.target }}/release"
|
||||||
Invoke-WebRequest -Uri "https://github.com/xjasonlyu/tun2socks/releases/download/v2.6.0/tun2socks-${{ matrix.tun2socks_arch }}.zip" -OutFile "tun2socks.zip"
|
Invoke-WebRequest -Uri "https://github.com/xjasonlyu/tun2socks/releases/download/v2.6.0/tun2socks-${{ matrix.tun2socks_arch }}.zip" -OutFile "$dir/t2s.zip"
|
||||||
Expand-Archive -Path "tun2socks.zip" -DestinationPath "tun_temp" -Force
|
Expand-Archive "$dir/t2s.zip" -DestinationPath "$dir/t2s_tmp" -Force
|
||||||
Get-ChildItem -Path "tun_temp" -Filter "*.exe" -Recurse | Copy-Item -Destination "tun2socks.exe" -Force
|
Get-ChildItem "$dir/t2s_tmp" -Filter "*.exe" -Recurse | Select-Object -First 1 | Copy-Item -Destination "$dir/tun2socks.exe"
|
||||||
# 2. Acquire wintun
|
Invoke-WebRequest -Uri "https://www.wintun.net/builds/wintun-0.14.1.zip" -OutFile "$dir/wt.zip"
|
||||||
Invoke-WebRequest -Uri "https://www.wintun.net/builds/wintun-0.14.1.zip" -OutFile "wintun.zip"
|
Expand-Archive "$dir/wt.zip" -DestinationPath "$dir/wt_tmp" -Force
|
||||||
Expand-Archive -Path "wintun.zip" -DestinationPath "wintun_temp" -Force
|
Get-ChildItem "$dir/wt_tmp" -Filter "wintun.dll" -Recurse | Where-Object { $_.FullName -match 'bin[\\/]${{ matrix.wintun_arch }}[\\/]' } | Copy-Item -Destination "$dir/"
|
||||||
Get-ChildItem -Path "wintun_temp" -Filter "wintun.dll" -Recurse | Where-Object { $_.FullName -match 'bin[\\/]${{ matrix.wintun_arch }}[\\/]' } | Copy-Item -Destination "." -Force
|
Remove-Item "$dir/t2s.zip","$dir/t2s_tmp","$dir/wt.zip","$dir/wt_tmp" -Recurse -Force
|
||||||
# Cleanup
|
|
||||||
Remove-Item "tun2socks.zip", "tun_temp", "wintun.zip", "wintun_temp" -Recurse -Force
|
|
||||||
|
|
||||||
- name: Inject Unix Driver Dependencies (Unix)
|
- name: Download tun2socks (Unix)
|
||||||
if: ${{ matrix.os != 'windows-latest' && matrix.tun2socks_arch }}
|
if: ${{ matrix.os != 'windows-latest' && matrix.tun2socks_arch }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cd target/${{ matrix.target }}/release
|
dir="target/${{ matrix.target }}/release"
|
||||||
# All platforms in tun2socks v2.6.0 use .zip packaging
|
|
||||||
URL="https://github.com/xjasonlyu/tun2socks/releases/download/v2.6.0/tun2socks-${{ matrix.tun2socks_arch }}.zip"
|
URL="https://github.com/xjasonlyu/tun2socks/releases/download/v2.6.0/tun2socks-${{ matrix.tun2socks_arch }}.zip"
|
||||||
curl -f -L "$URL" -o "tun2socks.zip" || { echo "Failed to download tun2socks"; exit 0; }
|
curl -fsSL "$URL" -o "$dir/t2s.zip" || exit 0
|
||||||
unzip -o "tun2socks.zip"
|
unzip -o "$dir/t2s.zip" -d "$dir/t2s_tmp"
|
||||||
find . -maxdepth 2 -name "tun2socks*" ! -name "*.zip" -type f -exec mv {} ./tun2socks \;
|
find "$dir/t2s_tmp" -type f -name "tun2socks*" ! -name "*.zip" | head -1 | xargs -I{} cp {} "$dir/tun2socks"
|
||||||
rm -f "tun2socks.zip"
|
chmod +x "$dir/tun2socks" 2>/dev/null || true
|
||||||
chmod +x tun2socks || true
|
rm -rf "$dir/t2s.zip" "$dir/t2s_tmp"
|
||||||
|
|
||||||
- name: Package release artifact (Windows)
|
# ── Package ────────────────────────────────────────────────────────────
|
||||||
|
- name: Package (Windows)
|
||||||
if: ${{ matrix.os == 'windows-latest' }}
|
if: ${{ matrix.os == 'windows-latest' }}
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
cd target/${{ matrix.target }}/release
|
$dir = "target/${{ matrix.target }}/release"
|
||||||
$files = @("ostp.exe")
|
$files = @("$dir/ostp.exe")
|
||||||
if (Test-Path "tun2socks.exe") { $files += "tun2socks.exe" }
|
if (Test-Path "$dir/tun2socks.exe") { $files += "$dir/tun2socks.exe" }
|
||||||
if (Test-Path "wintun.dll") { $files += "wintun.dll" }
|
if (Test-Path "$dir/wintun.dll") { $files += "$dir/wintun.dll" }
|
||||||
Compress-Archive -Path $files -DestinationPath ../../../${{ matrix.release_name }} -Force
|
Compress-Archive -Path $files -DestinationPath "${{ matrix.release_name }}" -Force
|
||||||
|
|
||||||
- name: Package release artifact (Unix Systems)
|
- name: Package (Unix)
|
||||||
if: ${{ matrix.os != 'windows-latest' }}
|
if: ${{ matrix.os != 'windows-latest' }}
|
||||||
run: |
|
run: |
|
||||||
cd target/${{ matrix.target }}/release
|
dir="target/${{ matrix.target }}/release"
|
||||||
FILES="${{ matrix.artifact_name }}"
|
FILES="$dir/${{ matrix.artifact_name }}"
|
||||||
if [ -f "tun2socks" ]; then
|
[ -f "$dir/tun2socks" ] && FILES="$FILES $dir/tun2socks"
|
||||||
FILES="$FILES tun2socks"
|
tar -czf "${{ matrix.release_name }}" -C "$dir" $(basename $FILES | tr '\n' ' ')
|
||||||
fi
|
|
||||||
tar -czf ../../../${{ matrix.release_name }} $FILES
|
|
||||||
|
|
||||||
- name: Inject artifact to Global GitHub Release Assets
|
# ── Upload ─────────────────────────────────────────────────────────────
|
||||||
|
- name: Upload to GitHub Release
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
|
|
@ -219,11 +222,9 @@ jobs:
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# ==========================================
|
# ── Windows GUI (Tauri) ──────────────────────────────────────────────────
|
||||||
# Windows GUI (Tauri) Build
|
|
||||||
# ==========================================
|
|
||||||
publish-gui:
|
publish-gui:
|
||||||
name: GUI for Windows ${{ matrix.arch_label }}
|
name: GUI Windows ${{ matrix.arch_label }}
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
@ -247,58 +248,70 @@ jobs:
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Activate Rust caching
|
- name: Restore Cargo cache
|
||||||
uses: swatinem/rust-cache@v2
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
target/
|
||||||
|
ostp-gui/src-tauri/target/
|
||||||
|
key: cargo-gui-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
cargo-gui-${{ matrix.target }}-
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: ostp-gui/package-lock.json
|
||||||
|
|
||||||
- name: Install Tauri CLI
|
# Cache tauri-cli binary to skip reinstall every run
|
||||||
run: cargo install tauri-cli --version "^2"
|
- name: Cache tauri-cli
|
||||||
|
id: tauri-cli-cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/bin/cargo-tauri*
|
||||||
|
key: tauri-cli-v2-${{ runner.os }}
|
||||||
|
|
||||||
- name: Build Tun Helper
|
- name: Install Tauri CLI (if not cached)
|
||||||
run: |
|
if: steps.tauri-cli-cache.outputs.cache-hit != 'true'
|
||||||
cargo build --release --target ${{ matrix.target }} -p ostp-tun-helper
|
run: cargo install tauri-cli --version "^2" --locked
|
||||||
|
|
||||||
- name: Build Tauri application
|
- name: Build tun-helper
|
||||||
|
run: cargo build --release --target ${{ matrix.target }} -p ostp-tun-helper
|
||||||
|
|
||||||
|
- name: Build Tauri app
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
cd ostp-gui
|
cd ostp-gui
|
||||||
cargo tauri build --target ${{ matrix.target }}
|
cargo tauri build --target ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Package GUI release
|
- name: Package GUI
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
$ProgressPreference = 'SilentlyContinue'
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
$dist = "ostp-gui-dist"
|
$dist = "ostp-gui-dist"
|
||||||
New-Item -ItemType Directory -Force -Path $dist
|
New-Item -ItemType Directory -Force -Path $dist
|
||||||
|
|
||||||
# 1. Copy the Tauri executable
|
$exePath = Get-ChildItem "ostp-gui/src-tauri/target/${{ matrix.target }}/release" -Filter "ostp-gui.exe" | Select-Object -First 1
|
||||||
$exePath = Get-ChildItem -Path "ostp-gui/src-tauri/target/${{ matrix.target }}/release" -Filter "ostp-gui.exe" | Select-Object -First 1
|
Copy-Item $exePath.FullName "$dist/ostp-gui.exe"
|
||||||
Copy-Item $exePath.FullName -Destination "$dist/ostp-gui.exe"
|
Copy-Item "target/${{ matrix.target }}/release/ostp-tun-helper.exe" "$dist/ostp-tun-helper.exe"
|
||||||
|
|
||||||
# 1.5 Copy the Tun Helper executable
|
Invoke-WebRequest "https://github.com/xjasonlyu/tun2socks/releases/download/v2.6.0/tun2socks-${{ matrix.tun2socks_arch }}.zip" -OutFile "t2s.zip"
|
||||||
Copy-Item "target/${{ matrix.target }}/release/ostp-tun-helper.exe" -Destination "$dist/ostp-tun-helper.exe"
|
Expand-Archive "t2s.zip" "t2s_tmp" -Force
|
||||||
|
Get-ChildItem "t2s_tmp" -Filter "*.exe" -Recurse | Select-Object -First 1 | Copy-Item -Destination "$dist/tun2socks.exe"
|
||||||
|
|
||||||
# 2. Download tun2socks
|
Invoke-WebRequest "https://www.wintun.net/builds/wintun-0.14.1.zip" -OutFile "wt.zip"
|
||||||
Invoke-WebRequest -Uri "https://github.com/xjasonlyu/tun2socks/releases/download/v2.6.0/tun2socks-${{ matrix.tun2socks_arch }}.zip" -OutFile "tun2socks.zip"
|
Expand-Archive "wt.zip" "wt_tmp" -Force
|
||||||
Expand-Archive -Path "tun2socks.zip" -DestinationPath "tun_temp" -Force
|
Get-ChildItem "wt_tmp" -Filter "wintun.dll" -Recurse | Where-Object { $_.FullName -match 'bin[\\/]${{ matrix.wintun_arch }}[\\/]' } | Copy-Item -Destination "$dist/"
|
||||||
Get-ChildItem -Path "tun_temp" -Filter "*.exe" -Recurse | Copy-Item -Destination "$dist/tun2socks.exe" -Force
|
|
||||||
|
|
||||||
# 3. Download wintun
|
Compress-Archive "$dist/*" "ostp-windows-gui-${{ matrix.arch_label }}.zip" -Force
|
||||||
Invoke-WebRequest -Uri "https://www.wintun.net/builds/wintun-0.14.1.zip" -OutFile "wintun.zip"
|
Remove-Item "t2s.zip","t2s_tmp","wt.zip","wt_tmp",$dist -Recurse -Force
|
||||||
Expand-Archive -Path "wintun.zip" -DestinationPath "wintun_temp" -Force
|
|
||||||
Get-ChildItem -Path "wintun_temp" -Filter "wintun.dll" -Recurse | Where-Object { $_.FullName -match 'bin[\\/]${{ matrix.wintun_arch }}[\\/]' } | Copy-Item -Destination "$dist/wintun.dll" -Force
|
|
||||||
|
|
||||||
# 4. Create zip archive
|
- name: Upload GUI to release
|
||||||
Compress-Archive -Path "$dist/*" -DestinationPath "ostp-windows-gui-${{ matrix.arch_label }}.zip" -Force
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
Remove-Item "tun2socks.zip", "tun_temp", "wintun.zip", "wintun_temp", $dist -Recurse -Force
|
|
||||||
|
|
||||||
- name: Upload GUI artifact to release
|
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -2,159 +2,245 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="stylesheet" href="styles.css" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>OSTP Client</title>
|
<title>OSTP</title>
|
||||||
<script type="module" src="main.js" defer></script>
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" />
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-container">
|
<div class="app-root">
|
||||||
<!-- Dynamic Mesh Background Particles -->
|
|
||||||
<div class="mesh-bg"></div>
|
|
||||||
<div class="blur-overlay"></div>
|
|
||||||
|
|
||||||
<!-- Main Screen -->
|
<!-- Ambient light blobs -->
|
||||||
<div id="home-screen" class="screen active">
|
<div class="ambient" aria-hidden="true">
|
||||||
<header class="app-header">
|
<div class="blob blob-1"></div>
|
||||||
<div class="logo-container">
|
<div class="blob blob-2"></div>
|
||||||
<div class="logo-icon"></div>
|
<div class="blob blob-3"></div>
|
||||||
<h1>OSTP</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
|
||||||
<button id="btn-lang" class="icon-btn lang-btn" aria-label="Language" title="Language">
|
<!-- ── HOME SCREEN ──────────────────────────────────────────── -->
|
||||||
|
<div id="home-screen" class="screen active">
|
||||||
|
|
||||||
|
<!-- Top bar -->
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="brand">
|
||||||
|
<div class="brand-dot" id="brand-dot"></div>
|
||||||
|
<span class="brand-name">OSTP</span>
|
||||||
|
</div>
|
||||||
|
<div class="topbar-right">
|
||||||
|
<button id="btn-lang" class="pill-btn" title="Language">
|
||||||
<span id="lang-label">EN</span>
|
<span id="lang-label">EN</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="btn-go-settings" class="icon-btn" aria-label="Settings">
|
<button id="btn-go-settings" class="icon-btn" aria-label="Settings">
|
||||||
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
<!-- Gear icon -->
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="main-content">
|
<!-- Center stage -->
|
||||||
<div class="power-button-container">
|
<main class="stage">
|
||||||
<div class="pulse-ring"></div>
|
|
||||||
<div class="pulse-ring delay-1"></div>
|
<!-- Orbit rings -->
|
||||||
<button id="btn-connect" class="power-btn">
|
<div class="orbit-wrap" id="orbit-wrap">
|
||||||
|
<div class="orbit orbit-1"></div>
|
||||||
|
<div class="orbit orbit-2"></div>
|
||||||
|
<div class="orbit orbit-3"></div>
|
||||||
|
|
||||||
|
<!-- Power button -->
|
||||||
|
<button id="btn-connect" class="power-btn" aria-label="Connect">
|
||||||
<div class="power-icon">
|
<div class="power-icon">
|
||||||
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path><line x1="12" y1="2" x2="12" y2="12"></line></svg>
|
<svg width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"/>
|
||||||
|
<line x1="12" y1="2" x2="12" y2="12"/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="status-display">
|
<!-- Status block -->
|
||||||
<span id="status-text" class="status-disconnected" data-i18n="status_disconnected">Disconnected</span>
|
<div class="status-block">
|
||||||
<span id="uptime-text" class="subtext" data-i18n="hint_tap">Tap to protect your traffic</span>
|
<div id="status-text" class="status-label" data-i18n="status_disconnected">Disconnected</div>
|
||||||
|
<div id="uptime-text" class="status-sub" data-i18n="hint_tap">Tap to protect your traffic</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="metrics-grid">
|
<!-- Server badge (shown when connected) -->
|
||||||
<div class="metric-card glass">
|
<div id="server-badge" class="server-badge hidden">
|
||||||
<div class="metric-icon down">
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M19 12l-7 7-7-7"/></svg>
|
<rect x="2" y="2" width="20" height="8" rx="2"/>
|
||||||
|
<rect x="2" y="14" width="20" height="8" rx="2"/>
|
||||||
|
<line x1="6" y1="6" x2="6.01" y2="6"/>
|
||||||
|
<line x1="6" y1="18" x2="6.01" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
<span id="server-badge-text">—</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-data">
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Traffic metrics bar -->
|
||||||
|
<footer class="metrics-bar">
|
||||||
|
<div class="metric">
|
||||||
|
<div class="metric-icon down-icon">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 5v14M19 12l-7 7-7-7"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="metric-body">
|
||||||
<span class="metric-label" data-i18n="download">Download</span>
|
<span class="metric-label" data-i18n="download">Download</span>
|
||||||
<span id="metric-down" class="metric-value">0.0 B</span>
|
<span id="metric-down" class="metric-value">0 B</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="metric-card glass">
|
|
||||||
<div class="metric-icon up">
|
|
||||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>
|
|
||||||
</div>
|
|
||||||
<div class="metric-data">
|
|
||||||
<span class="metric-label" data-i18n="upload">Upload</span>
|
|
||||||
<span id="metric-up" class="metric-value">0.0 B</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings Screen -->
|
<div class="metric-sep"></div>
|
||||||
|
|
||||||
|
<div class="metric">
|
||||||
|
<div class="metric-icon up-icon">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 19V5M5 12l7-7 7 7"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="metric-body">
|
||||||
|
<span class="metric-label" data-i18n="upload">Upload</span>
|
||||||
|
<span id="metric-up" class="metric-value">0 B</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metric-sep"></div>
|
||||||
|
|
||||||
|
<div class="metric">
|
||||||
|
<div class="metric-icon ping-icon">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M1 6l5.5 5.5L12 6l5.5 5.5L23 6"/>
|
||||||
|
<path d="M1 18l5.5-5.5L12 18l5.5-5.5L23 18"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="metric-body">
|
||||||
|
<span class="metric-label">Mode</span>
|
||||||
|
<span id="metric-mode" class="metric-value">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── SETTINGS SCREEN ──────────────────────────────────────── -->
|
||||||
<div id="settings-screen" class="screen">
|
<div id="settings-screen" class="screen">
|
||||||
<header class="app-header">
|
|
||||||
|
<header class="topbar">
|
||||||
<button id="btn-back" class="icon-btn" aria-label="Back">
|
<button id="btn-back" class="icon-btn" aria-label="Back">
|
||||||
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<h2 data-i18n="settings_title">Configuration</h2>
|
<span class="topbar-title" data-i18n="settings_title">Configuration</span>
|
||||||
<div style="width: 40px;"></div>
|
<div style="width:36px"></div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="settings-content">
|
<div class="settings-body">
|
||||||
<!-- Import Area -->
|
|
||||||
<div class="import-container glass">
|
<!-- Quick import -->
|
||||||
<input type="text" id="in-import-url" data-i18n-placeholder="import_placeholder" placeholder="Paste ostp:// share link here..." />
|
<div class="import-row">
|
||||||
<button id="btn-import-url" class="small-btn" data-i18n="import_btn">Import</button>
|
<input id="in-import-url"
|
||||||
|
class="import-input"
|
||||||
|
type="text"
|
||||||
|
data-i18n-placeholder="import_placeholder"
|
||||||
|
placeholder="Paste ostp:// share link..." />
|
||||||
|
<button id="btn-import-url" class="accent-btn" data-i18n="import_btn">Import</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form Settings -->
|
<!-- Form card -->
|
||||||
<div class="editor-container glass scrollable">
|
<div class="card scrollable">
|
||||||
<div class="form-group">
|
|
||||||
<label for="in-server" data-i18n="label_server">Server Address</label>
|
<div class="field-group">
|
||||||
<input type="text" id="in-server" placeholder="host:port" />
|
<label class="field-label" for="in-server" data-i18n="label_server">Server Address</label>
|
||||||
|
<input id="in-server" class="field-input" type="text" placeholder="host:port" spellcheck="false" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="field-group">
|
||||||
<label for="in-key" data-i18n="label_key">Access Key</label>
|
<label class="field-label" for="in-key" data-i18n="label_key">Access Key</label>
|
||||||
<input type="password" id="in-key" data-i18n-placeholder="ph_key" placeholder="Enter secure access key" />
|
<div class="input-wrap">
|
||||||
|
<input id="in-key" class="field-input has-icon" type="password" data-i18n-placeholder="ph_key" placeholder="Secure access key" spellcheck="false" />
|
||||||
|
<button class="peek-btn" id="btn-peek-key" tabindex="-1" aria-label="Show key">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="field-group">
|
||||||
<label for="in-socks" data-i18n="label_socks">Local Proxy Address</label>
|
<label class="field-label" for="in-socks" data-i18n="label_socks">Local Proxy</label>
|
||||||
<input type="text" id="in-socks" placeholder="127.0.0.1:1088" />
|
<input id="in-socks" class="field-input" type="text" placeholder="127.0.0.1:1088" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="field-group">
|
||||||
<label for="in-dns" data-i18n="label_dns">Custom DNS Server</label>
|
<label class="field-label" for="in-dns" data-i18n="label_dns">DNS Server</label>
|
||||||
<input type="text" id="in-dns" placeholder="8.8.8.8" />
|
<input id="in-dns" class="field-input" type="text" placeholder="1.1.1.1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group row-align">
|
<!-- Toggles -->
|
||||||
<div class="label-stack">
|
<div class="toggle-row">
|
||||||
<span class="toggle-label" data-i18n="label_tun">TUN Tunnel Mode</span>
|
<div class="toggle-text">
|
||||||
<span class="toggle-subtext" data-i18n="tun_hint">Route all system traffic (Admin req.)</span>
|
<span class="toggle-name" data-i18n="label_tun">TUN Mode</span>
|
||||||
|
<span class="toggle-hint" data-i18n="tun_hint">Route all system traffic</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="switch">
|
<label class="toggle">
|
||||||
<input type="checkbox" id="in-tun-mode">
|
<input type="checkbox" id="in-tun-mode" />
|
||||||
<span class="slider round"></span>
|
<span class="toggle-track">
|
||||||
|
<span class="toggle-thumb"></span>
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group row-align">
|
<div class="toggle-row">
|
||||||
<div class="label-stack">
|
<div class="toggle-text">
|
||||||
<span class="toggle-label" data-i18n="label_debug">Debug Logs</span>
|
<span class="toggle-name" data-i18n="label_debug">Debug Logs</span>
|
||||||
<span class="toggle-subtext" data-i18n="debug_hint">Enable verbose internal event outputs</span>
|
<span class="toggle-hint" data-i18n="debug_hint">Verbose output</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="switch">
|
<label class="toggle">
|
||||||
<input type="checkbox" id="in-debug">
|
<input type="checkbox" id="in-debug" />
|
||||||
<span class="slider round"></span>
|
<span class="toggle-track">
|
||||||
|
<span class="toggle-thumb"></span>
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Exclusions Section Divider -->
|
<!-- Exclusions -->
|
||||||
<div class="section-divider"><span data-i18n="excl_title">Exclusions</span> <span class="divider-hint" data-i18n="excl_hint">(one per line)</span></div>
|
<div class="section-head">
|
||||||
|
<span data-i18n="excl_title">Exclusions</span>
|
||||||
<div class="form-group">
|
<span class="section-hint" data-i18n="excl_hint">one per line</span>
|
||||||
<label for="in-ex-domains" data-i18n="excl_domains">Bypass Domains</label>
|
|
||||||
<textarea id="in-ex-domains" placeholder="example.com *.google.com" rows="2"></textarea>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="field-group">
|
||||||
<label for="in-ex-ips" data-i18n="excl_ips">Bypass IPs / CIDR Ranges</label>
|
<label class="field-label" for="in-ex-domains" data-i18n="excl_domains">Bypass Domains</label>
|
||||||
<textarea id="in-ex-ips" placeholder="192.168.1.0/24 10.0.0.1" rows="2"></textarea>
|
<textarea id="in-ex-domains" class="field-input mono" placeholder="example.com *.google.com" rows="2"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="field-group">
|
||||||
<label for="in-ex-processes" data-i18n="excl_processes">Bypass Processes</label>
|
<label class="field-label" for="in-ex-ips" data-i18n="excl_ips">Bypass IPs / CIDR</label>
|
||||||
<textarea id="in-ex-processes" placeholder="chrome.exe firefox.exe" rows="2"></textarea>
|
<textarea id="in-ex-ips" class="field-input mono" placeholder="192.168.1.0/24 10.0.0.1" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group">
|
||||||
|
<label class="field-label" for="in-ex-processes" data-i18n="excl_processes">Bypass Processes</label>
|
||||||
|
<textarea id="in-ex-processes" class="field-input mono" placeholder="chrome.exe firefox.exe" rows="2"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions-container">
|
<!-- Save -->
|
||||||
<button id="btn-save-config" class="primary-btn glass" data-i18n="save_btn">Save & Apply</button>
|
<button id="btn-save-config" class="save-btn" data-i18n="save_btn">Save & Apply</button>
|
||||||
</div>
|
|
||||||
<div id="config-toast" class="toast" data-i18n="toast_saved">Configuration saved</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast -->
|
||||||
|
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<script type="module" src="main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,176 +1,208 @@
|
||||||
import { t, toggleLang, applyTranslations, getLang } from './i18n.js';
|
import { t, toggleLang, applyTranslations } from './i18n.js';
|
||||||
|
|
||||||
let invoke = () => {
|
// ── Tauri invoke shim ────────────────────────────────────────────────────────
|
||||||
console.warn('Tauri invoke is not available in this environment.');
|
let invoke = () => Promise.resolve(null);
|
||||||
return Promise.resolve(null);
|
if (window.__TAURI__?.core) {
|
||||||
};
|
|
||||||
if (window.__TAURI__ && window.__TAURI__.core) {
|
|
||||||
invoke = window.__TAURI__.core.invoke;
|
invoke = window.__TAURI__.core.invoke;
|
||||||
}
|
}
|
||||||
|
|
||||||
// State management
|
// ── State ────────────────────────────────────────────────────────────────────
|
||||||
let appState = 'disconnected';
|
let appState = 'disconnected'; // 'disconnected' | 'connecting' | 'connected'
|
||||||
let pollInterval = null;
|
let pollTimer = null;
|
||||||
let elapsedSeconds = 0;
|
let uptimeTimer = null;
|
||||||
let elapsedTimer = null;
|
let uptimeSecs = 0;
|
||||||
let rawConfigObj = null;
|
let rawConfig = null; // parsed config.json object
|
||||||
|
let serverAddr = ''; // current server address (for badge)
|
||||||
|
|
||||||
// DOM Elements
|
// ── DOM refs ─────────────────────────────────────────────────────────────────
|
||||||
const btnConnect = document.getElementById('btn-connect');
|
const $ = id => document.getElementById(id);
|
||||||
const powerContainer = document.querySelector('.power-button-container');
|
|
||||||
const statusText = document.getElementById('status-text');
|
|
||||||
const uptimeText = document.getElementById('uptime-text');
|
|
||||||
const metricDown = document.getElementById('metric-down');
|
|
||||||
const metricUp = document.getElementById('metric-up');
|
|
||||||
|
|
||||||
const homeScreen = document.getElementById('home-screen');
|
const homeScreen = $('home-screen');
|
||||||
const settingsScreen = document.getElementById('settings-screen');
|
const settingsScreen = $('settings-screen');
|
||||||
const btnGoSettings = document.getElementById('btn-go-settings');
|
const btnConnect = $('btn-connect');
|
||||||
const btnBack = document.getElementById('btn-back');
|
const orbitWrap = $('orbit-wrap');
|
||||||
const btnSaveConfig = document.getElementById('btn-save-config');
|
const brandDot = $('brand-dot');
|
||||||
const configToast = document.getElementById('config-toast');
|
const statusLabel = $('status-text');
|
||||||
const btnLang = document.getElementById('btn-lang');
|
const statusSub = $('uptime-text');
|
||||||
|
const serverBadge = $('server-badge');
|
||||||
|
const serverBadgeTxt = $('server-badge-text');
|
||||||
|
const metricDown = $('metric-down');
|
||||||
|
const metricUp = $('metric-up');
|
||||||
|
const metricMode = $('metric-mode');
|
||||||
|
const toast = $('toast');
|
||||||
|
|
||||||
// Input Form Elements
|
const btnGoSettings = $('btn-go-settings');
|
||||||
const inImportUrl = document.getElementById('in-import-url');
|
const btnBack = $('btn-back');
|
||||||
const btnImportUrl = document.getElementById('btn-import-url');
|
const btnLang = $('btn-lang');
|
||||||
const inServer = document.getElementById('in-server');
|
const btnImport = $('btn-import-url');
|
||||||
const inKey = document.getElementById('in-key');
|
const btnPeekKey = $('btn-peek-key');
|
||||||
const inSocks = document.getElementById('in-socks');
|
const btnSave = $('btn-save-config');
|
||||||
const inDns = document.getElementById('in-dns');
|
const importInput = $('in-import-url');
|
||||||
const inTunMode = document.getElementById('in-tun-mode');
|
const inServer = $('in-server');
|
||||||
const inDebug = document.getElementById('in-debug');
|
const inKey = $('in-key');
|
||||||
|
const inSocks = $('in-socks');
|
||||||
|
const inDns = $('in-dns');
|
||||||
|
const inTun = $('in-tun-mode');
|
||||||
|
const inDebug = $('in-debug');
|
||||||
|
const inDomains = $('in-ex-domains');
|
||||||
|
const inIps = $('in-ex-ips');
|
||||||
|
const inProcesses = $('in-ex-processes');
|
||||||
|
|
||||||
// Exclusions Textareas
|
// ── Utilities ────────────────────────────────────────────────────────────────
|
||||||
const inExDomains = document.getElementById('in-ex-domains');
|
function fmtBytes(b) {
|
||||||
const inExIps = document.getElementById('in-ex-ips');
|
if (!b || b === 0) return '0 B';
|
||||||
const inExProcesses = document.getElementById('in-ex-processes');
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const i = Math.min(Math.floor(Math.log2(b) / 10), 4);
|
||||||
// Utils
|
return (b / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1) + ' ' + units[i];
|
||||||
function formatBytes(bytes) {
|
|
||||||
if (bytes === 0) return '0.0 B';
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(seconds) {
|
function fmtTime(s) {
|
||||||
const hrs = Math.floor(seconds / 3600);
|
const h = Math.floor(s / 3600);
|
||||||
const mins = Math.floor((seconds % 3600) / 60);
|
const m = Math.floor((s % 3600) / 60);
|
||||||
const secs = seconds % 60;
|
const sec = s % 60;
|
||||||
return [
|
const pad = n => String(n).padStart(2, '0');
|
||||||
hrs > 0 ? String(hrs).padStart(2, '0') : null,
|
return h > 0
|
||||||
String(mins).padStart(2, '0'),
|
? `${h}:${pad(m)}:${pad(sec)}`
|
||||||
String(secs).padStart(2, '0')
|
: `${pad(m)}:${pad(sec)}`;
|
||||||
].filter(x => x !== null).join(':');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// State Updates
|
function splitLines(val) {
|
||||||
function setUIState(state) {
|
return val.split('\n').map(l => l.trim()).filter(Boolean);
|
||||||
if (appState === state) return;
|
}
|
||||||
appState = state;
|
|
||||||
|
|
||||||
|
// ── Toast ────────────────────────────────────────────────────────────────────
|
||||||
|
let toastTimer = null;
|
||||||
|
function showToast(msg, variant = '') {
|
||||||
|
toast.textContent = msg;
|
||||||
|
toast.className = 'toast show' + (variant ? ' is-' + variant : '');
|
||||||
|
clearTimeout(toastTimer);
|
||||||
|
toastTimer = setTimeout(() => {
|
||||||
|
toast.classList.remove('show');
|
||||||
|
}, 2400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── State machine ────────────────────────────────────────────────────────────
|
||||||
|
function setState(next) {
|
||||||
|
if (appState === next) return;
|
||||||
|
appState = next;
|
||||||
|
|
||||||
|
// Reset all dynamic classes
|
||||||
btnConnect.className = 'power-btn';
|
btnConnect.className = 'power-btn';
|
||||||
powerContainer.className = 'power-button-container';
|
orbitWrap.className = 'orbit-wrap';
|
||||||
statusText.className = '';
|
brandDot.className = 'brand-dot';
|
||||||
|
statusLabel.className = 'status-label';
|
||||||
|
|
||||||
if (state === 'disconnected') {
|
if (next === 'disconnected') {
|
||||||
statusText.textContent = t('status_disconnected');
|
statusLabel.textContent = t('status_disconnected');
|
||||||
statusText.classList.add('status-disconnected');
|
statusSub.textContent = t('hint_tap');
|
||||||
uptimeText.textContent = t('hint_tap');
|
statusLabel.classList.add('');
|
||||||
|
serverBadge.classList.add('hidden');
|
||||||
|
metricDown.textContent = '0 B';
|
||||||
|
metricUp.textContent = '0 B';
|
||||||
|
metricMode.textContent = '—';
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
clearInterval(uptimeTimer);
|
||||||
|
pollTimer = uptimeTimer = null;
|
||||||
|
uptimeSecs = 0;
|
||||||
|
|
||||||
clearInterval(pollInterval);
|
} else if (next === 'connecting') {
|
||||||
clearInterval(elapsedTimer);
|
|
||||||
pollInterval = null;
|
|
||||||
elapsedTimer = null;
|
|
||||||
elapsedSeconds = 0;
|
|
||||||
|
|
||||||
} else if (state === 'connecting') {
|
|
||||||
btnConnect.classList.add('connecting');
|
btnConnect.classList.add('connecting');
|
||||||
powerContainer.classList.add('connecting');
|
orbitWrap.classList.add('connecting');
|
||||||
statusText.textContent = t('status_connecting');
|
brandDot.classList.add('connecting');
|
||||||
statusText.classList.add('status-connecting');
|
statusLabel.classList.add('is-connecting');
|
||||||
uptimeText.textContent = t('hint_connecting');
|
statusLabel.textContent = t('status_connecting');
|
||||||
|
statusSub.textContent = t('hint_connecting');
|
||||||
|
serverBadge.classList.add('hidden');
|
||||||
|
clearInterval(uptimeTimer);
|
||||||
|
uptimeSecs = 0;
|
||||||
|
|
||||||
clearInterval(elapsedTimer);
|
} else if (next === 'connected') {
|
||||||
elapsedTimer = null;
|
|
||||||
elapsedSeconds = 0;
|
|
||||||
|
|
||||||
} else if (state === 'connected') {
|
|
||||||
btnConnect.classList.add('connected');
|
btnConnect.classList.add('connected');
|
||||||
powerContainer.classList.add('connected');
|
orbitWrap.classList.add('connected');
|
||||||
statusText.textContent = t('status_connected');
|
brandDot.classList.add('connected');
|
||||||
statusText.classList.add('status-connected');
|
statusLabel.classList.add('is-connected');
|
||||||
|
statusLabel.textContent = t('status_connected');
|
||||||
|
|
||||||
if (!elapsedTimer) {
|
// Show server badge
|
||||||
elapsedSeconds = 0;
|
if (serverAddr) {
|
||||||
elapsedTimer = setInterval(() => {
|
serverBadgeTxt.textContent = serverAddr;
|
||||||
elapsedSeconds++;
|
serverBadge.classList.remove('hidden');
|
||||||
uptimeText.textContent = `${t('hint_connected')} | ${formatTime(elapsedSeconds)}`;
|
}
|
||||||
|
|
||||||
|
// Start uptime counter
|
||||||
|
if (!uptimeTimer) {
|
||||||
|
uptimeSecs = 0;
|
||||||
|
uptimeTimer = setInterval(() => {
|
||||||
|
uptimeSecs++;
|
||||||
|
statusSub.textContent = fmtTime(uptimeSecs);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI Event Handlers
|
// ── Polling ──────────────────────────────────────────────────────────────────
|
||||||
async function handleToggleConnect() {
|
async function poll() {
|
||||||
|
try {
|
||||||
|
const code = await invoke('get_tunnel_status');
|
||||||
|
if (code === 0) { setState('disconnected'); return; }
|
||||||
|
else if (code === 1) setState('connecting');
|
||||||
|
else if (code === 2) setState('connected');
|
||||||
|
|
||||||
|
const metrics = await invoke('get_metrics');
|
||||||
|
if (metrics) {
|
||||||
|
metricDown.textContent = fmtBytes(metrics.bytes_recv);
|
||||||
|
metricUp.textContent = fmtBytes(metrics.bytes_sent);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setState('disconnected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPolling() {
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
poll();
|
||||||
|
pollTimer = setInterval(poll, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Connect / Disconnect ─────────────────────────────────────────────────────
|
||||||
|
async function handleToggle() {
|
||||||
if (appState === 'disconnected') {
|
if (appState === 'disconnected') {
|
||||||
setUIState('connecting');
|
// Read server address for badge before connecting
|
||||||
try {
|
try {
|
||||||
const success = await invoke('start_tunnel');
|
const raw = await invoke('get_config');
|
||||||
if (success) {
|
const cfg = JSON.parse(raw);
|
||||||
startGlobalPolling();
|
serverAddr = cfg.server || '';
|
||||||
|
|
||||||
|
// Determine mode label
|
||||||
|
const isTun = cfg.tun?.enable;
|
||||||
|
metricMode.textContent = isTun ? 'TUN' : 'SOCKS5';
|
||||||
|
} catch { serverAddr = ''; }
|
||||||
|
|
||||||
|
setState('connecting');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ok = await invoke('start_tunnel');
|
||||||
|
if (ok) {
|
||||||
|
startPolling();
|
||||||
} else {
|
} else {
|
||||||
setUIState('disconnected');
|
setState('disconnected');
|
||||||
|
showToast(t('toast_error') || 'Failed to connect', 'error');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Tunnel start error:', err);
|
setState('disconnected');
|
||||||
setUIState('disconnected');
|
showToast(String(err), 'error');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try { await invoke('stop_tunnel'); } catch { /* ignore */ }
|
||||||
await invoke('stop_tunnel');
|
setState('disconnected');
|
||||||
} catch (err) {
|
showToast(t('toast_disconnected') || 'Disconnected');
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
setUIState('disconnected');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startGlobalPolling() {
|
// ── Screen navigation ────────────────────────────────────────────────────────
|
||||||
if (pollInterval) clearInterval(pollInterval);
|
function showScreen(name) {
|
||||||
pollInterval = setInterval(uiSyncTick, 1000);
|
if (name === 'settings') {
|
||||||
uiSyncTick();
|
loadConfigIntoForm();
|
||||||
}
|
|
||||||
|
|
||||||
async function uiSyncTick() {
|
|
||||||
try {
|
|
||||||
const statusCode = await invoke('get_tunnel_status');
|
|
||||||
|
|
||||||
if (statusCode === 0) {
|
|
||||||
setUIState('disconnected');
|
|
||||||
return;
|
|
||||||
} else if (statusCode === 1) {
|
|
||||||
setUIState('connecting');
|
|
||||||
} else if (statusCode === 2) {
|
|
||||||
setUIState('connected');
|
|
||||||
}
|
|
||||||
|
|
||||||
const stats = await invoke('get_metrics');
|
|
||||||
if (stats) {
|
|
||||||
metricDown.textContent = formatBytes(stats.bytes_recv);
|
|
||||||
metricUp.textContent = formatBytes(stats.bytes_sent);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Sync error', e);
|
|
||||||
setUIState('disconnected');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchScreen(target) {
|
|
||||||
if (target === 'settings') {
|
|
||||||
loadConfigIntoFields();
|
|
||||||
homeScreen.classList.remove('active');
|
homeScreen.classList.remove('active');
|
||||||
settingsScreen.classList.add('active');
|
settingsScreen.classList.add('active');
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -179,161 +211,127 @@ function switchScreen(target) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config Management
|
// ── Config — load ─────────────────────────────────────────────────────────────
|
||||||
async function loadConfigIntoFields() {
|
async function loadConfigIntoForm() {
|
||||||
try {
|
try {
|
||||||
const rawStr = await invoke('get_config');
|
const raw = await invoke('get_config');
|
||||||
rawConfigObj = JSON.parse(rawStr);
|
rawConfig = JSON.parse(raw);
|
||||||
|
const c = rawConfig.mode === 'client' ? rawConfig : null;
|
||||||
|
if (!c) return;
|
||||||
|
|
||||||
const isClient = rawConfigObj.mode === 'client';
|
inServer.value = c.server || '';
|
||||||
const clientConf = isClient ? rawConfigObj : null;
|
inKey.value = c.access_key || '';
|
||||||
|
inSocks.value = c.socks5_bind || '127.0.0.1:1088';
|
||||||
|
inTun.checked = !!c.tun?.enable;
|
||||||
|
inDns.value = c.tun?.dns || '';
|
||||||
|
inDebug.checked = !!c.debug;
|
||||||
|
|
||||||
if (clientConf) {
|
const ex = c.exclude || {};
|
||||||
inServer.value = clientConf.server || '';
|
inDomains.value = (ex.domains || []).join('\n');
|
||||||
inKey.value = clientConf.access_key || '';
|
inIps.value = (ex.ips || []).join('\n');
|
||||||
inSocks.value = clientConf.socks5_bind || '127.0.0.1:1088';
|
inProcesses.value = (ex.processes || []).join('\n');
|
||||||
|
|
||||||
const tunEnabled = clientConf.tun && clientConf.tun.enable;
|
|
||||||
inTunMode.checked = !!tunEnabled;
|
|
||||||
|
|
||||||
inDns.value = (clientConf.tun && clientConf.tun.dns) || '';
|
|
||||||
inDebug.checked = !!clientConf.debug;
|
|
||||||
|
|
||||||
// Load exclusions (arrays to multiline string)
|
|
||||||
const exc = clientConf.exclude || {};
|
|
||||||
inExDomains.value = (exc.domains || []).join('\n');
|
|
||||||
inExIps.value = (exc.ips || []).join('\n');
|
|
||||||
inExProcesses.value = (exc.processes || []).join('\n');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading config', err);
|
showToast(String(err), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTextAreaToArray(val) {
|
// ── Config — save ─────────────────────────────────────────────────────────────
|
||||||
return val.split('\n')
|
async function handleSave() {
|
||||||
.map(line => line.trim())
|
if (!rawConfig) rawConfig = { mode: 'client', log_level: 'info' };
|
||||||
.filter(line => line.length > 0);
|
|
||||||
|
const server = inServer.value.trim();
|
||||||
|
const key = inKey.value.trim();
|
||||||
|
|
||||||
|
if (!server) { showToast(t('err_server_req') || 'Server address required', 'error'); return; }
|
||||||
|
if (!key) { showToast(t('err_key_req') || 'Access key required', 'error'); return; }
|
||||||
|
|
||||||
|
rawConfig.mode = 'client';
|
||||||
|
rawConfig.server = server;
|
||||||
|
rawConfig.access_key = key;
|
||||||
|
rawConfig.socks5_bind = inSocks.value.trim() || null;
|
||||||
|
rawConfig.debug = inDebug.checked;
|
||||||
|
|
||||||
|
if (!rawConfig.tun) {
|
||||||
|
rawConfig.tun = { wintun_path: './wintun.dll', ipv4_address: '10.1.0.2/24' };
|
||||||
}
|
}
|
||||||
|
rawConfig.tun.enable = inTun.checked;
|
||||||
|
rawConfig.tun.dns = inDns.value.trim() || null;
|
||||||
|
|
||||||
async function handleSaveConfig() {
|
rawConfig.exclude = {
|
||||||
if (!rawConfigObj) rawConfigObj = { mode: 'client', log_level: 'info' };
|
domains: splitLines(inDomains.value),
|
||||||
|
ips: splitLines(inIps.value),
|
||||||
rawConfigObj.mode = 'client';
|
processes: splitLines(inProcesses.value),
|
||||||
rawConfigObj.server = inServer.value.trim();
|
|
||||||
rawConfigObj.access_key = inKey.value.trim();
|
|
||||||
rawConfigObj.socks5_bind = inSocks.value.trim() || null;
|
|
||||||
|
|
||||||
if (!rawConfigObj.tun) {
|
|
||||||
rawConfigObj.tun = {
|
|
||||||
wintun_path: "./wintun.dll",
|
|
||||||
ipv4_address: "10.1.0.2/24"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
rawConfigObj.tun.enable = inTunMode.checked;
|
|
||||||
|
|
||||||
const dnsVal = inDns.value.trim();
|
|
||||||
rawConfigObj.tun.dns = dnsVal ? dnsVal : null;
|
|
||||||
|
|
||||||
rawConfigObj.debug = inDebug.checked;
|
|
||||||
|
|
||||||
// Save Exclusions
|
|
||||||
rawConfigObj.exclude = {
|
|
||||||
domains: parseTextAreaToArray(inExDomains.value),
|
|
||||||
ips: parseTextAreaToArray(inExIps.value),
|
|
||||||
processes: parseTextAreaToArray(inExProcesses.value)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validation
|
|
||||||
if (!rawConfigObj.server) {
|
|
||||||
showToast(t('err_server_req') || 'Server address is required');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!rawConfigObj.access_key) {
|
|
||||||
showToast(t('err_key_req') || 'Access key is required');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const finalJson = JSON.stringify(rawConfigObj, null, 2);
|
const ok = await invoke('save_config', { jsonContent: JSON.stringify(rawConfig, null, 2) });
|
||||||
const success = await invoke('save_config', { jsonContent: finalJson });
|
if (ok) {
|
||||||
if (success) {
|
showToast(t('toast_saved'), 'ok');
|
||||||
showToast(t('toast_saved'));
|
setTimeout(() => showScreen('home'), 700);
|
||||||
setTimeout(() => switchScreen('home'), 800);
|
} else {
|
||||||
|
showToast(t('toast_error'), 'error');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(t('toast_error') + ': ' + err);
|
showToast(String(err), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// OSTP URI Sharing Parser
|
// ── Import share link ─────────────────────────────────────────────────────────
|
||||||
function handleImportUrl() {
|
function handleImport() {
|
||||||
const urlStr = inImportUrl.value.trim();
|
const raw = importInput.value.trim();
|
||||||
if (!urlStr) return;
|
if (!raw) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!urlStr.startsWith('ostp://')) {
|
if (!raw.startsWith('ostp://')) throw new Error('Link must start with ostp://');
|
||||||
throw new Error('Link must start with ostp://');
|
const url = new URL(raw);
|
||||||
}
|
const key = decodeURIComponent(url.username);
|
||||||
const url = new URL(urlStr);
|
const host = url.host;
|
||||||
|
if (!key || !host) throw new Error('Incomplete link parameters');
|
||||||
const accessKey = decodeURIComponent(url.username);
|
inServer.value = host;
|
||||||
const serverHost = url.host;
|
inKey.value = key;
|
||||||
|
importInput.value = '';
|
||||||
if (!accessKey || !serverHost) {
|
showToast(t('toast_imported'), 'ok');
|
||||||
throw new Error('Incomplete parameters');
|
|
||||||
}
|
|
||||||
|
|
||||||
inServer.value = serverHost;
|
|
||||||
inKey.value = accessKey;
|
|
||||||
inImportUrl.value = '';
|
|
||||||
showToast(t('toast_imported'));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(t('toast_error') + ': ' + err.message);
|
showToast(err.message, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showToast(message) {
|
// ── Peek key ──────────────────────────────────────────────────────────────────
|
||||||
configToast.textContent = message || t('toast_saved');
|
let peeking = false;
|
||||||
configToast.classList.add('show');
|
function togglePeek() {
|
||||||
setTimeout(() => configToast.classList.remove('show'), 2000);
|
peeking = !peeking;
|
||||||
|
inKey.type = peeking ? 'text' : 'password';
|
||||||
|
btnPeekKey.style.color = peeking
|
||||||
|
? 'var(--c-accent)'
|
||||||
|
: 'var(--c-txt-3)';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialization
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||||
window.addEventListener('DOMContentLoaded', async () => {
|
window.addEventListener('DOMContentLoaded', async () => {
|
||||||
// Apply translations on load
|
|
||||||
applyTranslations();
|
applyTranslations();
|
||||||
|
setState('disconnected');
|
||||||
|
|
||||||
// Re-apply dynamic status text
|
// Event wiring
|
||||||
setUIState(appState);
|
btnConnect.addEventListener('click', handleToggle);
|
||||||
|
btnGoSettings.addEventListener('click', () => showScreen('settings'));
|
||||||
|
btnBack.addEventListener('click', () => showScreen('home'));
|
||||||
|
btnSave.addEventListener('click', handleSave);
|
||||||
|
btnImport.addEventListener('click', handleImport);
|
||||||
|
btnPeekKey.addEventListener('click', togglePeek);
|
||||||
|
importInput.addEventListener('keydown', e => { if (e.key === 'Enter') handleImport(); });
|
||||||
|
|
||||||
btnConnect.addEventListener('click', handleToggleConnect);
|
|
||||||
btnGoSettings.addEventListener('click', () => switchScreen('settings'));
|
|
||||||
btnBack.addEventListener('click', () => switchScreen('home'));
|
|
||||||
btnSaveConfig.addEventListener('click', handleSaveConfig);
|
|
||||||
|
|
||||||
btnImportUrl.addEventListener('click', handleImportUrl);
|
|
||||||
inImportUrl.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Enter') handleImportUrl();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Language toggle
|
|
||||||
btnLang.addEventListener('click', () => {
|
btnLang.addEventListener('click', () => {
|
||||||
toggleLang();
|
toggleLang();
|
||||||
// Re-apply dynamic elements
|
// Refresh dynamic text without losing state
|
||||||
const currentState = appState;
|
const cur = appState;
|
||||||
appState = ''; // Force refresh
|
appState = '';
|
||||||
setUIState(currentState);
|
setState(cur);
|
||||||
|
document.getElementById('lang-label').textContent =
|
||||||
|
localStorage.getItem('ostp_lang') === 'ru' ? 'RU' : 'EN';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Restore status on app open
|
||||||
try {
|
try {
|
||||||
const statusCode = await invoke('get_tunnel_status');
|
const code = await invoke('get_tunnel_status');
|
||||||
if (statusCode > 0) {
|
if (code > 0) startPolling();
|
||||||
startGlobalPolling();
|
} catch { /* not in Tauri context */ }
|
||||||
} else {
|
|
||||||
setUIState('disconnected');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setUIState('disconnected');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1 +1 @@
|
||||||
Subproject commit cd3d7d8dae142112d58bc216777b77a2ded83b4e
|
Subproject commit 64efa677aca2551b61f71f2168611d884619e5ca
|
||||||
Loading…
Reference in New Issue