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:
ospab 2026-05-17 22:13:03 +03:00
parent 3a16373a31
commit ee14a60348
5 changed files with 1071 additions and 911 deletions

View File

@ -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:

View File

@ -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&#10;*.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;10.0.0.1" rows="2"></textarea> <textarea id="in-ex-domains" class="field-input mono" placeholder="example.com&#10;*.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&#10;firefox.exe" rows="2"></textarea> <textarea id="in-ex-ips" class="field-input mono" placeholder="192.168.1.0/24&#10;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&#10;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 &amp; 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>

View File

@ -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);
}
async function handleSaveConfig() { const server = inServer.value.trim();
if (!rawConfigObj) rawConfigObj = { mode: 'client', log_level: 'info' }; const key = inKey.value.trim();
rawConfigObj.mode = 'client'; if (!server) { showToast(t('err_server_req') || 'Server address required', 'error'); return; }
rawConfigObj.server = inServer.value.trim(); if (!key) { showToast(t('err_key_req') || 'Access key required', 'error'); return; }
rawConfigObj.access_key = inKey.value.trim();
rawConfigObj.socks5_bind = inSocks.value.trim() || null;
if (!rawConfigObj.tun) { rawConfig.mode = 'client';
rawConfigObj.tun = { rawConfig.server = server;
wintun_path: "./wintun.dll", rawConfig.access_key = key;
ipv4_address: "10.1.0.2/24" 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' };
} }
rawConfigObj.tun.enable = inTunMode.checked; rawConfig.tun.enable = inTun.checked;
rawConfig.tun.dns = inDns.value.trim() || null;
const dnsVal = inDns.value.trim(); rawConfig.exclude = {
rawConfigObj.tun.dns = dnsVal ? dnsVal : null; domains: splitLines(inDomains.value),
ips: splitLines(inIps.value),
rawConfigObj.debug = inDebug.checked; processes: splitLines(inProcesses.value),
// 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