Merge 7e24655385
into 4e5f595951
1
.github/config/dependency-review-config.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
allow-ghsas: GHSA-wrw7-89jp-8q8g
|
182
.github/workflows/README-tauri.md
vendored
Normal file
@ -0,0 +1,182 @@
|
||||
# Tauri Build Workflows
|
||||
|
||||
This directory contains GitHub Actions workflows for building Tauri desktop applications for Stirling-PDF.
|
||||
|
||||
## Workflow
|
||||
|
||||
### `tauri-build.yml` - Unified Build Workflow
|
||||
|
||||
**Purpose**: Build Tauri applications for all platforms (Windows, macOS, Linux) with comprehensive testing and validation.
|
||||
|
||||
**Triggers**:
|
||||
- Manual dispatch with platform selection (windows, macos, linux, or all)
|
||||
- Pull requests affecting Tauri-related files
|
||||
- Pushes to main branch affecting Tauri-related files
|
||||
|
||||
**Platforms**:
|
||||
- **Windows**: x86_64 (exe and msi)
|
||||
- **macOS**: Apple Silicon (aarch64) and Intel (x86_64) (dmg)
|
||||
- **Linux**: x86_64 (deb and AppImage)
|
||||
|
||||
**Features**:
|
||||
- **Dynamic Platform Selection**: Choose specific platforms or build all
|
||||
- **Smart JRE Bundling**: Uses JLink to create optimized custom JRE
|
||||
- **Apple Code Signing**: Full macOS notarization and signing support
|
||||
- **Comprehensive Validation**: Artifact verification and size checks
|
||||
- **Self-Contained**: No Java installation required for end users
|
||||
- **Cross-Platform**: Builds on actual target platforms for compatibility
|
||||
- **Detailed Logging**: Complete build process visibility
|
||||
|
||||
## Usage
|
||||
|
||||
### Manual Testing
|
||||
|
||||
1. **Test All Platforms**:
|
||||
```bash
|
||||
# Go to Actions tab in GitHub
|
||||
# Run "Build Tauri Applications" workflow
|
||||
# Select "all" for platform
|
||||
```
|
||||
|
||||
2. **Test Specific Platform**:
|
||||
```bash
|
||||
# Go to Actions tab in GitHub
|
||||
# Run "Build Tauri Applications" workflow
|
||||
# Select specific platform (windows/macos/linux)
|
||||
```
|
||||
|
||||
3. **Automatic Testing**:
|
||||
- Builds are automatically triggered on PRs and pushes
|
||||
- All platforms are tested by default
|
||||
- Artifacts are uploaded for download and testing
|
||||
|
||||
## Configuration
|
||||
|
||||
### Required Secrets
|
||||
|
||||
#### For macOS Code Signing (Required for distribution)
|
||||
|
||||
Configure these secrets in your repository for macOS app signing:
|
||||
|
||||
- `APPLE_CERTIFICATE`: Base64-encoded .p12 certificate file
|
||||
- `APPLE_CERTIFICATE_PASSWORD`: Password for the .p12 certificate
|
||||
- `APPLE_SIGNING_IDENTITY`: Certificate name (e.g., "Developer ID Application: Your Name")
|
||||
- `APPLE_ID`: Your Apple ID email
|
||||
- `APPLE_PASSWORD`: App-specific password for your Apple ID
|
||||
- `APPLE_TEAM_ID`: Your Apple Developer Team ID
|
||||
|
||||
#### Setting Up Apple Code Signing
|
||||
|
||||
1. **Get a Developer ID Certificate**:
|
||||
- Join the Apple Developer Program ($99/year)
|
||||
- Create a "Developer ID Application" certificate in Apple Developer portal
|
||||
- Download the certificate as a .p12 file
|
||||
|
||||
2. **Convert Certificate to Base64**:
|
||||
```bash
|
||||
base64 -i certificate.p12 | pbcopy
|
||||
```
|
||||
|
||||
3. **Create App-Specific Password**:
|
||||
- Go to appleid.apple.com → Sign-In and Security → App-Specific Passwords
|
||||
- Generate a new password for "Tauri CI"
|
||||
|
||||
4. **Find Your Team ID**:
|
||||
- Apple Developer portal → Membership → Team ID
|
||||
|
||||
5. **Add to GitHub Secrets**:
|
||||
- Repository → Settings → Secrets and variables → Actions
|
||||
- Add each secret with the exact names listed above
|
||||
|
||||
#### For General Tauri Signing (Optional)
|
||||
|
||||
- `TAURI_SIGNING_PRIVATE_KEY`: Private key for signing Tauri applications
|
||||
- `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`: Password for the signing private key
|
||||
|
||||
### File Structure
|
||||
|
||||
The workflows expect this structure:
|
||||
```
|
||||
├── frontend/
|
||||
│ ├── src-tauri/
|
||||
│ │ ├── Cargo.toml
|
||||
│ │ ├── tauri.conf.json
|
||||
│ │ └── src/
|
||||
│ ├── package.json
|
||||
│ └── src/
|
||||
├── gradlew
|
||||
└── stirling-pdf/
|
||||
└── build/libs/
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
Both workflows include comprehensive validation:
|
||||
|
||||
1. **Build Validation**: Ensures all expected artifacts are created
|
||||
2. **Size Validation**: Checks artifacts aren't suspiciously small
|
||||
3. **Platform Validation**: Verifies platform-specific requirements
|
||||
4. **Integration Testing**: Tests that Java backend builds correctly
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Missing Dependencies**:
|
||||
- Ubuntu: Ensure system dependencies are installed
|
||||
- macOS: Check Rust toolchain targets
|
||||
- Windows: Verify MSVC tools are available
|
||||
|
||||
2. **Java Backend Build Fails**:
|
||||
- Check Gradle permissions (`chmod +x ./gradlew`)
|
||||
- Verify JDK 21 is properly configured
|
||||
|
||||
3. **Artifact Size Issues**:
|
||||
- Small artifacts usually indicate build failures
|
||||
- Check that backend JAR is properly copied to Tauri resources
|
||||
|
||||
4. **Signing Issues**:
|
||||
- Ensure signing secrets are configured if needed
|
||||
- Check that signing keys are valid
|
||||
|
||||
### Debugging
|
||||
|
||||
1. **Check Logs**: Each step provides detailed logging
|
||||
2. **Artifact Inspection**: Download artifacts to verify contents
|
||||
3. **Local Testing**: Test builds locally before running workflows
|
||||
|
||||
## JLink Integration Benefits
|
||||
|
||||
The workflows now use JLink to create custom Java runtimes:
|
||||
|
||||
### **Self-Contained Applications**
|
||||
- **No Java Required**: Users don't need Java installed
|
||||
- **Consistent Runtime**: Same Java version across all deployments
|
||||
- **Smaller Size**: Only includes needed Java modules (~30-50MB vs full JRE)
|
||||
|
||||
### **Security & Performance**
|
||||
- **Minimal Attack Surface**: Only required modules included
|
||||
- **Faster Startup**: Optimized runtime with stripped debug info
|
||||
- **Better Compression**: JLink level 2 compression reduces size
|
||||
|
||||
### **Module Analysis**
|
||||
- **Automatic Detection**: Uses `jdeps` to analyze JAR dependencies
|
||||
- **Fallback Safety**: Predefined module list if analysis fails
|
||||
- **Platform Optimized**: Different modules per platform if needed
|
||||
|
||||
## Integration with Existing Workflows
|
||||
|
||||
These workflows are designed to complement the existing build system:
|
||||
|
||||
- Uses same JDK and Gradle setup as `build.yml`
|
||||
- Follows same security practices as `multiOSReleases.yml`
|
||||
- Compatible with existing release processes
|
||||
- Integrates JLink logic from `build-tauri-jlink.sh/bat` scripts
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Test the workflows on your branch
|
||||
2. Verify all platforms build successfully
|
||||
3. Check artifact quality and sizes
|
||||
4. Configure signing if needed
|
||||
5. Merge when all tests pass
|
2
.github/workflows/dependency-review.yml
vendored
@ -25,3 +25,5 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
|
||||
with:
|
||||
config-file: './.github/config/dependency-review-config.yml'
|
||||
|
329
.github/workflows/tauri-build.yml
vendored
Normal file
@ -0,0 +1,329 @@
|
||||
name: Build Tauri Applications
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
description: "Platform to build (windows, macos, linux, or all)"
|
||||
required: true
|
||||
default: "all"
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- windows
|
||||
- macos
|
||||
- linux
|
||||
pull_request:
|
||||
branches: [main, V2]
|
||||
paths:
|
||||
- 'frontend/src-tauri/**'
|
||||
- 'frontend/src/**'
|
||||
- 'frontend/package.json'
|
||||
- 'frontend/package-lock.json'
|
||||
- '.github/workflows/tauri-build.yml'
|
||||
push:
|
||||
branches: [main, V2]
|
||||
paths:
|
||||
- 'frontend/src-tauri/**'
|
||||
- 'frontend/src/**'
|
||||
- 'frontend/package.json'
|
||||
- 'frontend/package-lock.json'
|
||||
- '.github/workflows/tauri-build.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
determine-matrix:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Determine build matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
case "${{ github.event.inputs.platform }}" in
|
||||
"windows")
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"macos")
|
||||
echo 'matrix={"include":[{"platform":"macos-latest","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-13","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"linux")
|
||||
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
*)
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-latest","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-13","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# For PR/push events, build all platforms
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-latest","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-13","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build:
|
||||
needs: determine-matrix
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.determine-matrix.outputs.matrix) }}
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ (matrix.platform == 'macos-latest' || matrix.platform == 'macos-13') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Build Java backend with JLink
|
||||
working-directory: ./
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x ./gradlew
|
||||
echo "🔧 Building Stirling-PDF JAR..."
|
||||
# STIRLING_PDF_DESKTOP_UI=false ./gradlew clean bootJar --no-daemon
|
||||
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
|
||||
# Find the built JAR
|
||||
STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1)
|
||||
echo "✅ Built JAR: $STIRLING_JAR"
|
||||
|
||||
# Create Tauri directories
|
||||
mkdir -p ./frontend/src-tauri/libs
|
||||
mkdir -p ./frontend/src-tauri/runtime
|
||||
|
||||
# Copy JAR to Tauri libs
|
||||
cp "$STIRLING_JAR" ./frontend/src-tauri/libs/
|
||||
echo "✅ JAR copied to Tauri libs"
|
||||
|
||||
# Analyze JAR dependencies for jlink modules
|
||||
echo "🔍 Analyzing JAR dependencies..."
|
||||
if command -v jdeps &> /dev/null; then
|
||||
DETECTED_MODULES=$(jdeps --print-module-deps --ignore-missing-deps "$STIRLING_JAR" 2>/dev/null || echo "")
|
||||
if [ -n "$DETECTED_MODULES" ]; then
|
||||
echo "📋 jdeps detected modules: $DETECTED_MODULES"
|
||||
MODULES="$DETECTED_MODULES,java.compiler,java.instrument,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
||||
else
|
||||
echo "⚠️ jdeps analysis failed, using predefined modules"
|
||||
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ jdeps not available, using predefined modules"
|
||||
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
||||
fi
|
||||
|
||||
# Create custom JRE with jlink (always rebuild)
|
||||
echo "🔧 Creating custom JRE with jlink..."
|
||||
echo "📋 Using modules: $MODULES"
|
||||
|
||||
# Remove any existing JRE
|
||||
rm -rf ./frontend/src-tauri/runtime/jre
|
||||
|
||||
# Create the custom JRE
|
||||
jlink \
|
||||
--add-modules "$MODULES" \
|
||||
--strip-debug \
|
||||
--compress=2 \
|
||||
--no-header-files \
|
||||
--no-man-pages \
|
||||
--output ./frontend/src-tauri/runtime/jre
|
||||
|
||||
if [ ! -d "./frontend/src-tauri/runtime/jre" ]; then
|
||||
echo "❌ Failed to create JLink runtime"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test the bundled runtime
|
||||
if [ -f "./frontend/src-tauri/runtime/jre/bin/java" ]; then
|
||||
RUNTIME_VERSION=$(./frontend/src-tauri/runtime/jre/bin/java --version 2>&1 | head -n 1)
|
||||
echo "✅ Custom JRE created successfully: $RUNTIME_VERSION"
|
||||
else
|
||||
echo "❌ Custom JRE executable not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Calculate runtime size
|
||||
RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1)
|
||||
echo "📊 Custom JRE size: $RUNTIME_SIZE"
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: ./frontend
|
||||
run: npm install
|
||||
|
||||
- name: Import Apple Developer Certificate
|
||||
if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-13'
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
echo "Importing Apple Developer Certificate..."
|
||||
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security set-keychain-settings -t 3600 -u build.keychain
|
||||
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security find-identity -v -p codesigning build.keychain
|
||||
- name: Verify Certificate
|
||||
if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-13'
|
||||
run: |
|
||||
echo "Verifying Apple Developer Certificate..."
|
||||
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
|
||||
echo "Certificate Info: $CERT_INFO"
|
||||
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||
echo "Certificate ID: $CERT_ID"
|
||||
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
||||
echo "Certificate imported."
|
||||
|
||||
- name: Check DMG creation dependencies (macOS only)
|
||||
if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-13'
|
||||
run: |
|
||||
echo "🔍 Checking DMG creation dependencies on ${{ matrix.platform }}..."
|
||||
echo "hdiutil version: $(hdiutil --version || echo 'NOT FOUND')"
|
||||
echo "create-dmg availability: $(which create-dmg || echo 'NOT FOUND')"
|
||||
echo "Available disk space: $(df -h /tmp | tail -1)"
|
||||
echo "macOS version: $(sw_vers -productVersion)"
|
||||
echo "Available tools:"
|
||||
ls -la /usr/bin/hd* || echo "No hd* tools found"
|
||||
|
||||
- name: Build Tauri app
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }}
|
||||
SIGN: 1
|
||||
CI: true
|
||||
with:
|
||||
projectPath: ./frontend
|
||||
tauriScript: npx tauri
|
||||
args: ${{ matrix.args }}
|
||||
|
||||
- name: Rename artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p ./dist
|
||||
cd ./frontend/src-tauri/target
|
||||
|
||||
# Find and rename artifacts based on platform
|
||||
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||
find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \;
|
||||
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
|
||||
elif [ "${{ matrix.platform }}" = "macos-latest" ] || [ "${{ matrix.platform }}" = "macos-13" ]; then
|
||||
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
||||
find . -name "*.app" -exec cp -r {} "../../../dist/Stirling-PDF-${{ matrix.name }}.app" \;
|
||||
else
|
||||
find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \;
|
||||
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
|
||||
fi
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: Stirling-PDF-${{ matrix.name }}
|
||||
path: ./dist/*
|
||||
retention-days: 7
|
||||
|
||||
- name: Verify build artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
cd ./frontend/src-tauri/target
|
||||
|
||||
# Check for expected artifacts based on platform
|
||||
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||
echo "Checking for Windows artifacts..."
|
||||
find . -name "*.exe" -o -name "*.msi" | head -5
|
||||
if [ $(find . -name "*.exe" | wc -l) -eq 0 ]; then
|
||||
echo "❌ No Windows executable found"
|
||||
exit 1
|
||||
fi
|
||||
elif [ "${{ matrix.platform }}" = "macos-latest" ] || [ "${{ matrix.platform }}" = "macos-13" ]; then
|
||||
echo "Checking for macOS artifacts..."
|
||||
find . -name "*.dmg" -o -name "*.app" | head -5
|
||||
if [ $(find . -name "*.dmg" -o -name "*.app" | wc -l) -eq 0 ]; then
|
||||
echo "❌ No macOS artifacts found"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Checking for Linux artifacts..."
|
||||
find . -name "*.deb" -o -name "*.AppImage" | head -5
|
||||
if [ $(find . -name "*.deb" -o -name "*.AppImage" | wc -l) -eq 0 ]; then
|
||||
echo "❌ No Linux artifacts found"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✅ Build artifacts found for ${{ matrix.name }}"
|
||||
|
||||
- name: Test artifact sizes
|
||||
shell: bash
|
||||
run: |
|
||||
cd ./frontend/src-tauri/target
|
||||
echo "Artifact sizes for ${{ matrix.name }}:"
|
||||
find . -name "*.exe" -o -name "*.dmg" -o -name "*.deb" -o -name "*.AppImage" -o -name "*.msi" | while read file; do
|
||||
if [ -f "$file" ]; then
|
||||
size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo "unknown")
|
||||
echo "$file: $size bytes"
|
||||
# Check if file is suspiciously small (less than 1MB)
|
||||
if [ "$size" != "unknown" ] && [ "$size" -lt 1048576 ]; then
|
||||
echo "⚠️ Warning: $file is smaller than 1MB"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
report:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
- name: Report build results
|
||||
run: |
|
||||
if [ "${{ needs.build.result }}" = "success" ]; then
|
||||
echo "✅ All Tauri builds completed successfully!"
|
||||
echo "Artifacts are ready for distribution."
|
||||
else
|
||||
echo "❌ Some Tauri builds failed."
|
||||
echo "Please check the logs and fix any issues."
|
||||
exit 1
|
||||
fi
|
228
DesktopApplicationDevelopmentGuide.md
Normal file
@ -0,0 +1,228 @@
|
||||
# JLink Runtime Bundling for Stirling-PDF
|
||||
|
||||
This guide explains how to use JLink to bundle a custom Java runtime with your Tauri application, eliminating the need for users to have Java installed.
|
||||
|
||||
## Overview
|
||||
|
||||
Instead of requiring users to install a JRE separately, JLink creates a minimal, custom Java runtime that includes only the modules your application needs. This approach:
|
||||
|
||||
- **Eliminates JRE dependency**: Users don't need Java installed
|
||||
- **Reduces size**: Only includes necessary Java modules
|
||||
- **Improves security**: Minimal attack surface with fewer modules
|
||||
- **Ensures consistency**: Same Java version across all deployments
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **JDK 17 or higher** (not just JRE - you need `jlink` command)
|
||||
- **Node.js and npm** for the frontend
|
||||
- **Rust and Tauri CLI** for building the desktop app
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Build with JLink
|
||||
|
||||
Run the appropriate build script for your platform:
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
./scripts/build-tauri-jlink.sh
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```cmd
|
||||
scripts\build-tauri-jlink.bat
|
||||
```
|
||||
|
||||
### 2. Build Tauri Application
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run tauri-build
|
||||
```
|
||||
|
||||
The resulting application will include the bundled JRE and won't require Java to be installed on the target system.
|
||||
|
||||
## What the Build Script Does
|
||||
|
||||
1. **Builds the Stirling-PDF JAR** using Gradle
|
||||
2. **Analyzes dependencies** using `jdeps` to determine required Java modules
|
||||
3. **Creates custom JRE** using `jlink` with only necessary modules
|
||||
4. **Copies files** to the correct Tauri directories:
|
||||
- JAR file → `frontend/src-tauri/libs/`
|
||||
- Custom JRE → `frontend/src-tauri/runtime/jre/`
|
||||
5. **Creates test launchers** for standalone testing
|
||||
|
||||
## Directory Structure
|
||||
|
||||
After running the build script:
|
||||
|
||||
```
|
||||
frontend/src-tauri/
|
||||
├── libs/
|
||||
│ └── Stirling-PDF-X.X.X.jar
|
||||
├── runtime/
|
||||
│ ├── jre/ # Custom JLink runtime
|
||||
│ │ ├── bin/
|
||||
│ │ │ ├── java(.exe)
|
||||
│ │ │ └── ...
|
||||
│ │ ├── lib/
|
||||
│ │ └── ...
|
||||
│ ├── launch-stirling.sh # Test launcher (Linux/macOS)
|
||||
│ └── launch-stirling.bat # Test launcher (Windows)
|
||||
└── tauri.conf.json # Already configured to bundle runtime
|
||||
```
|
||||
|
||||
## Testing the Bundled Runtime
|
||||
|
||||
Before building the full Tauri app, you can test the bundled runtime:
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
./frontend/src-tauri/runtime/launch-stirling.sh
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```cmd
|
||||
frontend\src-tauri\runtime\launch-stirling.bat
|
||||
```
|
||||
|
||||
This will start Stirling-PDF using the bundled JRE, accessible at http://localhost:8080
|
||||
|
||||
## Configuration Details
|
||||
|
||||
### Tauri Configuration (`tauri.conf.json`)
|
||||
|
||||
The bundle resources are configured to include both the JAR and runtime:
|
||||
|
||||
```json
|
||||
{
|
||||
"bundle": {
|
||||
"resources": [
|
||||
"libs/*.jar",
|
||||
"runtime/jre/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Gradle Configuration (`build.gradle`)
|
||||
|
||||
JLink options are configured in the jpackage section:
|
||||
|
||||
```gradle
|
||||
jLinkOptions = [
|
||||
"--strip-debug",
|
||||
"--compress=2",
|
||||
"--no-header-files",
|
||||
"--no-man-pages"
|
||||
]
|
||||
|
||||
addModules = [
|
||||
"java.base",
|
||||
"java.desktop",
|
||||
"java.logging",
|
||||
"java.management",
|
||||
// ... other required modules
|
||||
]
|
||||
```
|
||||
|
||||
### Rust Code (`lib.rs`)
|
||||
|
||||
The application automatically detects and uses the bundled JRE instead of system Java.
|
||||
|
||||
## Modules Included
|
||||
|
||||
The custom runtime includes these Java modules:
|
||||
|
||||
- `java.base` - Core Java functionality
|
||||
- `java.desktop` - AWT/Swing (for UI components)
|
||||
- `java.instrument` - Java instrumentation (required by Jetty)
|
||||
- `java.logging` - Logging framework
|
||||
- `java.management` - JMX and monitoring
|
||||
- `java.naming` - JNDI services
|
||||
- `java.net.http` - HTTP client
|
||||
- `java.security.jgss` - Security services
|
||||
- `java.sql` - Database connectivity
|
||||
- `java.xml` - XML processing
|
||||
- `java.xml.crypto` - XML security
|
||||
- `jdk.crypto.ec` - Elliptic curve cryptography
|
||||
- `jdk.crypto.cryptoki` - PKCS#11 support
|
||||
- `jdk.unsupported` - Internal APIs (used by some libraries)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### JLink Not Found
|
||||
```
|
||||
❌ jlink is not available
|
||||
```
|
||||
**Solution**: Install a full JDK (not just JRE). JLink is included with JDK 9+.
|
||||
|
||||
### Module Not Found During Runtime
|
||||
If the application fails with module-related errors, you may need to add additional modules to the `addModules` list in `build.gradle`.
|
||||
|
||||
### Large Runtime Size
|
||||
The bundled runtime should be 50-80MB. If it's much larger:
|
||||
- Ensure `--strip-debug` and `--compress=2` options are used
|
||||
- Review the module list - remove unnecessary modules
|
||||
- Consider using `--no-header-files` and `--no-man-pages`
|
||||
|
||||
## Benefits Over Traditional JAR Approach
|
||||
|
||||
| Aspect | Traditional JAR | JLink Bundle |
|
||||
|--------|----------------|--------------|
|
||||
| User Setup | Requires JRE installation | No Java installation needed |
|
||||
| Distribution Size | Smaller JAR, but requires ~200MB JRE | Larger bundle (~80MB), but self-contained |
|
||||
| Java Version | Depends on user's installed version | Consistent, controlled version |
|
||||
| Security Updates | User manages JRE updates | Developer controls runtime version |
|
||||
| Startup Time | May be faster (shared JRE) | Slightly slower (isolated runtime) |
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Module Analysis
|
||||
|
||||
To analyze your specific JAR's module requirements:
|
||||
|
||||
```bash
|
||||
jdeps --print-module-deps --ignore-missing-deps build/libs/Stirling-PDF-*.jar
|
||||
```
|
||||
|
||||
### Manual JLink Command
|
||||
|
||||
If you want to create the runtime manually:
|
||||
|
||||
```bash
|
||||
jlink \
|
||||
--add-modules java.base,java.desktop,java.logging,java.management,java.naming,java.net.http,java.security.jgss,java.sql,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported \
|
||||
--strip-debug \
|
||||
--compress=2 \
|
||||
--no-header-files \
|
||||
--no-man-pages \
|
||||
--output frontend/src-tauri/runtime/jre
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From JAR-based Tauri App
|
||||
|
||||
1. Update your Tauri configuration to include the runtime resources
|
||||
2. Update your Rust code to use the bundled JRE path
|
||||
3. Run the JLink build script
|
||||
4. Test the bundled runtime
|
||||
5. Build and distribute the new self-contained app
|
||||
|
||||
### Deployment
|
||||
|
||||
The final Tauri application will be completely self-contained. Users can:
|
||||
- Install the app normally (no Java installation required)
|
||||
- Run the app immediately after installation
|
||||
- Not worry about Java version compatibility issues
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter issues with the JLink bundling:
|
||||
|
||||
1. Ensure you have a JDK (not JRE) installed
|
||||
2. Check that the Java version is 17 or higher
|
||||
3. Verify that the build script completed successfully
|
||||
4. Test the bundled runtime using the provided launcher scripts
|
||||
5. Check the Tauri build logs for any missing resources
|
@ -32,6 +32,12 @@ This guide focuses on developing for Stirling 2.0, including both the React fron
|
||||
- Docker for containerization
|
||||
- Gradle for build management
|
||||
|
||||
**Desktop Application (Tauri):**
|
||||
- Tauri for cross-platform desktop app packaging
|
||||
- Rust backend for system integration
|
||||
- PDF file association support
|
||||
- Self-contained JRE bundling with JLink
|
||||
|
||||
**Legacy (reference only during development):**
|
||||
- Thymeleaf templates (being completely replaced in 2.0)
|
||||
|
||||
@ -44,6 +50,8 @@ This guide focuses on developing for Stirling 2.0, including both the React fron
|
||||
- Java JDK 17 or later (JDK 21 recommended)
|
||||
- Node.js 18+ and npm (required for frontend development)
|
||||
- Gradle 7.0 or later (Included within the repo)
|
||||
- Rust and Cargo (required for Tauri desktop app development)
|
||||
- Tauri CLI (install with `cargo install tauri-cli`)
|
||||
|
||||
### Setup Steps
|
||||
|
||||
@ -95,6 +103,14 @@ Stirling 2.0 uses client-side file storage:
|
||||
### Legacy Code Reference
|
||||
The existing Thymeleaf templates remain in the codebase during development as reference material but will be completely removed for the 2.0 release.
|
||||
|
||||
### Tauri Desktop App Development
|
||||
Stirling-PDF can be packaged as a cross-platform desktop application using Tauri with PDF file association support and bundled JRE:
|
||||
|
||||
**Quick Start:**
|
||||
1. **Development/Testing**: `npm run tauri-dev "path/to/test.pdf"`
|
||||
2. **Building**: See [DesktopApplicationDevelopmentGuide.md](DesktopApplicationDevelopmentGuide.md) for complete build instructions
|
||||
3. **Features**: File associations, self-contained JRE, cross-platform support
|
||||
|
||||
## 5. Project Structure
|
||||
|
||||
```bash
|
||||
@ -109,6 +125,12 @@ Stirling-PDF/
|
||||
│ │ ├── services/ # API and utility services
|
||||
│ │ ├── types/ # TypeScript type definitions
|
||||
│ │ └── utils/ # Utility functions
|
||||
│ ├── src-tauri/ # Tauri desktop app configuration
|
||||
│ │ ├── src/ # Rust backend code
|
||||
│ │ ├── libs/ # JAR files (generated by build scripts)
|
||||
│ │ ├── runtime/ # Bundled JRE (generated by build scripts)
|
||||
│ │ ├── Cargo.toml # Rust dependencies
|
||||
│ │ └── tauri.conf.json # Tauri configuration
|
||||
│ ├── public/
|
||||
│ │ └── locales/ # Internationalization files (JSON)
|
||||
│ ├── package.json # Frontend dependencies
|
||||
|
@ -29,7 +29,6 @@ dependencies {
|
||||
if (System.getenv('STIRLING_PDF_DESKTOP_UI') != 'false'
|
||||
|| (project.hasProperty('STIRLING_PDF_DESKTOP_UI')
|
||||
&& project.getProperty('STIRLING_PDF_DESKTOP_UI') != 'false')) {
|
||||
implementation 'me.friwi:jcefmaven:132.3.1'
|
||||
implementation 'org.openjfx:javafx-controls:21'
|
||||
implementation 'org.openjfx:javafx-swing:21'
|
||||
}
|
||||
|
@ -151,6 +151,13 @@ public class SPDFApplication {
|
||||
serverPortStatic = serverPort;
|
||||
String url = baseUrl + ":" + getStaticPort() + contextPath;
|
||||
|
||||
// Log Tauri mode information
|
||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_TAURI_MODE", "false"))) {
|
||||
String parentPid = System.getenv("TAURI_PARENT_PID");
|
||||
log.info(
|
||||
"Running in Tauri mode. Parent process PID: {}",
|
||||
parentPid != null ? parentPid : "not set");
|
||||
}
|
||||
if (webBrowser != null
|
||||
&& Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||
webBrowser.initWebUI(url);
|
||||
|
@ -1,497 +0,0 @@
|
||||
package stirling.software.SPDF.UI.impl;
|
||||
|
||||
import java.awt.AWTException;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Frame;
|
||||
import java.awt.Image;
|
||||
import java.awt.MenuItem;
|
||||
import java.awt.PopupMenu;
|
||||
import java.awt.SystemTray;
|
||||
import java.awt.TrayIcon;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.event.WindowStateListener;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.Timer;
|
||||
|
||||
import org.cef.CefApp;
|
||||
import org.cef.CefClient;
|
||||
import org.cef.CefSettings;
|
||||
import org.cef.browser.CefBrowser;
|
||||
import org.cef.callback.CefBeforeDownloadCallback;
|
||||
import org.cef.callback.CefDownloadItem;
|
||||
import org.cef.callback.CefDownloadItemCallback;
|
||||
import org.cef.handler.CefDownloadHandlerAdapter;
|
||||
import org.cef.handler.CefLoadHandlerAdapter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import me.friwi.jcefmaven.CefAppBuilder;
|
||||
import me.friwi.jcefmaven.EnumProgress;
|
||||
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
|
||||
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
|
||||
|
||||
import stirling.software.SPDF.UI.WebBrowser;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.util.UIScaling;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@ConditionalOnProperty(
|
||||
name = "STIRLING_PDF_DESKTOP_UI",
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
public class DesktopBrowser implements WebBrowser {
|
||||
private static CefApp cefApp;
|
||||
private static CefClient client;
|
||||
private static CefBrowser browser;
|
||||
private static JFrame frame;
|
||||
private static LoadingWindow loadingWindow;
|
||||
private static volatile boolean browserInitialized = false;
|
||||
private static TrayIcon trayIcon;
|
||||
private static SystemTray systemTray;
|
||||
|
||||
public DesktopBrowser() {
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
loadingWindow = new LoadingWindow(null, "Initializing...");
|
||||
loadingWindow.setVisible(true);
|
||||
});
|
||||
}
|
||||
|
||||
public void initWebUI(String url) {
|
||||
CompletableFuture.runAsync(
|
||||
() -> {
|
||||
try {
|
||||
CefAppBuilder builder = new CefAppBuilder();
|
||||
configureCefSettings(builder);
|
||||
builder.setProgressHandler(createProgressHandler());
|
||||
builder.setInstallDir(
|
||||
new File(InstallationPathConfig.getClientWebUIPath()));
|
||||
// Build and initialize CEF
|
||||
cefApp = builder.build();
|
||||
client = cefApp.createClient();
|
||||
|
||||
// Set up download handler
|
||||
setupDownloadHandler();
|
||||
|
||||
// Create browser and frame on EDT
|
||||
SwingUtilities.invokeAndWait(
|
||||
() -> {
|
||||
browser = client.createBrowser(url, false, false);
|
||||
setupMainFrame();
|
||||
setupLoadHandler();
|
||||
|
||||
// Force initialize UI after 7 seconds if not already done
|
||||
Timer timeoutTimer =
|
||||
new Timer(
|
||||
2500,
|
||||
e -> {
|
||||
log.warn(
|
||||
"Loading timeout reached. Forcing"
|
||||
+ " UI transition.");
|
||||
if (!browserInitialized) {
|
||||
// Force UI initialization
|
||||
forceInitializeUI();
|
||||
}
|
||||
});
|
||||
timeoutTimer.setRepeats(false);
|
||||
timeoutTimer.start();
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("Error initializing JCEF browser: ", e);
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void configureCefSettings(CefAppBuilder builder) {
|
||||
CefSettings settings = builder.getCefSettings();
|
||||
String basePath = InstallationPathConfig.getClientWebUIPath();
|
||||
log.info("basePath " + basePath);
|
||||
settings.cache_path = new File(basePath + "cache").getAbsolutePath();
|
||||
settings.root_cache_path = new File(basePath + "root_cache").getAbsolutePath();
|
||||
// settings.browser_subprocess_path = new File(basePath +
|
||||
// "subprocess").getAbsolutePath();
|
||||
// settings.resources_dir_path = new File(basePath + "resources").getAbsolutePath();
|
||||
// settings.locales_dir_path = new File(basePath + "locales").getAbsolutePath();
|
||||
settings.log_file = new File(basePath, "debug.log").getAbsolutePath();
|
||||
|
||||
settings.persist_session_cookies = true;
|
||||
settings.windowless_rendering_enabled = false;
|
||||
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO;
|
||||
|
||||
builder.setAppHandler(
|
||||
new MavenCefAppHandlerAdapter() {
|
||||
@Override
|
||||
public void stateHasChanged(org.cef.CefApp.CefAppState state) {
|
||||
log.info("CEF state changed: " + state);
|
||||
if (state == CefApp.CefAppState.TERMINATED) {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupDownloadHandler() {
|
||||
client.addDownloadHandler(
|
||||
new CefDownloadHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforeDownload(
|
||||
CefBrowser browser,
|
||||
CefDownloadItem downloadItem,
|
||||
String suggestedName,
|
||||
CefBeforeDownloadCallback callback) {
|
||||
callback.Continue("", true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadUpdated(
|
||||
CefBrowser browser,
|
||||
CefDownloadItem downloadItem,
|
||||
CefDownloadItemCallback callback) {
|
||||
if (downloadItem.isComplete()) {
|
||||
log.info("Download completed: " + downloadItem.getFullPath());
|
||||
} else if (downloadItem.isCanceled()) {
|
||||
log.info("Download canceled: " + downloadItem.getFullPath());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ConsoleProgressHandler createProgressHandler() {
|
||||
return new ConsoleProgressHandler() {
|
||||
@Override
|
||||
public void handleProgress(EnumProgress state, float percent) {
|
||||
Objects.requireNonNull(state, "state cannot be null");
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
if (loadingWindow != null) {
|
||||
switch (state) {
|
||||
case LOCATING:
|
||||
loadingWindow.setStatus("Locating Files...");
|
||||
loadingWindow.setProgress(0);
|
||||
break;
|
||||
case DOWNLOADING:
|
||||
if (percent >= 0) {
|
||||
loadingWindow.setStatus(
|
||||
String.format(
|
||||
"Downloading additional files: %.0f%%",
|
||||
percent));
|
||||
loadingWindow.setProgress((int) percent);
|
||||
}
|
||||
break;
|
||||
case EXTRACTING:
|
||||
loadingWindow.setStatus("Extracting files...");
|
||||
loadingWindow.setProgress(60);
|
||||
break;
|
||||
case INITIALIZING:
|
||||
loadingWindow.setStatus("Initializing UI...");
|
||||
loadingWindow.setProgress(80);
|
||||
break;
|
||||
case INITIALIZED:
|
||||
loadingWindow.setStatus("Finalising startup...");
|
||||
loadingWindow.setProgress(90);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void setupMainFrame() {
|
||||
frame = new JFrame("Stirling-PDF");
|
||||
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||||
frame.setUndecorated(true);
|
||||
frame.setOpacity(0.0f);
|
||||
|
||||
JPanel contentPane = new JPanel(new BorderLayout());
|
||||
contentPane.setDoubleBuffered(true);
|
||||
contentPane.add(browser.getUIComponent(), BorderLayout.CENTER);
|
||||
frame.setContentPane(contentPane);
|
||||
|
||||
frame.addWindowListener(
|
||||
new java.awt.event.WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(java.awt.event.WindowEvent windowEvent) {
|
||||
cleanup();
|
||||
System.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
frame.setSize(UIScaling.scaleWidth(1280), UIScaling.scaleHeight(800));
|
||||
frame.setLocationRelativeTo(null);
|
||||
|
||||
loadIcon();
|
||||
}
|
||||
|
||||
private void setupLoadHandler() {
|
||||
final long initStartTime = System.currentTimeMillis();
|
||||
log.info("Setting up load handler at: {}", initStartTime);
|
||||
|
||||
client.addLoadHandler(
|
||||
new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadingStateChange(
|
||||
CefBrowser browser,
|
||||
boolean isLoading,
|
||||
boolean canGoBack,
|
||||
boolean canGoForward) {
|
||||
log.debug(
|
||||
"Loading state change - isLoading: {}, canGoBack: {}, canGoForward:"
|
||||
+ " {}, browserInitialized: {}, Time elapsed: {}ms",
|
||||
isLoading,
|
||||
canGoBack,
|
||||
canGoForward,
|
||||
browserInitialized,
|
||||
System.currentTimeMillis() - initStartTime);
|
||||
|
||||
if (!isLoading && !browserInitialized) {
|
||||
log.info(
|
||||
"Browser finished loading, preparing to initialize UI"
|
||||
+ " components");
|
||||
browserInitialized = true;
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
try {
|
||||
if (loadingWindow != null) {
|
||||
log.info("Starting UI initialization sequence");
|
||||
|
||||
// Close loading window first
|
||||
loadingWindow.setVisible(false);
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
log.info("Loading window disposed");
|
||||
|
||||
// Then setup the main frame
|
||||
frame.setVisible(false);
|
||||
frame.dispose();
|
||||
frame.setOpacity(1.0f);
|
||||
frame.setUndecorated(false);
|
||||
frame.pack();
|
||||
frame.setSize(
|
||||
UIScaling.scaleWidth(1280),
|
||||
UIScaling.scaleHeight(800));
|
||||
frame.setLocationRelativeTo(null);
|
||||
log.debug("Frame reconfigured");
|
||||
|
||||
// Show the main frame
|
||||
frame.setVisible(true);
|
||||
frame.requestFocus();
|
||||
frame.toFront();
|
||||
log.info("Main frame displayed and focused");
|
||||
|
||||
// Focus the browser component
|
||||
Timer focusTimer =
|
||||
new Timer(
|
||||
100,
|
||||
e -> {
|
||||
try {
|
||||
browser.getUIComponent()
|
||||
.requestFocus();
|
||||
log.info(
|
||||
"Browser component"
|
||||
+ " focused");
|
||||
} catch (Exception ex) {
|
||||
log.error(
|
||||
"Error focusing"
|
||||
+ " browser",
|
||||
ex);
|
||||
}
|
||||
});
|
||||
focusTimer.setRepeats(false);
|
||||
focusTimer.start();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error during UI initialization", e);
|
||||
// Attempt cleanup on error
|
||||
if (loadingWindow != null) {
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
}
|
||||
if (frame != null) {
|
||||
frame.setVisible(true);
|
||||
frame.requestFocus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupTrayIcon(Image icon) {
|
||||
if (!SystemTray.isSupported()) {
|
||||
log.warn("System tray is not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
systemTray = SystemTray.getSystemTray();
|
||||
|
||||
// Create popup menu
|
||||
PopupMenu popup = new PopupMenu();
|
||||
|
||||
// Create menu items
|
||||
MenuItem showItem = new MenuItem("Show");
|
||||
showItem.addActionListener(
|
||||
e -> {
|
||||
frame.setVisible(true);
|
||||
frame.setState(Frame.NORMAL);
|
||||
});
|
||||
|
||||
MenuItem exitItem = new MenuItem("Exit");
|
||||
exitItem.addActionListener(
|
||||
e -> {
|
||||
cleanup();
|
||||
System.exit(0);
|
||||
});
|
||||
|
||||
// Add menu items to popup menu
|
||||
popup.add(showItem);
|
||||
popup.addSeparator();
|
||||
popup.add(exitItem);
|
||||
|
||||
// Create tray icon
|
||||
trayIcon = new TrayIcon(icon, "Stirling-PDF", popup);
|
||||
trayIcon.setImageAutoSize(true);
|
||||
|
||||
// Add double-click behavior
|
||||
trayIcon.addActionListener(
|
||||
e -> {
|
||||
frame.setVisible(true);
|
||||
frame.setState(Frame.NORMAL);
|
||||
});
|
||||
|
||||
// Add tray icon to system tray
|
||||
systemTray.add(trayIcon);
|
||||
|
||||
// Modify frame behavior to minimize to tray
|
||||
frame.addWindowStateListener(
|
||||
new WindowStateListener() {
|
||||
public void windowStateChanged(WindowEvent e) {
|
||||
if (e.getNewState() == Frame.ICONIFIED) {
|
||||
frame.setVisible(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (AWTException e) {
|
||||
log.error("Error setting up system tray icon", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadIcon() {
|
||||
try {
|
||||
Image icon = null;
|
||||
String[] iconPaths = {"/static/favicon.ico"};
|
||||
|
||||
for (String path : iconPaths) {
|
||||
if (icon != null) break;
|
||||
try {
|
||||
try (InputStream is = getClass().getResourceAsStream(path)) {
|
||||
if (is != null) {
|
||||
icon = ImageIO.read(is);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not load icon from " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (icon != null) {
|
||||
frame.setIconImage(icon);
|
||||
setupTrayIcon(icon);
|
||||
} else {
|
||||
log.warn("Could not load icon from any source");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error loading icon", e);
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
if (browser != null) browser.close(true);
|
||||
if (client != null) client.dispose();
|
||||
if (cefApp != null) cefApp.dispose();
|
||||
if (loadingWindow != null) loadingWindow.dispose();
|
||||
}
|
||||
|
||||
public static void forceInitializeUI() {
|
||||
try {
|
||||
if (loadingWindow != null) {
|
||||
log.info("Forcing start of UI initialization sequence");
|
||||
|
||||
// Close loading window first
|
||||
loadingWindow.setVisible(false);
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
log.info("Loading window disposed");
|
||||
|
||||
// Then setup the main frame
|
||||
frame.setVisible(false);
|
||||
frame.dispose();
|
||||
frame.setOpacity(1.0f);
|
||||
frame.setUndecorated(false);
|
||||
frame.pack();
|
||||
frame.setSize(UIScaling.scaleWidth(1280), UIScaling.scaleHeight(800));
|
||||
frame.setLocationRelativeTo(null);
|
||||
log.debug("Frame reconfigured");
|
||||
|
||||
// Show the main frame
|
||||
frame.setVisible(true);
|
||||
frame.requestFocus();
|
||||
frame.toFront();
|
||||
log.info("Main frame displayed and focused");
|
||||
|
||||
// Focus the browser component if available
|
||||
if (browser != null) {
|
||||
Timer focusTimer =
|
||||
new Timer(
|
||||
100,
|
||||
e -> {
|
||||
try {
|
||||
browser.getUIComponent().requestFocus();
|
||||
log.info("Browser component focused");
|
||||
} catch (Exception ex) {
|
||||
log.error(
|
||||
"Error focusing browser during force ui"
|
||||
+ " initialization.",
|
||||
ex);
|
||||
}
|
||||
});
|
||||
focusTimer.setRepeats(false);
|
||||
focusTimer.start();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error during Forced UI initialization.", e);
|
||||
// Attempt cleanup on error
|
||||
if (loadingWindow != null) {
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
}
|
||||
if (frame != null) {
|
||||
frame.setVisible(true);
|
||||
frame.setOpacity(1.0f);
|
||||
frame.setUndecorated(false);
|
||||
frame.requestFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
/**
|
||||
* Monitor for Tauri parent process to detect orphaned Java backend processes. When running in Tauri
|
||||
* mode, this component periodically checks if the parent Tauri process is still alive. If the
|
||||
* parent process terminates unexpectedly, this will trigger a graceful shutdown of the Java backend
|
||||
* to prevent orphaned processes.
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "STIRLING_PDF_TAURI_MODE", havingValue = "true")
|
||||
public class TauriProcessMonitor {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TauriProcessMonitor.class);
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
private String parentProcessId;
|
||||
private ScheduledExecutorService scheduler;
|
||||
private volatile boolean monitoring = false;
|
||||
|
||||
public TauriProcessMonitor(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
parentProcessId = System.getenv("TAURI_PARENT_PID");
|
||||
|
||||
if (parentProcessId != null && !parentProcessId.trim().isEmpty()) {
|
||||
logger.info("Tauri mode detected. Parent process ID: {}", parentProcessId);
|
||||
startMonitoring();
|
||||
} else {
|
||||
logger.warn(
|
||||
"TAURI_PARENT_PID environment variable not found. Tauri process monitoring disabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private void startMonitoring() {
|
||||
scheduler =
|
||||
Executors.newSingleThreadScheduledExecutor(
|
||||
r -> {
|
||||
Thread t = new Thread(r, "tauri-process-monitor");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
monitoring = true;
|
||||
|
||||
// Check every 5 seconds
|
||||
scheduler.scheduleAtFixedRate(this::checkParentProcess, 5, 5, TimeUnit.SECONDS);
|
||||
|
||||
logger.info("Started monitoring parent Tauri process (PID: {})", parentProcessId);
|
||||
}
|
||||
|
||||
private void checkParentProcess() {
|
||||
if (!monitoring) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!isProcessAlive(parentProcessId)) {
|
||||
logger.warn(
|
||||
"Parent Tauri process (PID: {}) is no longer alive. Initiating graceful shutdown...",
|
||||
parentProcessId);
|
||||
initiateGracefulShutdown();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Error checking parent process status", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isProcessAlive(String pid) {
|
||||
try {
|
||||
long processId = Long.parseLong(pid);
|
||||
|
||||
// Check if process exists using ProcessHandle (Java 9+)
|
||||
return ProcessHandle.of(processId).isPresent();
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
logger.error("Invalid parent process ID format: {}", pid);
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
logger.error("Error checking if process {} is alive", pid, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void initiateGracefulShutdown() {
|
||||
monitoring = false;
|
||||
|
||||
logger.info("Orphaned Java backend detected. Shutting down gracefully...");
|
||||
|
||||
// Shutdown asynchronously to avoid blocking the monitor thread
|
||||
CompletableFuture.runAsync(
|
||||
() -> {
|
||||
try {
|
||||
// Give a small delay to ensure logging completes
|
||||
Thread.sleep(1000);
|
||||
|
||||
if (applicationContext instanceof ConfigurableApplicationContext) {
|
||||
((ConfigurableApplicationContext) applicationContext).close();
|
||||
} else {
|
||||
// Fallback to system exit
|
||||
logger.warn(
|
||||
"Unable to shutdown Spring context gracefully, using System.exit");
|
||||
System.exit(0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Error during graceful shutdown", e);
|
||||
System.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
monitoring = false;
|
||||
|
||||
if (scheduler != null && !scheduler.isShutdown()) {
|
||||
logger.info("Shutting down Tauri process monitor");
|
||||
scheduler.shutdown();
|
||||
|
||||
try {
|
||||
if (!scheduler.awaitTermination(2, TimeUnit.SECONDS)) {
|
||||
scheduler.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
scheduler.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the current Java process ID for logging/debugging purposes */
|
||||
public static String getCurrentProcessId() {
|
||||
try {
|
||||
return ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
|
||||
} catch (Exception e) {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
@ -28,4 +29,17 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
"file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
|
||||
// .setCachePeriod(0); // Optional: disable caching
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_TAURI_MODE", "false"))) {
|
||||
// Tauri mode CORS configuration
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins(
|
||||
"http://localhost:5173", "http://tauri.localhost", "tauri://localhost")
|
||||
.allowedMethods("*")
|
||||
.allowedHeaders("*");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,5 +6,13 @@ VITE_API_BASE_URL=${VITE_API_BASE_URL:-"http://backend:8080"}
|
||||
# Replace the placeholder in nginx.conf with the actual backend URL
|
||||
sed -i "s|\${VITE_API_BASE_URL}|${VITE_API_BASE_URL}|g" /etc/nginx/nginx.conf
|
||||
|
||||
# Inject runtime configuration into config.js
|
||||
cat > /usr/share/nginx/html/config.js << EOF
|
||||
// Runtime configuration - injected at container startup
|
||||
window.runtimeConfig = {
|
||||
apiBaseUrl: '${VITE_API_BASE_URL}'
|
||||
};
|
||||
EOF
|
||||
|
||||
# Start nginx
|
||||
exec nginx -g "daemon off;"
|
@ -72,3 +72,15 @@ This section has moved here: [https://facebook.github.io/create-react-app/docs/d
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||
|
||||
|
||||
## Tauri
|
||||
### Dev
|
||||
To run Tauri in development. Use the command:
|
||||
````npm run tauri-dev```
|
||||
This will run the gradle runboot command and the tauri dev command concurrently, starting the app once both are stable.
|
||||
|
||||
### Build
|
||||
To build a deployment of the Tauri app. Use the command:
|
||||
```npm run tauri-build```
|
||||
This will bundle the backend and frontend into one executable for each target. Targets can be set within the `tauri.conf.json` file.
|
||||
|
@ -12,11 +12,12 @@
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<title>Vite App</title>
|
||||
<title>Stirling-PDF</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script src="/config.js"></script>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
327
frontend/package-lock.json
generated
@ -17,6 +17,8 @@
|
||||
"@mui/icons-material": "^7.1.0",
|
||||
"@mui/material": "^7.1.0",
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@ -37,6 +39,7 @@
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.5.0",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@vitejs/plugin-react": "^4.5.0",
|
||||
@ -2019,6 +2022,242 @@
|
||||
"tailwindcss": "4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.6.0.tgz",
|
||||
"integrity": "sha512-hRNcdercfgpzgFrMXWwNDBN0B7vNzOzRepy6ZAmhxi5mDLVPNrTpo9MGg2tN/F7JRugj4d2aF7E1rtPXAHaetg==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.5.0.tgz",
|
||||
"integrity": "sha512-rAtHqG0Gh/IWLjN2zTf3nZqYqbo81oMbqop56rGTjrlWk9pTTAjkqOjSL9XQLIMZ3RbeVjveCqqCA0s8RnLdMg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
"tauri": "tauri.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.5.0",
|
||||
"@tauri-apps/cli-darwin-x64": "2.5.0",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.5.0",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.5.0",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.5.0",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.5.0",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.5.0",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.5.0",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.5.0",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.5.0",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.5.0.tgz",
|
||||
"integrity": "sha512-VuVAeTFq86dfpoBDNYAdtQVLbP0+2EKCHIIhkaxjeoPARR0sLpFHz2zs0PcFU76e+KAaxtEtAJAXGNUc8E1PzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.5.0.tgz",
|
||||
"integrity": "sha512-hUF01sC06cZVa8+I0/VtsHOk9BbO75rd+YdtHJ48xTdcYaQ5QIwL4yZz9OR1AKBTaUYhBam8UX9Pvd5V2/4Dpw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.5.0.tgz",
|
||||
"integrity": "sha512-LQKqttsK252LlqYyX8R02MinUsfFcy3+NZiJwHFgi5Y3+ZUIAED9cSxJkyNtuY5KMnR4RlpgWyLv4P6akN1xhg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.5.0.tgz",
|
||||
"integrity": "sha512-mTQufsPcpdHg5RW0zypazMo4L55EfeE5snTzrPqbLX4yCK2qalN7+rnP8O8GT06xhp6ElSP/Ku1M2MR297SByQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.0.tgz",
|
||||
"integrity": "sha512-rQO1HhRUQqyEaal5dUVOQruTRda/TD36s9kv1hTxZiFuSq3558lsTjAcUEnMAtBcBkps20sbyTJNMT0AwYIk8Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.5.0.tgz",
|
||||
"integrity": "sha512-7oS18FN46yDxyw1zX/AxhLAd7T3GrLj3Ai6s8hZKd9qFVzrAn36ESL7d3G05s8wEtsJf26qjXnVF4qleS3dYsA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.5.0.tgz",
|
||||
"integrity": "sha512-SG5sFNL7VMmDBdIg3nO3EzNRT306HsiEQ0N90ILe3ZABYAVoPDO/ttpCO37ApLInTzrq/DLN+gOlC/mgZvLw1w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.0.tgz",
|
||||
"integrity": "sha512-QXDM8zp/6v05PNWju5ELsVwF0VH1n6b5pk2E6W/jFbbiwz80Vs1lACl9pv5kEHkrxBj+aWU/03JzGuIj2g3SkQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.5.0.tgz",
|
||||
"integrity": "sha512-pFSHFK6b+o9y4Un8w0gGLwVyFTZaC3P0kQ7umRt/BLDkzD5RnQ4vBM7CF8BCU5nkwmEBUCZd7Wt3TWZxe41o6Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.5.0.tgz",
|
||||
"integrity": "sha512-EArv1IaRlogdLAQyGlKmEqZqm5RfHCUMhJoedWu7GtdbOMUfSAz6FMX2boE1PtEmNO4An+g188flLeVErrxEKg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.5.0.tgz",
|
||||
"integrity": "sha512-lj43EFYbnAta8pd9JnUq87o+xRUR0odz+4rixBtTUwUgdRdwQ2V9CzFtsMu6FQKpFQ6mujRK6P1IEwhL6ADRsQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-fs": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.4.0.tgz",
|
||||
"integrity": "sha512-Sp8AdDcbyXyk6LD6Pmdx44SH3LPeNAvxR2TFfq/8CwqzfO1yOyV+RzT8fov0NNN7d9nvW7O7MtMAptJ42YXA5g==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
||||
@ -2629,6 +2868,21 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@ -4719,21 +4973,6 @@
|
||||
"postcss": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-cli/node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-cli/node_modules/fs-extra": {
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
|
||||
@ -4815,35 +5054,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-cli/node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-cli/node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-js": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
|
||||
@ -6341,6 +6551,35 @@
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@
|
||||
"@mui/icons-material": "^7.1.0",
|
||||
"@mui/material": "^7.1.0",
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@ -36,6 +38,8 @@
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri-dev": "tauri dev --no-watch",
|
||||
"tauri-build": "tauri build",
|
||||
"generate-licenses": "node scripts/generate-licenses.js"
|
||||
},
|
||||
"eslintConfig": {
|
||||
@ -57,6 +61,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.5.0",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@vitejs/plugin-react": "^4.5.0",
|
||||
|
4
frontend/public/config.js
Normal file
@ -0,0 +1,4 @@
|
||||
// Runtime configuration - injected at container startup
|
||||
window.runtimeConfig = {
|
||||
apiBaseUrl: 'http://localhost:8080'
|
||||
};
|
5
frontend/src-tauri/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
/gen/schemas
|
||||
/runtime/
|
5420
frontend/src-tauri/Cargo.lock
generated
Normal file
35
frontend/src-tauri/Cargo.toml
Normal file
@ -0,0 +1,35 @@
|
||||
[package]
|
||||
name = "stirling-pdf"
|
||||
version = "0.1.0"
|
||||
description = "Stirling-PDF Desktop Application"
|
||||
authors = ["Stirling-PDF Contributors"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.2.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4"
|
||||
tauri = { version = "2.5.0", features = [] }
|
||||
tauri-plugin-log = "2.0.0-rc"
|
||||
tauri-plugin-shell = "2.1.0"
|
||||
tauri-plugin-fs = "2.0.0"
|
||||
tokio = { version = "1.0", features = ["time"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
|
||||
# macOS-specific dependencies for native file opening
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc = "0.2"
|
||||
cocoa = "0.24"
|
||||
once_cell = "1.19"
|
3
frontend/src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
15
frontend/src-tauri/capabilities/default.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
{
|
||||
"identifier": "fs:allow-read-file",
|
||||
"allow": [{ "path": "**" }]
|
||||
}
|
||||
]
|
||||
}
|
BIN
frontend/src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
frontend/src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
frontend/src-tauri/icons/16x16.png
Normal file
After Width: | Height: | Size: 829 B |
BIN
frontend/src-tauri/icons/192x192.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
frontend/src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/src-tauri/icons/64x64.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
frontend/src-tauri/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
frontend/src-tauri/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
frontend/src-tauri/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
frontend/src-tauri/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
frontend/src-tauri/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
frontend/src-tauri/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
frontend/src-tauri/icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
frontend/src-tauri/icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
frontend/src-tauri/icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
frontend/src-tauri/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
frontend/src-tauri/icons/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
frontend/src-tauri/icons/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 3.2 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 3.1 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 6.8 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 11 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 15 KiB |
BIN
frontend/src-tauri/icons/icon.icns
Normal file
BIN
frontend/src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
frontend/src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
frontend/src-tauri/icons/icon_orig.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
frontend/src-tauri/icons/mstile-144x144.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
frontend/src-tauri/icons/mstile-150x150.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
frontend/src-tauri/icons/mstile-310x150.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
frontend/src-tauri/icons/mstile-310x310.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
frontend/src-tauri/icons/mstile-70x70.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
1
frontend/src-tauri/icons/rainbow.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-rainbow" viewBox="0 0 16 16"><path d="M8 4.5a7 7 0 0 0-7 7 .5.5 0 0 1-1 0 8 8 0 1 1 16 0 .5.5 0 0 1-1 0 7 7 0 0 0-7-7zm0 2a5 5 0 0 0-5 5 .5.5 0 0 1-1 0 6 6 0 1 1 12 0 .5.5 0 0 1-1 0 5 5 0 0 0-5-5zm0 2a3 3 0 0 0-3 3 .5.5 0 0 1-1 0 4 4 0 1 1 8 0 .5.5 0 0 1-1 0 3 3 0 0 0-3-3zm0 2a1 1 0 0 0-1 1 .5.5 0 0 1-1 0 2 2 0 1 1 4 0 .5.5 0 0 1-1 0 1 1 0 0 0-1-1z"/></svg>
|
After Width: | Height: | Size: 455 B |
379
frontend/src-tauri/src/commands/backend.rs
Normal file
@ -0,0 +1,379 @@
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tauri::Manager;
|
||||
use std::sync::Mutex;
|
||||
use std::path::PathBuf;
|
||||
use crate::utils::add_log;
|
||||
|
||||
// Store backend process handle globally
|
||||
static BACKEND_PROCESS: Mutex<Option<tauri_plugin_shell::process::CommandChild>> = Mutex::new(None);
|
||||
static BACKEND_STARTING: Mutex<bool> = Mutex::new(false);
|
||||
|
||||
// Helper function to reset starting flag
|
||||
fn reset_starting_flag() {
|
||||
let mut starting_guard = BACKEND_STARTING.lock().unwrap();
|
||||
*starting_guard = false;
|
||||
}
|
||||
|
||||
// Check if backend is already running or starting
|
||||
fn check_backend_status() -> Result<(), String> {
|
||||
// Check if backend is already running
|
||||
{
|
||||
let process_guard = BACKEND_PROCESS.lock().unwrap();
|
||||
if process_guard.is_some() {
|
||||
add_log("⚠️ Backend process already running, skipping start".to_string());
|
||||
return Err("Backend already running".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Check and set starting flag to prevent multiple simultaneous starts
|
||||
{
|
||||
let mut starting_guard = BACKEND_STARTING.lock().unwrap();
|
||||
if *starting_guard {
|
||||
add_log("⚠️ Backend already starting, skipping duplicate start".to_string());
|
||||
return Err("Backend startup already in progress".to_string());
|
||||
}
|
||||
*starting_guard = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Find the bundled JRE and return the java executable path
|
||||
fn find_bundled_jre(resource_dir: &PathBuf) -> Result<PathBuf, String> {
|
||||
let jre_dir = resource_dir.join("runtime").join("jre");
|
||||
let java_executable = if cfg!(windows) {
|
||||
jre_dir.join("bin").join("java.exe")
|
||||
} else {
|
||||
jre_dir.join("bin").join("java")
|
||||
};
|
||||
|
||||
if !java_executable.exists() {
|
||||
let error_msg = format!("❌ Bundled JRE not found at: {:?}", java_executable);
|
||||
add_log(error_msg.clone());
|
||||
return Err(error_msg);
|
||||
}
|
||||
|
||||
add_log(format!("✅ Found bundled JRE: {:?}", java_executable));
|
||||
Ok(java_executable)
|
||||
}
|
||||
|
||||
// Find the Stirling-PDF JAR file
|
||||
fn find_stirling_jar(resource_dir: &PathBuf) -> Result<PathBuf, String> {
|
||||
let libs_dir = resource_dir.join("libs");
|
||||
let mut jar_files: Vec<_> = std::fs::read_dir(&libs_dir)
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("Failed to read libs directory: {}. Make sure the JAR is copied to libs/", e);
|
||||
add_log(error_msg.clone());
|
||||
error_msg
|
||||
})?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| {
|
||||
let path = entry.path();
|
||||
// Match any .jar file containing "stirling-pdf" (case-insensitive)
|
||||
path.extension().and_then(|s| s.to_str()).map(|ext| ext.eq_ignore_ascii_case("jar")).unwrap_or(false)
|
||||
&& path.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
.map(|name| name.to_ascii_lowercase().contains("stirling-pdf"))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if jar_files.is_empty() {
|
||||
let error_msg = "No Stirling-PDF JAR found in libs directory.".to_string();
|
||||
add_log(error_msg.clone());
|
||||
return Err(error_msg);
|
||||
}
|
||||
|
||||
// Sort by filename to get the latest version (case-insensitive)
|
||||
jar_files.sort_by(|a, b| {
|
||||
let name_a = a.file_name().to_string_lossy().to_ascii_lowercase();
|
||||
let name_b = b.file_name().to_string_lossy().to_ascii_lowercase();
|
||||
name_b.cmp(&name_a) // Reverse order to get latest first
|
||||
});
|
||||
|
||||
let jar_path = jar_files[0].path();
|
||||
add_log(format!("📋 Selected JAR: {:?}", jar_path.file_name().unwrap()));
|
||||
Ok(jar_path)
|
||||
}
|
||||
|
||||
// Normalize path to remove Windows UNC prefix
|
||||
fn normalize_path(path: &PathBuf) -> PathBuf {
|
||||
if cfg!(windows) {
|
||||
let path_str = path.to_string_lossy();
|
||||
if path_str.starts_with(r"\\?\") {
|
||||
PathBuf::from(&path_str[4..]) // Remove \\?\ prefix
|
||||
} else {
|
||||
path.clone()
|
||||
}
|
||||
} else {
|
||||
path.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// Create, configure and run the Java command to run Stirling-PDF JAR
|
||||
fn run_stirling_pdf_jar(app: &tauri::AppHandle, java_path: &PathBuf, jar_path: &PathBuf) -> Result<(), String> {
|
||||
// Get platform-specific application data directory for Tauri mode
|
||||
let app_data_dir = if cfg!(target_os = "macos") {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
PathBuf::from(home).join("Library").join("Application Support").join("Stirling-PDF")
|
||||
} else if cfg!(target_os = "windows") {
|
||||
let appdata = std::env::var("APPDATA").unwrap_or_else(|_| std::env::temp_dir().to_string_lossy().to_string());
|
||||
PathBuf::from(appdata).join("Stirling-PDF")
|
||||
} else {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
PathBuf::from(home).join(".config").join("Stirling-PDF")
|
||||
};
|
||||
|
||||
// Create subdirectories for different purposes
|
||||
let config_dir = app_data_dir.join("configs");
|
||||
let log_dir = app_data_dir.join("logs");
|
||||
let work_dir = app_data_dir.join("workspace");
|
||||
|
||||
// Create all necessary directories
|
||||
std::fs::create_dir_all(&app_data_dir).ok();
|
||||
std::fs::create_dir_all(&log_dir).ok();
|
||||
std::fs::create_dir_all(&work_dir).ok();
|
||||
std::fs::create_dir_all(&config_dir).ok();
|
||||
|
||||
add_log(format!("📁 App data directory: {}", app_data_dir.display()));
|
||||
add_log(format!("📁 Log directory: {}", log_dir.display()));
|
||||
add_log(format!("📁 Working directory: {}", work_dir.display()));
|
||||
add_log(format!("📁 Config directory: {}", config_dir.display()));
|
||||
|
||||
// Define all Java options with Tauri-specific paths
|
||||
let log_path_option = format!("-Dlogging.file.path={}", log_dir.display());
|
||||
|
||||
let java_options = vec![
|
||||
"-Xmx2g",
|
||||
"-DBROWSER_OPEN=false",
|
||||
"-DSTIRLING_PDF_DESKTOP_UI=false",
|
||||
"-DSTIRLING_PDF_TAURI_MODE=true",
|
||||
&log_path_option,
|
||||
"-Dlogging.file.name=stirling-pdf.log",
|
||||
"-jar",
|
||||
jar_path.to_str().unwrap()
|
||||
];
|
||||
|
||||
// Log the equivalent command for external testing
|
||||
let java_command = format!(
|
||||
"TAURI_PARENT_PID={} \"{}\" {}",
|
||||
std::process::id(),
|
||||
java_path.display(),
|
||||
java_options.join(" ")
|
||||
);
|
||||
add_log(format!("🔧 Equivalent command: {}", java_command));
|
||||
add_log(format!("📁 Backend logs will be in: {}", log_dir.display()));
|
||||
|
||||
// Additional macOS-specific checks
|
||||
if cfg!(target_os = "macos") {
|
||||
// Check if java executable has execute permissions
|
||||
if let Ok(metadata) = std::fs::metadata(java_path) {
|
||||
let permissions = metadata.permissions();
|
||||
add_log(format!("🔍 Java executable permissions: {:?}", permissions));
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mode = permissions.mode();
|
||||
add_log(format!("🔍 Java executable mode: 0o{:o}", mode));
|
||||
if mode & 0o111 == 0 {
|
||||
add_log("⚠️ Java executable may not have execute permissions".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we can read the JAR file
|
||||
if let Ok(metadata) = std::fs::metadata(jar_path) {
|
||||
add_log(format!("📦 JAR file size: {} bytes", metadata.len()));
|
||||
} else {
|
||||
add_log("⚠️ Cannot read JAR file metadata".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let sidecar_command = app
|
||||
.shell()
|
||||
.command(java_path.to_str().unwrap())
|
||||
.args(java_options)
|
||||
.current_dir(&work_dir) // Set working directory to writable location
|
||||
.env("TAURI_PARENT_PID", std::process::id().to_string())
|
||||
.env("STIRLING_PDF_CONFIG_DIR", config_dir.to_str().unwrap())
|
||||
.env("STIRLING_PDF_LOG_DIR", log_dir.to_str().unwrap())
|
||||
.env("STIRLING_PDF_WORK_DIR", work_dir.to_str().unwrap());
|
||||
|
||||
add_log("⚙️ Starting backend with bundled JRE...".to_string());
|
||||
|
||||
let (rx, child) = sidecar_command
|
||||
.spawn()
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("❌ Failed to spawn sidecar: {}", e);
|
||||
add_log(error_msg.clone());
|
||||
error_msg
|
||||
})?;
|
||||
|
||||
// Store the process handle
|
||||
{
|
||||
let mut process_guard = BACKEND_PROCESS.lock().unwrap();
|
||||
*process_guard = Some(child);
|
||||
}
|
||||
|
||||
add_log("✅ Backend started with bundled JRE, monitoring output...".to_string());
|
||||
|
||||
// Start monitoring output
|
||||
monitor_backend_output(rx);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Monitor backend output in a separate task
|
||||
fn monitor_backend_output(mut rx: tauri::async_runtime::Receiver<tauri_plugin_shell::process::CommandEvent>) {
|
||||
tokio::spawn(async move {
|
||||
let mut _startup_detected = false;
|
||||
let mut error_count = 0;
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
tauri_plugin_shell::process::CommandEvent::Stdout(output) => {
|
||||
let output_str = String::from_utf8_lossy(&output);
|
||||
add_log(format!("📤 Backend: {}", output_str));
|
||||
|
||||
// Look for startup indicators
|
||||
if output_str.contains("Started SPDFApplication") ||
|
||||
output_str.contains("Navigate to "){
|
||||
_startup_detected = true;
|
||||
add_log(format!("🎉 Backend startup detected: {}", output_str));
|
||||
}
|
||||
|
||||
// Look for port binding
|
||||
if output_str.contains("8080") {
|
||||
add_log(format!("🔌 Port 8080 related output: {}", output_str));
|
||||
}
|
||||
}
|
||||
tauri_plugin_shell::process::CommandEvent::Stderr(output) => {
|
||||
let output_str = String::from_utf8_lossy(&output);
|
||||
add_log(format!("📥 Backend Error: {}", output_str));
|
||||
|
||||
// Look for error indicators
|
||||
if output_str.contains("ERROR") || output_str.contains("Exception") || output_str.contains("FATAL") {
|
||||
error_count += 1;
|
||||
add_log(format!("⚠️ Backend error #{}: {}", error_count, output_str));
|
||||
}
|
||||
|
||||
// Look for specific common issues
|
||||
if output_str.contains("Address already in use") {
|
||||
add_log("🚨 CRITICAL: Port 8080 is already in use by another process!".to_string());
|
||||
}
|
||||
if output_str.contains("java.lang.ClassNotFoundException") {
|
||||
add_log("🚨 CRITICAL: Missing Java dependencies!".to_string());
|
||||
}
|
||||
if output_str.contains("java.io.FileNotFoundException") {
|
||||
add_log("🚨 CRITICAL: Required file not found!".to_string());
|
||||
}
|
||||
}
|
||||
tauri_plugin_shell::process::CommandEvent::Error(error) => {
|
||||
add_log(format!("❌ Backend process error: {}", error));
|
||||
}
|
||||
tauri_plugin_shell::process::CommandEvent::Terminated(payload) => {
|
||||
add_log(format!("💀 Backend terminated with code: {:?}", payload.code));
|
||||
if let Some(code) = payload.code {
|
||||
match code {
|
||||
0 => println!("✅ Process terminated normally"),
|
||||
1 => println!("❌ Process terminated with generic error"),
|
||||
2 => println!("❌ Process terminated due to misuse"),
|
||||
126 => println!("❌ Command invoked cannot execute"),
|
||||
127 => println!("❌ Command not found"),
|
||||
128 => println!("❌ Invalid exit argument"),
|
||||
130 => println!("❌ Process terminated by Ctrl+C"),
|
||||
_ => println!("❌ Process terminated with code: {}", code),
|
||||
}
|
||||
}
|
||||
// Clear the stored process handle
|
||||
let mut process_guard = BACKEND_PROCESS.lock().unwrap();
|
||||
*process_guard = None;
|
||||
}
|
||||
_ => {
|
||||
println!("🔍 Unknown command event: {:?}", event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if error_count > 0 {
|
||||
println!("⚠️ Backend process ended with {} errors detected", error_count);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Command to start the backend with bundled JRE
|
||||
#[tauri::command]
|
||||
pub async fn start_backend(app: tauri::AppHandle) -> Result<String, String> {
|
||||
add_log("🚀 start_backend() called - Attempting to start backend with bundled JRE...".to_string());
|
||||
|
||||
// Check if backend is already running or starting
|
||||
if let Err(msg) = check_backend_status() {
|
||||
return Ok(msg);
|
||||
}
|
||||
|
||||
// Use Tauri's resource API to find the bundled JRE and JAR
|
||||
let resource_dir = app.path().resource_dir().map_err(|e| {
|
||||
let error_msg = format!("❌ Failed to get resource directory: {}", e);
|
||||
add_log(error_msg.clone());
|
||||
reset_starting_flag();
|
||||
error_msg
|
||||
})?;
|
||||
|
||||
add_log(format!("🔍 Resource directory: {:?}", resource_dir));
|
||||
|
||||
// Find the bundled JRE
|
||||
let java_executable = find_bundled_jre(&resource_dir).map_err(|e| {
|
||||
reset_starting_flag();
|
||||
e
|
||||
})?;
|
||||
|
||||
// Find the Stirling-PDF JAR
|
||||
let jar_path = find_stirling_jar(&resource_dir).map_err(|e| {
|
||||
reset_starting_flag();
|
||||
e
|
||||
})?;
|
||||
|
||||
// Normalize the paths to remove Windows UNC prefix
|
||||
let normalized_java_path = normalize_path(&java_executable);
|
||||
let normalized_jar_path = normalize_path(&jar_path);
|
||||
|
||||
add_log(format!("📦 Found JAR file: {:?}", jar_path));
|
||||
add_log(format!("📦 Normalized JAR path: {:?}", normalized_jar_path));
|
||||
add_log(format!("📦 Normalized Java path: {:?}", normalized_java_path));
|
||||
|
||||
// Create and start the Java command
|
||||
run_stirling_pdf_jar(&app, &normalized_java_path, &normalized_jar_path).map_err(|e| {
|
||||
reset_starting_flag();
|
||||
e
|
||||
})?;
|
||||
|
||||
// Wait for the backend to start
|
||||
println!("⏳ Waiting for backend startup...");
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10000)).await;
|
||||
|
||||
// Reset the starting flag since startup is complete
|
||||
reset_starting_flag();
|
||||
add_log("✅ Backend startup sequence completed, starting flag cleared".to_string());
|
||||
|
||||
Ok("Backend startup initiated successfully with bundled JRE".to_string())
|
||||
}
|
||||
|
||||
// Cleanup function to stop backend on app exit
|
||||
pub fn cleanup_backend() {
|
||||
let mut process_guard = BACKEND_PROCESS.lock().unwrap();
|
||||
if let Some(child) = process_guard.take() {
|
||||
let pid = child.pid();
|
||||
add_log(format!("🧹 App shutting down, cleaning up backend process (PID: {})", pid));
|
||||
|
||||
match child.kill() {
|
||||
Ok(_) => {
|
||||
add_log(format!("✅ Backend process (PID: {}) terminated during cleanup", pid));
|
||||
}
|
||||
Err(e) => {
|
||||
add_log(format!("❌ Failed to terminate backend process during cleanup: {}", e));
|
||||
println!("❌ Failed to terminate backend process during cleanup: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
48
frontend/src-tauri/src/commands/files.rs
Normal file
@ -0,0 +1,48 @@
|
||||
use crate::utils::add_log;
|
||||
use std::sync::Mutex;
|
||||
|
||||
// Store the opened file path globally
|
||||
static OPENED_FILE: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
// Set the opened file path (called by macOS file open events)
|
||||
pub fn set_opened_file(file_path: String) {
|
||||
let mut opened_file = OPENED_FILE.lock().unwrap();
|
||||
*opened_file = Some(file_path.clone());
|
||||
add_log(format!("📂 File opened via file open event: {}", file_path));
|
||||
}
|
||||
|
||||
// Command to get opened file path (if app was launched with a file)
|
||||
#[tauri::command]
|
||||
pub async fn get_opened_file() -> Result<Option<String>, String> {
|
||||
// First check if we have a file from macOS file open events
|
||||
{
|
||||
let opened_file = OPENED_FILE.lock().unwrap();
|
||||
if let Some(ref file_path) = *opened_file {
|
||||
add_log(format!("📂 Returning stored opened file: {}", file_path));
|
||||
return Ok(Some(file_path.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to command line arguments (Windows/Linux)
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
// Look for a PDF file argument (skip the first arg which is the executable)
|
||||
for arg in args.iter().skip(1) {
|
||||
if arg.ends_with(".pdf") && std::path::Path::new(arg).exists() {
|
||||
add_log(format!("📂 PDF file opened via command line: {}", arg));
|
||||
return Ok(Some(arg.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// Command to clear the opened file (after processing)
|
||||
#[tauri::command]
|
||||
pub async fn clear_opened_file() -> Result<(), String> {
|
||||
let mut opened_file = OPENED_FILE.lock().unwrap();
|
||||
*opened_file = None;
|
||||
add_log("📂 Cleared opened file".to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
36
frontend/src-tauri/src/commands/health.rs
Normal file
@ -0,0 +1,36 @@
|
||||
// Command to check if backend is healthy
|
||||
#[tauri::command]
|
||||
pub async fn check_backend_health() -> Result<bool, String> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
|
||||
|
||||
match client.get("http://localhost:8080/api/v1/info/status").send().await {
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
match response.text().await {
|
||||
Ok(_body) => {
|
||||
println!("✅ Backend health check successful");
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("⚠️ Failed to read health response: {}", e);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("⚠️ Health check failed with status: {}", status);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// Only log connection errors if they're not the common "connection refused" during startup
|
||||
if !e.to_string().contains("connection refused") && !e.to_string().contains("No connection could be made") {
|
||||
println!("❌ Health check error: {}", e);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
7
frontend/src-tauri/src/commands/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub mod backend;
|
||||
pub mod health;
|
||||
pub mod files;
|
||||
|
||||
pub use backend::{start_backend, cleanup_backend};
|
||||
pub use health::check_backend_health;
|
||||
pub use files::{get_opened_file, clear_opened_file, set_opened_file};
|
189
frontend/src-tauri/src/file_handler.rs
Normal file
@ -0,0 +1,189 @@
|
||||
/// Multi-platform file opening handler
|
||||
///
|
||||
/// This module provides unified file opening support across platforms:
|
||||
/// - macOS: Uses native NSApplication delegate (proper Apple Events)
|
||||
/// - Windows/Linux: Uses command line arguments (fallback approach)
|
||||
/// - All platforms: Runtime event handling via Tauri events
|
||||
|
||||
use crate::utils::add_log;
|
||||
use crate::commands::set_opened_file;
|
||||
use tauri::AppHandle;
|
||||
|
||||
|
||||
/// Initialize file handling for the current platform
|
||||
pub fn initialize_file_handler(app: &AppHandle<tauri::Wry>) {
|
||||
add_log("🔧 Initializing file handler...".to_string());
|
||||
|
||||
// Platform-specific initialization
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
add_log("🍎 Using macOS native file handler".to_string());
|
||||
macos_native::register_open_file_handler(app);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
add_log("🖥️ Using command line argument file handler".to_string());
|
||||
let _ = app; // Suppress unused variable warning
|
||||
}
|
||||
|
||||
// Universal: Check command line arguments (works on all platforms)
|
||||
check_command_line_args();
|
||||
}
|
||||
|
||||
/// Early initialization for macOS delegate registration
|
||||
pub fn early_init() {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
add_log("🔄 Early macOS initialization...".to_string());
|
||||
macos_native::register_delegate_early();
|
||||
}
|
||||
}
|
||||
|
||||
/// Check command line arguments for file paths (universal fallback)
|
||||
fn check_command_line_args() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
add_log(format!("🔍 DEBUG: All command line args: {:?}", args));
|
||||
|
||||
// Check command line arguments for file opening
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
add_log(format!("🔍 DEBUG: Arg {}: {}", i, arg));
|
||||
if i > 0 && arg.ends_with(".pdf") && std::path::Path::new(arg).exists() {
|
||||
add_log(format!("📂 File argument detected: {}", arg));
|
||||
set_opened_file(arg.clone());
|
||||
break; // Only handle the first PDF file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle runtime file open events (for future single-instance support)
|
||||
#[allow(dead_code)]
|
||||
pub fn handle_runtime_file_open(file_path: String) {
|
||||
if file_path.ends_with(".pdf") && std::path::Path::new(&file_path).exists() {
|
||||
add_log(format!("📂 Runtime file open: {}", file_path));
|
||||
set_opened_file(file_path);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos_native {
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
use objc::runtime::{Class, Object, Sel};
|
||||
use cocoa::appkit::NSApplication;
|
||||
use cocoa::base::{id, nil};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::Mutex;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
use crate::utils::add_log;
|
||||
use crate::commands::set_opened_file;
|
||||
|
||||
// Static app handle storage
|
||||
static APP_HANDLE: Lazy<Mutex<Option<AppHandle<tauri::Wry>>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
// Store files opened during launch
|
||||
static LAUNCH_FILES: Lazy<Mutex<Vec<String>>> = Lazy::new(|| Mutex::new(Vec::new()));
|
||||
|
||||
|
||||
extern "C" fn open_files(_self: &Object, _cmd: Sel, _sender: id, filenames: id) {
|
||||
unsafe {
|
||||
add_log(format!("📂 macOS native openFiles event called"));
|
||||
|
||||
// filenames is an NSArray of NSString objects
|
||||
let count: usize = msg_send![filenames, count];
|
||||
add_log(format!("📂 Number of files to open: {}", count));
|
||||
|
||||
for i in 0..count {
|
||||
let filename: id = msg_send![filenames, objectAtIndex: i];
|
||||
let cstr = {
|
||||
let bytes: *const std::os::raw::c_char = msg_send![filename, UTF8String];
|
||||
std::ffi::CStr::from_ptr(bytes)
|
||||
};
|
||||
|
||||
if let Ok(path) = cstr.to_str() {
|
||||
add_log(format!("📂 macOS file open: {}", path));
|
||||
if path.ends_with(".pdf") {
|
||||
// Always set the opened file for command-line interface
|
||||
set_opened_file(path.to_string());
|
||||
|
||||
if let Some(app) = APP_HANDLE.lock().unwrap().as_ref() {
|
||||
// App is running, emit event immediately
|
||||
add_log(format!("✅ App running, emitting file event: {}", path));
|
||||
let _ = app.emit("macos://open-file", path.to_string());
|
||||
} else {
|
||||
// App not ready yet, store for later processing
|
||||
add_log(format!("🚀 App not ready, storing file for later: {}", path));
|
||||
LAUNCH_FILES.lock().unwrap().push(path.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register the delegate immediately when the module loads
|
||||
pub fn register_delegate_early() {
|
||||
add_log("🔧 Registering macOS delegate early...".to_string());
|
||||
|
||||
unsafe {
|
||||
let ns_app = NSApplication::sharedApplication(nil);
|
||||
|
||||
// Check if there's already a delegate
|
||||
let existing_delegate: id = msg_send![ns_app, delegate];
|
||||
if existing_delegate != nil {
|
||||
add_log("⚠️ Tauri already has an NSApplication delegate, trying to extend it...".to_string());
|
||||
|
||||
// Try to add our method to the existing delegate's class
|
||||
let delegate_class: id = msg_send![existing_delegate, class];
|
||||
let class_name: *const std::os::raw::c_char = msg_send![delegate_class, name];
|
||||
let class_name_str = std::ffi::CStr::from_ptr(class_name).to_string_lossy();
|
||||
add_log(format!("🔍 Existing delegate class: {}", class_name_str));
|
||||
|
||||
// This approach won't work with existing classes, so let's try a different method
|
||||
// We'll use method swizzling or create a new delegate that forwards to the old one
|
||||
add_log("🔄 Will try alternative approach...".to_string());
|
||||
}
|
||||
|
||||
let delegate_class = Class::get("StirlingAppDelegate").unwrap_or_else(|| {
|
||||
let superclass = class!(NSObject);
|
||||
let mut decl = objc::declare::ClassDecl::new("StirlingAppDelegate", superclass).unwrap();
|
||||
|
||||
// Add file opening delegate method (modern plural version)
|
||||
decl.add_method(
|
||||
sel!(application:openFiles:),
|
||||
open_files as extern "C" fn(&Object, Sel, id, id)
|
||||
);
|
||||
|
||||
decl.register()
|
||||
});
|
||||
|
||||
let delegate: id = msg_send![delegate_class, new];
|
||||
let _: () = msg_send![ns_app, setDelegate:delegate];
|
||||
}
|
||||
|
||||
add_log("✅ macOS delegate registered early".to_string());
|
||||
}
|
||||
|
||||
pub fn register_open_file_handler(app: &AppHandle<tauri::Wry>) {
|
||||
add_log("🔧 Connecting app handle to file handler...".to_string());
|
||||
|
||||
// Store the app handle
|
||||
*APP_HANDLE.lock().unwrap() = Some(app.clone());
|
||||
|
||||
// Process any files that were opened during launch
|
||||
let launch_files = {
|
||||
let mut files = LAUNCH_FILES.lock().unwrap();
|
||||
let result = files.clone();
|
||||
files.clear();
|
||||
result
|
||||
};
|
||||
|
||||
for file_path in launch_files {
|
||||
add_log(format!("📂 Processing stored launch file: {}", file_path));
|
||||
set_opened_file(file_path.clone());
|
||||
let _ = app.emit("macos://open-file", file_path);
|
||||
}
|
||||
|
||||
add_log("✅ macOS file handler connected successfully".to_string());
|
||||
}
|
||||
}
|
65
frontend/src-tauri/src/lib.rs
Normal file
@ -0,0 +1,65 @@
|
||||
use tauri::{RunEvent, WindowEvent, Emitter};
|
||||
|
||||
mod utils;
|
||||
mod commands;
|
||||
mod file_handler;
|
||||
|
||||
use commands::{start_backend, check_backend_health, get_opened_file, clear_opened_file, cleanup_backend, set_opened_file};
|
||||
use utils::{add_log, get_tauri_logs};
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// Initialize file handler early for macOS
|
||||
file_handler::early_init();
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.setup(|app| {
|
||||
add_log("🚀 Tauri app setup started".to_string());
|
||||
|
||||
// Initialize platform-specific file handler
|
||||
file_handler::initialize_file_handler(&app.handle());
|
||||
|
||||
add_log("🔍 DEBUG: Setup completed".to_string());
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![start_backend, check_backend_health, get_opened_file, clear_opened_file, get_tauri_logs])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
.run(|app_handle, event| {
|
||||
match event {
|
||||
RunEvent::ExitRequested { .. } => {
|
||||
add_log("🔄 App exit requested, cleaning up...".to_string());
|
||||
cleanup_backend();
|
||||
// Use Tauri's built-in cleanup
|
||||
app_handle.cleanup_before_exit();
|
||||
}
|
||||
RunEvent::WindowEvent { event: WindowEvent::CloseRequested {.. }, .. } => {
|
||||
add_log("🔄 Window close requested, cleaning up...".to_string());
|
||||
cleanup_backend();
|
||||
// Allow the window to close
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
RunEvent::Opened { urls } => {
|
||||
add_log(format!("📂 Tauri file opened event: {:?}", urls));
|
||||
for url in urls {
|
||||
let url_str = url.as_str();
|
||||
if url_str.starts_with("file://") {
|
||||
let file_path = url_str.strip_prefix("file://").unwrap_or(url_str);
|
||||
if file_path.ends_with(".pdf") {
|
||||
add_log(format!("📂 Processing opened PDF: {}", file_path));
|
||||
set_opened_file(file_path.to_string());
|
||||
let _ = app_handle.emit("macos://open-file", file_path.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Only log unhandled events in debug mode to reduce noise
|
||||
// #[cfg(debug_assertions)]
|
||||
// add_log(format!("🔍 DEBUG: Unhandled event: {:?}", event));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
6
frontend/src-tauri/src/main.rs
Normal file
@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
app_lib::run();
|
||||
}
|
90
frontend/src-tauri/src/utils/logging.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use std::sync::Mutex;
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Store backend logs globally
|
||||
static BACKEND_LOGS: Mutex<VecDeque<String>> = Mutex::new(VecDeque::new());
|
||||
|
||||
// Get platform-specific log directory
|
||||
fn get_log_directory() -> PathBuf {
|
||||
if cfg!(target_os = "macos") {
|
||||
// macOS: ~/Library/Logs/Stirling-PDF
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
PathBuf::from(home).join("Library").join("Logs").join("Stirling-PDF")
|
||||
} else if cfg!(target_os = "windows") {
|
||||
// Windows: %APPDATA%\Stirling-PDF\logs
|
||||
let appdata = std::env::var("APPDATA").unwrap_or_else(|_| std::env::temp_dir().to_string_lossy().to_string());
|
||||
PathBuf::from(appdata).join("Stirling-PDF").join("logs")
|
||||
} else {
|
||||
// Linux: ~/.config/Stirling-PDF/logs
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
PathBuf::from(home).join(".config").join("Stirling-PDF").join("logs")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to add log entry
|
||||
pub fn add_log(message: String) {
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
let log_entry = format!("{}: {}", timestamp, message);
|
||||
|
||||
// Add to memory logs
|
||||
{
|
||||
let mut logs = BACKEND_LOGS.lock().unwrap();
|
||||
logs.push_back(log_entry.clone());
|
||||
// Keep only last 100 log entries
|
||||
if logs.len() > 100 {
|
||||
logs.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
// Write to file
|
||||
write_to_log_file(&log_entry);
|
||||
|
||||
// Remove trailing newline if present
|
||||
let clean_message = message.trim_end_matches('\n').to_string();
|
||||
println!("{}", clean_message); // Also print to console
|
||||
}
|
||||
|
||||
// Write log entry to file
|
||||
fn write_to_log_file(log_entry: &str) {
|
||||
let log_dir = get_log_directory();
|
||||
if let Err(e) = std::fs::create_dir_all(&log_dir) {
|
||||
eprintln!("Failed to create log directory: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
let log_file = log_dir.join("tauri-backend.log");
|
||||
|
||||
match OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_file)
|
||||
{
|
||||
Ok(mut file) => {
|
||||
if let Err(e) = writeln!(file, "{}", log_entry) {
|
||||
eprintln!("Failed to write to log file: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to open log file {:?}: {}", log_file, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get current logs for debugging
|
||||
pub fn get_logs() -> Vec<String> {
|
||||
let logs = BACKEND_LOGS.lock().unwrap();
|
||||
logs.iter().cloned().collect()
|
||||
}
|
||||
|
||||
// Command to get logs from frontend
|
||||
#[tauri::command]
|
||||
pub async fn get_tauri_logs() -> Result<Vec<String>, String> {
|
||||
Ok(get_logs())
|
||||
}
|
3
frontend/src-tauri/src/utils/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod logging;
|
||||
|
||||
pub use logging::{add_log, get_tauri_logs};
|
14
frontend/src-tauri/stirling-pdf.desktop
Normal file
@ -0,0 +1,14 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=Stirling-PDF
|
||||
Comment=Locally hosted web application that allows you to perform various operations on PDF files
|
||||
Icon={{icon}}
|
||||
Terminal=false
|
||||
MimeType=application/pdf;
|
||||
Categories=Office;Graphics;Utility;
|
||||
Actions=open-file;
|
||||
|
||||
[Desktop Action open-file]
|
||||
Name=Open PDF File
|
||||
Exec=/usr/bin/stirling-pdf %F
|
63
frontend/src-tauri/tauri.conf.json
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "Stirling-PDF",
|
||||
"version": "2.0.0",
|
||||
"identifier": "stirling.pdf.dev",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Stirling-PDF",
|
||||
"width": 1024,
|
||||
"height": 768,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["deb", "rpm", "dmg", "msi"],
|
||||
"icon": [
|
||||
"icons/icon.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico",
|
||||
"icons/16x16.png",
|
||||
"icons/32x32.png",
|
||||
"icons/64x64.png",
|
||||
"icons/128x128.png",
|
||||
"icons/192x192.png"
|
||||
],
|
||||
"resources": [
|
||||
"libs/*.jar",
|
||||
"runtime/jre/**/*"
|
||||
],
|
||||
"fileAssociations": [
|
||||
{
|
||||
"ext": ["pdf"],
|
||||
"name": "PDF Document",
|
||||
"description": "Open PDF files with Stirling-PDF",
|
||||
"role": "Editor",
|
||||
"mimeType": "application/pdf"
|
||||
}
|
||||
],
|
||||
"linux": {
|
||||
"deb": {
|
||||
"desktopTemplate": "stirling-pdf.desktop"
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"shell": {
|
||||
"open": true
|
||||
},
|
||||
"fs": {
|
||||
"requireLiteralLeadingDot": false
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +1,56 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider';
|
||||
import { FileContextProvider } from './contexts/FileContext';
|
||||
import HomePage from './pages/HomePage';
|
||||
import { useOpenedFile } from './hooks/useOpenedFile';
|
||||
import { useBackendInitializer } from './hooks/useBackendInitializer';
|
||||
import { fileOpenService } from './services/fileOpenService';
|
||||
|
||||
// Import global styles
|
||||
import './styles/tailwind.css';
|
||||
import './index.css';
|
||||
|
||||
import { BackendHealthIndicator } from './components/BackendHealthIndicator';
|
||||
|
||||
export default function App() {
|
||||
|
||||
// Initialize backend on app startup
|
||||
useBackendInitializer();
|
||||
|
||||
// Handle file opened with app (Tauri mode)
|
||||
const { openedFilePath, loading: openedFileLoading } = useOpenedFile();
|
||||
const [openedFile, setOpenedFile] = useState<File | null>(null);
|
||||
|
||||
// Load opened file once when path is available
|
||||
useEffect(() => {
|
||||
if (openedFilePath && !openedFileLoading) {
|
||||
const loadOpenedFile = async () => {
|
||||
try {
|
||||
const fileData = await fileOpenService.readFileAsArrayBuffer(openedFilePath);
|
||||
if (fileData) {
|
||||
// Create a File object from the ArrayBuffer
|
||||
const file = new File([fileData.arrayBuffer], fileData.fileName, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
setOpenedFile(file);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load opened file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadOpenedFile();
|
||||
}
|
||||
}, [openedFilePath, openedFileLoading]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackendHealthIndicator className="absolute top-3 left-3 z-10" />
|
||||
<RainbowThemeProvider>
|
||||
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
||||
<HomePage />
|
||||
<HomePage openedFile={openedFile} />
|
||||
</FileContextProvider>
|
||||
</RainbowThemeProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
28
frontend/src/components/BackendHealthIndicator.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import '../index.css';
|
||||
import { useBackendHealth } from '../hooks/useBackendHealth';
|
||||
|
||||
interface BackendHealthIndicatorProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const BackendHealthIndicator: React.FC<BackendHealthIndicatorProps> = ({
|
||||
className = ''
|
||||
}) => {
|
||||
const { isHealthy, isChecking, error, checkHealth } = useBackendHealth();
|
||||
|
||||
let statusColor = 'bg-red-500'; // offline
|
||||
if (isChecking || (!isHealthy && error === 'Backend starting up...')) {
|
||||
statusColor = 'bg-yellow-500'; // starting/checking
|
||||
} else if (isHealthy) {
|
||||
statusColor = 'bg-green-500'; // online
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-2xs h-2xs ${statusColor} rounded-full cursor-pointer ${isChecking ? 'animate-pulse' : ''} ${className}`}
|
||||
onClick={checkHealth}
|
||||
title={isHealthy ? 'Backend Online' : isChecking ? 'Checking...' : 'Backend Offline'}
|
||||
/>
|
||||
);
|
||||
};
|
@ -5,6 +5,7 @@ import { useFileContext } from '../../../contexts/FileContext';
|
||||
import { FileOperation } from '../../../types/fileContext';
|
||||
import { zipFileService } from '../../../services/zipFileService';
|
||||
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
|
||||
import {makeApiUrl} from '../../../utils/api';
|
||||
|
||||
export interface CompressParameters {
|
||||
compressionLevel: number;
|
||||
@ -184,7 +185,7 @@ export const useCompressOperation = (): CompressOperationHook => {
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
||||
const response = await axios.post(makeApiUrl(endpoint), formData, { responseType: "blob" });
|
||||
|
||||
// Determine the correct content type from the response
|
||||
const contentType = response.headers['content-type'] || 'application/zip';
|
||||
|
@ -7,6 +7,7 @@ import { zipFileService } from '../../../services/zipFileService';
|
||||
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
|
||||
import { SplitParameters } from '../../../components/tools/split/SplitSettings';
|
||||
import { SPLIT_MODES, ENDPOINTS, type SplitMode } from '../../../constants/splitConstants';
|
||||
import { makeApiUrl } from '../../../utils/api';
|
||||
|
||||
export interface SplitOperationHook {
|
||||
executeOperation: (
|
||||
@ -184,7 +185,7 @@ export const useSplitOperation = (): SplitOperationHook => {
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
||||
const response = await axios.post(makeApiUrl(endpoint), formData, { responseType: "blob" });
|
||||
const blob = new Blob([response.data], { type: "application/zip" });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { makeApiUrl } from '../utils/api';
|
||||
|
||||
export interface AppConfig {
|
||||
baseUrl?: string;
|
||||
@ -46,7 +47,7 @@ export function useAppConfig(): UseAppConfigReturn {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch('/api/v1/config/app-config');
|
||||
const response = await fetch(makeApiUrl('/api/v1/config/app-config'));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch config: ${response.status} ${response.statusText}`);
|
||||
|
124
frontend/src/hooks/useBackendHealth.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { makeApiUrl } from '../utils/api';
|
||||
|
||||
export interface BackendHealthState {
|
||||
isHealthy: boolean;
|
||||
isChecking: boolean;
|
||||
lastChecked: Date | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export const useBackendHealth = (checkInterval: number = 2000) => {
|
||||
const [healthState, setHealthState] = useState<BackendHealthState>({
|
||||
isHealthy: false,
|
||||
isChecking: false,
|
||||
lastChecked: null,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const [startupTime] = useState<Date>(new Date());
|
||||
const [attemptCount, setAttemptCount] = useState<number>(0);
|
||||
|
||||
const checkHealth = useCallback(async () => {
|
||||
setHealthState(prev => ({ ...prev, isChecking: true, error: null }));
|
||||
setAttemptCount(prev => prev + 1);
|
||||
|
||||
try {
|
||||
// Direct HTTP call to backend health endpoint using api.ts
|
||||
const healthUrl = makeApiUrl('/api/v1/info/status');
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
|
||||
|
||||
const response = await fetch(healthUrl, {
|
||||
method: 'GET',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const isHealthy = response.ok;
|
||||
|
||||
setHealthState({
|
||||
isHealthy,
|
||||
isChecking: false,
|
||||
lastChecked: new Date(),
|
||||
error: null,
|
||||
});
|
||||
|
||||
if (isHealthy) {
|
||||
// Log success message if this is the first successful check after failures
|
||||
if (attemptCount > 0) {
|
||||
const now = new Date();
|
||||
const timeSinceStartup = now.getTime() - startupTime.getTime();
|
||||
console.log('✅ Backend health check successful:', {
|
||||
timeSinceStartup: Math.round(timeSinceStartup / 1000) + 's',
|
||||
attemptsBeforeSuccess: attemptCount,
|
||||
});
|
||||
}
|
||||
setAttemptCount(0); // Reset attempt count on success
|
||||
}
|
||||
} catch (error) {
|
||||
const now = new Date();
|
||||
const timeSinceStartup = now.getTime() - startupTime.getTime();
|
||||
const isWithinStartupPeriod = timeSinceStartup < 60000; // 60 seconds
|
||||
|
||||
let errorMessage: string;
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.name === 'AbortError') {
|
||||
errorMessage = isWithinStartupPeriod ? 'Backend starting up...' : 'Health check timeout';
|
||||
} else if (error.message.includes('fetch')) {
|
||||
errorMessage = isWithinStartupPeriod ? 'Backend starting up...' : 'Cannot connect to backend';
|
||||
} else {
|
||||
errorMessage = isWithinStartupPeriod ? 'Backend starting up...' : error.message;
|
||||
}
|
||||
} else {
|
||||
errorMessage = isWithinStartupPeriod ? 'Backend starting up...' : 'Health check failed';
|
||||
}
|
||||
|
||||
// Only log errors to console after startup period
|
||||
if (!isWithinStartupPeriod) {
|
||||
console.error('Backend health check failed:', {
|
||||
error: error instanceof Error ? error.message : error,
|
||||
timeSinceStartup: Math.round(timeSinceStartup / 1000) + 's',
|
||||
attemptCount,
|
||||
});
|
||||
} else {
|
||||
// During startup, only log on first few attempts to reduce noise
|
||||
if (attemptCount <= 3) {
|
||||
console.log('Backend health check (startup period):', {
|
||||
message: errorMessage,
|
||||
timeSinceStartup: Math.round(timeSinceStartup / 1000) + 's',
|
||||
attempt: attemptCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setHealthState({
|
||||
isHealthy: false,
|
||||
isChecking: false,
|
||||
lastChecked: new Date(),
|
||||
error: errorMessage,
|
||||
});
|
||||
}
|
||||
}, [startupTime]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initial health check
|
||||
checkHealth();
|
||||
|
||||
// Set up periodic health checks
|
||||
const interval = setInterval(checkHealth, checkInterval);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [checkHealth, checkInterval]);
|
||||
|
||||
return {
|
||||
...healthState,
|
||||
checkHealth,
|
||||
};
|
||||
};
|