V2 Tauri Build Actions (#3899)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
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
|
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]
|
||||||
|
paths:
|
||||||
|
- 'frontend/src-tauri/**'
|
||||||
|
- 'frontend/src/**'
|
||||||
|
- 'frontend/package.json'
|
||||||
|
- 'frontend/package-lock.json'
|
||||||
|
- '.github/workflows/tauri-build.yml'
|
||||||
|
push:
|
||||||
|
branches: [main, V2, "infra/tauri-actions"]
|
||||||
|
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
|
@ -29,7 +29,6 @@ dependencies {
|
|||||||
if (System.getenv('STIRLING_PDF_DESKTOP_UI') != 'false'
|
if (System.getenv('STIRLING_PDF_DESKTOP_UI') != 'false'
|
||||||
|| (project.hasProperty('STIRLING_PDF_DESKTOP_UI')
|
|| (project.hasProperty('STIRLING_PDF_DESKTOP_UI')
|
||||||
&& project.getProperty('STIRLING_PDF_DESKTOP_UI') != 'false')) {
|
&& project.getProperty('STIRLING_PDF_DESKTOP_UI') != 'false')) {
|
||||||
implementation 'me.friwi:jcefmaven:132.3.1'
|
|
||||||
implementation 'org.openjfx:javafx-controls:21'
|
implementation 'org.openjfx:javafx-controls:21'
|
||||||
implementation 'org.openjfx:javafx-swing:21'
|
implementation 'org.openjfx:javafx-swing:21'
|
||||||
}
|
}
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -35,7 +35,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
@ConditionalOnProperty(name = "STIRLING_PDF_TAURI_MODE", havingValue = "true")
|
@ConditionalOnProperty(name = "STIRLING_PDF_TAURI_MODE", havingValue = "true")
|
||||||
public void addCorsMappings(CorsRegistry registry) {
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
registry.addMapping("/**")
|
registry.addMapping("/**")
|
||||||
.allowedOrigins("http://localhost:5173", "http://tauri.localhost")
|
.allowedOrigins("http://localhost:5173", "http://tauri.localhost", "tauri://localhost")
|
||||||
.allowedMethods("*")
|
.allowedMethods("*")
|
||||||
.allowedHeaders("*");
|
.allowedHeaders("*");
|
||||||
}
|
}
|
||||||
|
87
frontend/src-tauri/Cargo.lock
generated
@ -94,7 +94,10 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
|||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"cocoa",
|
||||||
"log",
|
"log",
|
||||||
|
"objc",
|
||||||
|
"once_cell",
|
||||||
"reqwest 0.11.27",
|
"reqwest 0.11.27",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -195,6 +198,12 @@ dependencies = [
|
|||||||
"wyz",
|
"wyz",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@ -454,6 +463,36 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cocoa"
|
||||||
|
version = "0.24.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"block",
|
||||||
|
"cocoa-foundation",
|
||||||
|
"core-foundation 0.9.4",
|
||||||
|
"core-graphics 0.22.3",
|
||||||
|
"foreign-types 0.3.2",
|
||||||
|
"libc",
|
||||||
|
"objc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cocoa-foundation"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"block",
|
||||||
|
"core-foundation 0.9.4",
|
||||||
|
"core-graphics-types 0.1.3",
|
||||||
|
"libc",
|
||||||
|
"objc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@ -506,6 +545,19 @@ version = "0.8.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-graphics"
|
||||||
|
version = "0.22.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"core-foundation 0.9.4",
|
||||||
|
"core-graphics-types 0.1.3",
|
||||||
|
"foreign-types 0.3.2",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-graphics"
|
name = "core-graphics"
|
||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
@ -514,11 +566,22 @@ checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"core-foundation 0.10.1",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics-types",
|
"core-graphics-types 0.2.0",
|
||||||
"foreign-types 0.5.0",
|
"foreign-types 0.5.0",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-graphics-types"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"core-foundation 0.9.4",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-graphics-types"
|
name = "core-graphics-types"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -1978,6 +2041,15 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "malloc_buf"
|
||||||
|
version = "0.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markup5ever"
|
name = "markup5ever"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@ -2165,6 +2237,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
|
||||||
|
dependencies = [
|
||||||
|
"malloc_buf",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc-sys"
|
name = "objc-sys"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
@ -3509,7 +3590,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"core-graphics",
|
"core-graphics 0.24.0",
|
||||||
"foreign-types 0.5.0",
|
"foreign-types 0.5.0",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
@ -3687,7 +3768,7 @@ checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"core-foundation 0.10.1",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics",
|
"core-graphics 0.24.0",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dispatch",
|
"dispatch",
|
||||||
"dlopen2",
|
"dlopen2",
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "stirling-pdf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "A Tauri App"
|
description = "Stirling-PDF Desktop Application"
|
||||||
authors = ["you"]
|
authors = ["Stirling-PDF Contributors"]
|
||||||
license = ""
|
license = ""
|
||||||
repository = ""
|
repository = ""
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@ -27,3 +27,9 @@ tauri-plugin-shell = "2.1.0"
|
|||||||
tauri-plugin-fs = "2.0.0"
|
tauri-plugin-fs = "2.0.0"
|
||||||
tokio = { version = "1.0", features = ["time"] }
|
tokio = { version = "1.0", features = ["time"] }
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
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"
|
||||||
|
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 |
Before Width: | Height: | Size: 1.5 KiB 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/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 |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 8.7 KiB 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 |
@ -112,13 +112,37 @@ fn normalize_path(path: &PathBuf) -> PathBuf {
|
|||||||
|
|
||||||
// Create, configure and run the Java command to run Stirling-PDF JAR
|
// 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> {
|
fn run_stirling_pdf_jar(app: &tauri::AppHandle, java_path: &PathBuf, jar_path: &PathBuf) -> Result<(), String> {
|
||||||
// Configure logging to write outside src-tauri to prevent dev server restarts
|
// Get platform-specific application data directory for Tauri mode
|
||||||
let temp_dir = std::env::temp_dir();
|
let app_data_dir = if cfg!(target_os = "macos") {
|
||||||
let log_dir = temp_dir.join("stirling-pdf-logs");
|
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||||
std::fs::create_dir_all(&log_dir).ok(); // Create log directory if it doesn't exist
|
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")
|
||||||
|
};
|
||||||
|
|
||||||
// Define all Java options in an array
|
// 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 log_path_option = format!("-Dlogging.file.path={}", log_dir.display());
|
||||||
|
|
||||||
let java_options = vec![
|
let java_options = vec![
|
||||||
"-Xmx2g",
|
"-Xmx2g",
|
||||||
"-DBROWSER_OPEN=false",
|
"-DBROWSER_OPEN=false",
|
||||||
@ -132,18 +156,49 @@ fn run_stirling_pdf_jar(app: &tauri::AppHandle, java_path: &PathBuf, jar_path: &
|
|||||||
|
|
||||||
// Log the equivalent command for external testing
|
// Log the equivalent command for external testing
|
||||||
let java_command = format!(
|
let java_command = format!(
|
||||||
"TAURI_PARENT_PID={} && \"{}\" {}",
|
"TAURI_PARENT_PID={} \"{}\" {}",
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
java_path.display(),
|
java_path.display(),
|
||||||
java_options.join(" ")
|
java_options.join(" ")
|
||||||
);
|
);
|
||||||
add_log(format!("🔧 Equivalent command: {}", java_command));
|
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
|
let sidecar_command = app
|
||||||
.shell()
|
.shell()
|
||||||
.command(java_path.to_str().unwrap())
|
.command(java_path.to_str().unwrap())
|
||||||
.args(java_options)
|
.args(java_options)
|
||||||
.env("TAURI_PARENT_PID", std::process::id().to_string());
|
.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());
|
add_log("⚙️ Starting backend with bundled JRE...".to_string());
|
||||||
|
|
||||||
|
@ -1,15 +1,35 @@
|
|||||||
use crate::utils::add_log;
|
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)
|
// Command to get opened file path (if app was launched with a file)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_opened_file() -> Result<Option<String>, String> {
|
pub async fn get_opened_file() -> Result<Option<String>, String> {
|
||||||
// Get command line arguments
|
// 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();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
|
||||||
// Look for a PDF file argument (skip the first arg which is the executable)
|
// Look for a PDF file argument (skip the first arg which is the executable)
|
||||||
for arg in args.iter().skip(1) {
|
for arg in args.iter().skip(1) {
|
||||||
if arg.ends_with(".pdf") && std::path::Path::new(arg).exists() {
|
if arg.ends_with(".pdf") && std::path::Path::new(arg).exists() {
|
||||||
add_log(format!("📂 PDF file opened: {}", arg));
|
add_log(format!("📂 PDF file opened via command line: {}", arg));
|
||||||
return Ok(Some(arg.clone()));
|
return Ok(Some(arg.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -17,3 +37,12 @@ pub async fn get_opened_file() -> Result<Option<String>, String> {
|
|||||||
Ok(None)
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -4,4 +4,4 @@ pub mod files;
|
|||||||
|
|
||||||
pub use backend::{start_backend, cleanup_backend};
|
pub use backend::{start_backend, cleanup_backend};
|
||||||
pub use health::check_backend_health;
|
pub use health::check_backend_health;
|
||||||
pub use files::get_opened_file;
|
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());
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,30 @@
|
|||||||
use tauri::{RunEvent, WindowEvent};
|
use tauri::{RunEvent, WindowEvent, Emitter};
|
||||||
|
|
||||||
mod utils;
|
mod utils;
|
||||||
mod commands;
|
mod commands;
|
||||||
|
mod file_handler;
|
||||||
|
|
||||||
use commands::{start_backend, check_backend_health, get_opened_file, cleanup_backend};
|
use commands::{start_backend, check_backend_health, get_opened_file, clear_opened_file, cleanup_backend, set_opened_file};
|
||||||
use utils::add_log;
|
use utils::{add_log, get_tauri_logs};
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
|
// Initialize file handler early for macOS
|
||||||
|
file_handler::early_init();
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
.setup(|_app| {Ok(())})
|
.setup(|app| {
|
||||||
.invoke_handler(tauri::generate_handler![start_backend, check_backend_health, get_opened_file])
|
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!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while building tauri application")
|
.expect("error while building tauri application")
|
||||||
.run(|app_handle, event| {
|
.run(|app_handle, event| {
|
||||||
@ -28,7 +40,26 @@ pub fn run() {
|
|||||||
cleanup_backend();
|
cleanup_backend();
|
||||||
// Allow the window to close
|
// 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -1,20 +1,90 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
// Store backend logs globally
|
// Store backend logs globally
|
||||||
static BACKEND_LOGS: Mutex<VecDeque<String>> = Mutex::new(VecDeque::new());
|
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
|
// Helper function to add log entry
|
||||||
pub fn add_log(message: String) {
|
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();
|
let mut logs = BACKEND_LOGS.lock().unwrap();
|
||||||
logs.push_back(format!("{}: {}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(), message));
|
logs.push_back(log_entry.clone());
|
||||||
// Keep only last 100 log entries
|
// Keep only last 100 log entries
|
||||||
if logs.len() > 100 {
|
if logs.len() > 100 {
|
||||||
logs.pop_front();
|
logs.pop_front();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to file
|
||||||
|
write_to_log_file(&log_entry);
|
||||||
|
|
||||||
// Remove trailing newline if present
|
// Remove trailing newline if present
|
||||||
let clean_message = message.trim_end_matches('\n').to_string();
|
let clean_message = message.trim_end_matches('\n').to_string();
|
||||||
println!("{}", clean_message); // Also print to console
|
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())
|
||||||
|
}
|
@ -1,3 +1,3 @@
|
|||||||
pub mod logging;
|
pub mod logging;
|
||||||
|
|
||||||
pub use logging::add_log;
|
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
|
@ -13,21 +13,25 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "Stirling-PDF",
|
"title": "Stirling-PDF",
|
||||||
"width": 800,
|
"width": 1024,
|
||||||
"height": 600,
|
"height": 768,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"fullscreen": false
|
"fullscreen": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": ["deb", "rpm", "dmg", "msi"],
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/icon.png",
|
||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico",
|
||||||
|
"icons/16x16.png",
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/64x64.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/192x192.png"
|
||||||
],
|
],
|
||||||
"resources": [
|
"resources": [
|
||||||
"libs/*.jar",
|
"libs/*.jar",
|
||||||
@ -38,9 +42,15 @@
|
|||||||
"ext": ["pdf"],
|
"ext": ["pdf"],
|
||||||
"name": "PDF Document",
|
"name": "PDF Document",
|
||||||
"description": "Open PDF files with Stirling-PDF",
|
"description": "Open PDF files with Stirling-PDF",
|
||||||
"role": "Editor"
|
"role": "Editor",
|
||||||
|
"mimeType": "application/pdf"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"linux": {
|
||||||
|
"deb": {
|
||||||
|
"desktopTemplate": "stirling-pdf.desktop"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"shell": {
|
"shell": {
|
||||||
|
@ -7,12 +7,19 @@ export function useOpenedFile() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkForOpenedFile = async () => {
|
const checkForOpenedFile = async () => {
|
||||||
|
console.log('🔍 Checking for opened file...');
|
||||||
try {
|
try {
|
||||||
const filePath = await fileOpenService.getOpenedFile();
|
const filePath = await fileOpenService.getOpenedFile();
|
||||||
|
console.log('🔍 fileOpenService.getOpenedFile() returned:', filePath);
|
||||||
|
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
console.log('✅ App opened with file:', filePath);
|
console.log('✅ App opened with file:', filePath);
|
||||||
setOpenedFilePath(filePath);
|
setOpenedFilePath(filePath);
|
||||||
|
|
||||||
|
// Clear the file from service state after consuming it
|
||||||
|
await fileOpenService.clearOpenedFile();
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ No file was opened with the app');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -23,6 +30,17 @@ export function useOpenedFile() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
checkForOpenedFile();
|
checkForOpenedFile();
|
||||||
|
|
||||||
|
// Listen for runtime file open events (abstracted through service)
|
||||||
|
const unlistenRuntimeEvents = fileOpenService.onFileOpened((filePath) => {
|
||||||
|
console.log('📂 Runtime file open event:', filePath);
|
||||||
|
setOpenedFilePath(filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup function
|
||||||
|
return () => {
|
||||||
|
unlistenRuntimeEvents();
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { openedFilePath, loading };
|
return { openedFilePath, loading };
|
||||||
|
@ -3,15 +3,19 @@ import { invoke } from '@tauri-apps/api/core';
|
|||||||
export interface FileOpenService {
|
export interface FileOpenService {
|
||||||
getOpenedFile(): Promise<string | null>;
|
getOpenedFile(): Promise<string | null>;
|
||||||
readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null>;
|
readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null>;
|
||||||
|
clearOpenedFile(): Promise<void>;
|
||||||
|
onFileOpened(callback: (filePath: string) => void): () => void; // Returns unlisten function
|
||||||
}
|
}
|
||||||
|
|
||||||
class TauriFileOpenService implements FileOpenService {
|
class TauriFileOpenService implements FileOpenService {
|
||||||
async getOpenedFile(): Promise<string | null> {
|
async getOpenedFile(): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
|
console.log('🔍 Calling invoke(get_opened_file)...');
|
||||||
const result = await invoke<string | null>('get_opened_file');
|
const result = await invoke<string | null>('get_opened_file');
|
||||||
|
console.log('🔍 invoke(get_opened_file) returned:', result);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get opened file:', error);
|
console.error('❌ Failed to get opened file:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,6 +36,85 @@ class TauriFileOpenService implements FileOpenService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async clearOpenedFile(): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Calling invoke(clear_opened_file)...');
|
||||||
|
await invoke('clear_opened_file');
|
||||||
|
console.log('✅ Successfully cleared opened file');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to clear opened file:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileOpened(callback: (filePath: string) => void): () => void {
|
||||||
|
let cleanup: (() => void) | null = null;
|
||||||
|
let isCleanedUp = false;
|
||||||
|
|
||||||
|
const setupEventListeners = async () => {
|
||||||
|
try {
|
||||||
|
// Check if already cleaned up before async setup completes
|
||||||
|
if (isCleanedUp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only import if in Tauri environment
|
||||||
|
if (typeof window !== 'undefined' && ('__TAURI__' in window || '__TAURI_INTERNALS__' in window)) {
|
||||||
|
const { listen } = await import('@tauri-apps/api/event');
|
||||||
|
|
||||||
|
// Check again after async import
|
||||||
|
if (isCleanedUp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for macOS native file open events
|
||||||
|
const unlistenMacOS = await listen('macos://open-file', (event) => {
|
||||||
|
console.log('📂 macOS native file open event:', event.payload);
|
||||||
|
callback(event.payload as string);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for fallback file open events
|
||||||
|
const unlistenFallback = await listen('file-opened', (event) => {
|
||||||
|
console.log('📂 Fallback file open event:', event.payload);
|
||||||
|
callback(event.payload as string);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up cleanup function only if not already cleaned up
|
||||||
|
if (!isCleanedUp) {
|
||||||
|
cleanup = () => {
|
||||||
|
try {
|
||||||
|
unlistenMacOS();
|
||||||
|
unlistenFallback();
|
||||||
|
console.log('✅ File event listeners cleaned up');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error during file event cleanup:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Clean up immediately if cleanup was called during setup
|
||||||
|
try {
|
||||||
|
unlistenMacOS();
|
||||||
|
unlistenFallback();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error during immediate cleanup:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to setup file event listeners:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setupEventListeners();
|
||||||
|
|
||||||
|
// Return cleanup function
|
||||||
|
return () => {
|
||||||
|
isCleanedUp = true;
|
||||||
|
if (cleanup) {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebFileOpenService implements FileOpenService {
|
class WebFileOpenService implements FileOpenService {
|
||||||
@ -44,6 +127,18 @@ class WebFileOpenService implements FileOpenService {
|
|||||||
// In web mode, cannot read arbitrary file paths
|
// In web mode, cannot read arbitrary file paths
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async clearOpenedFile(): Promise<void> {
|
||||||
|
// In web mode, no file clearing needed
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileOpened(callback: (filePath: string) => void): () => void {
|
||||||
|
// In web mode, no file events - return no-op cleanup function
|
||||||
|
console.log('ℹ️ Web mode: File event listeners not supported');
|
||||||
|
return () => {
|
||||||
|
// No-op cleanup for web mode
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export the appropriate service based on environment
|
// Export the appropriate service based on environment
|
||||||
|
@ -28,8 +28,8 @@ if errorlevel 1 (
|
|||||||
|
|
||||||
REM Find the built JAR(s)
|
REM Find the built JAR(s)
|
||||||
echo ▶ Listing all built JAR files in app\core\build\libs:
|
echo ▶ Listing all built JAR files in app\core\build\libs:
|
||||||
dir /b app\core\build\libs\Stirling-PDF-*.jar
|
dir /b app\core\build\libs\stirling-pdf-*.jar
|
||||||
for %%f in (app\core\build\libs\Stirling-PDF-*.jar) do set STIRLING_JAR=%%f
|
for %%f in (app\core\build\libs\stirling-pdf-*.jar) do set STIRLING_JAR=%%f
|
||||||
if not exist "%STIRLING_JAR%" (
|
if not exist "%STIRLING_JAR%" (
|
||||||
echo ❌ No Stirling-PDF JAR found in build/libs/
|
echo ❌ No Stirling-PDF JAR found in build/libs/
|
||||||
exit /b 1
|
exit /b 1
|
||||||
@ -47,7 +47,7 @@ echo ✅ JAR copied to frontend\src-tauri\libs\
|
|||||||
|
|
||||||
REM Log out all JAR files now in the Tauri libs directory
|
REM Log out all JAR files now in the Tauri libs directory
|
||||||
echo ▶ Listing all JAR files in frontend\src-tauri\libs after copy:
|
echo ▶ Listing all JAR files in frontend\src-tauri\libs after copy:
|
||||||
dir /b frontend\src-tauri\libs\Stirling-PDF-*.jar
|
dir /b frontend\src-tauri\libs\stirling-pdf-*.jar
|
||||||
|
|
||||||
echo ▶ Creating custom JRE with jlink...
|
echo ▶ Creating custom JRE with jlink...
|
||||||
if exist "frontend\src-tauri\runtime\jre" rmdir /s /q "frontend\src-tauri\runtime\jre"
|
if exist "frontend\src-tauri\runtime\jre" rmdir /s /q "frontend\src-tauri\runtime\jre"
|
||||||
|
@ -62,13 +62,13 @@ fi
|
|||||||
print_step "Building Stirling-PDF JAR..."
|
print_step "Building Stirling-PDF JAR..."
|
||||||
./gradlew clean bootJar --no-daemon
|
./gradlew clean bootJar --no-daemon
|
||||||
|
|
||||||
if [ ! -f "stirling-pdf/build/libs/Stirling-PDF-"*.jar ]; then
|
if [ ! -f "app\core\build\libs\stirling-pdf-*.jar"*.jar ]; then
|
||||||
print_error "Failed to build Stirling-PDF JAR"
|
print_error "Failed to build stirling-pdf JAR"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Find the built JAR
|
# Find the built JAR
|
||||||
STIRLING_JAR=$(ls stirling-pdf/build/libs/Stirling-PDF-*.jar | head -n 1)
|
STIRLING_JAR=$(ls app\core\build\libs\stirling-pdf-*.jar | head -n 1)
|
||||||
print_success "Built JAR: $STIRLING_JAR"
|
print_success "Built JAR: $STIRLING_JAR"
|
||||||
|
|
||||||
# Create directories for Tauri
|
# Create directories for Tauri
|
||||||
@ -98,7 +98,7 @@ if command -v jdeps &> /dev/null; then
|
|||||||
if [ -n "$REQUIRED_MODULES" ]; then
|
if [ -n "$REQUIRED_MODULES" ]; then
|
||||||
print_success "jdeps detected modules: $REQUIRED_MODULES"
|
print_success "jdeps detected modules: $REQUIRED_MODULES"
|
||||||
# Add additional modules we know Stirling-PDF needs
|
# Add additional modules we know Stirling-PDF needs
|
||||||
MODULES="$REQUIRED_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.transaction.xa,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
MODULES="$REQUIRED_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
|
else
|
||||||
print_warning "jdeps analysis failed, using predefined module list"
|
print_warning "jdeps analysis failed, using predefined module list"
|
||||||
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"
|
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"
|
||||||
|