diff --git a/.github/workflows/README-tauri.md b/.github/workflows/README-tauri.md new file mode 100644 index 000000000..be6346045 --- /dev/null +++ b/.github/workflows/README-tauri.md @@ -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 \ No newline at end of file diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml new file mode 100644 index 000000000..0ea6ce236 --- /dev/null +++ b/.github/workflows/tauri-build.yml @@ -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 \ No newline at end of file diff --git a/app/core/build.gradle b/app/core/build.gradle index 745dbb87a..014f934de 100644 --- a/app/core/build.gradle +++ b/app/core/build.gradle @@ -29,7 +29,6 @@ dependencies { if (System.getenv('STIRLING_PDF_DESKTOP_UI') != 'false' || (project.hasProperty('STIRLING_PDF_DESKTOP_UI') && project.getProperty('STIRLING_PDF_DESKTOP_UI') != 'false')) { - implementation 'me.friwi:jcefmaven:132.3.1' implementation 'org.openjfx:javafx-controls:21' implementation 'org.openjfx:javafx-swing:21' } diff --git a/app/core/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java b/app/core/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java deleted file mode 100644 index 959e7f354..000000000 --- a/app/core/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java +++ /dev/null @@ -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(); - } - } - } -} diff --git a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java index 31047d209..458441522 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java @@ -35,7 +35,7 @@ public class WebMvcConfig implements WebMvcConfigurer { @ConditionalOnProperty(name = "STIRLING_PDF_TAURI_MODE", havingValue = "true") public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:5173", "http://tauri.localhost") + .allowedOrigins("http://localhost:5173", "http://tauri.localhost", "tauri://localhost") .allowedMethods("*") .allowedHeaders("*"); } diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 32a032e70..7c63b0d41 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -94,7 +94,10 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" name = "app" version = "0.1.0" dependencies = [ + "cocoa", "log", + "objc", + "once_cell", "reqwest 0.11.27", "serde", "serde_json", @@ -195,6 +198,12 @@ dependencies = [ "wyz", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -454,6 +463,36 @@ dependencies = [ "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]] name = "combine" version = "4.6.7" @@ -506,6 +545,19 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "core-graphics" version = "0.24.0" @@ -514,11 +566,22 @@ checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.9.1", "core-foundation 0.10.1", - "core-graphics-types", + "core-graphics-types 0.2.0", "foreign-types 0.5.0", "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]] name = "core-graphics-types" version = "0.2.0" @@ -1978,6 +2041,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "markup5ever" version = "0.11.0" @@ -2165,6 +2237,15 @@ dependencies = [ "libc", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -3509,7 +3590,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" dependencies = [ "bytemuck", "cfg_aliases", - "core-graphics", + "core-graphics 0.24.0", "foreign-types 0.5.0", "js-sys", "log", @@ -3687,7 +3768,7 @@ checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82" dependencies = [ "bitflags 2.9.1", "core-foundation 0.10.1", - "core-graphics", + "core-graphics 0.24.0", "crossbeam-channel", "dispatch", "dlopen2", diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index c09d1455b..17a2bd30e 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "app" +name = "stirling-pdf" version = "0.1.0" -description = "A Tauri App" -authors = ["you"] +description = "Stirling-PDF Desktop Application" +authors = ["Stirling-PDF Contributors"] license = "" repository = "" edition = "2021" @@ -27,3 +27,9 @@ tauri-plugin-shell = "2.1.0" tauri-plugin-fs = "2.0.0" tokio = { version = "1.0", features = ["time"] } reqwest = { version = "0.11", features = ["json"] } + +# macOS-specific dependencies for native file opening +[target.'cfg(target_os = "macos")'.dependencies] +objc = "0.2" +cocoa = "0.24" +once_cell = "1.19" diff --git a/frontend/src-tauri/icons/128x128.png b/frontend/src-tauri/icons/128x128.png new file mode 100644 index 000000000..d233685dd Binary files /dev/null and b/frontend/src-tauri/icons/128x128.png differ diff --git a/frontend/src-tauri/icons/128x128@2x.png b/frontend/src-tauri/icons/128x128@2x.png new file mode 100644 index 000000000..0ac88b8db Binary files /dev/null and b/frontend/src-tauri/icons/128x128@2x.png differ diff --git a/frontend/src-tauri/icons/32x32.png b/frontend/src-tauri/icons/32x32.png index f44f0c371..9e8dd8a5d 100644 Binary files a/frontend/src-tauri/icons/32x32.png and b/frontend/src-tauri/icons/32x32.png differ diff --git a/frontend/src-tauri/icons/64x64.png b/frontend/src-tauri/icons/64x64.png new file mode 100644 index 000000000..280a9c5ac Binary files /dev/null and b/frontend/src-tauri/icons/64x64.png differ diff --git a/frontend/src-tauri/icons/Square107x107Logo.png b/frontend/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 000000000..840e43164 Binary files /dev/null and b/frontend/src-tauri/icons/Square107x107Logo.png differ diff --git a/frontend/src-tauri/icons/Square142x142Logo.png b/frontend/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 000000000..aa8b40576 Binary files /dev/null and b/frontend/src-tauri/icons/Square142x142Logo.png differ diff --git a/frontend/src-tauri/icons/Square150x150Logo.png b/frontend/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 000000000..3c33180f9 Binary files /dev/null and b/frontend/src-tauri/icons/Square150x150Logo.png differ diff --git a/frontend/src-tauri/icons/Square284x284Logo.png b/frontend/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 000000000..72c5d7188 Binary files /dev/null and b/frontend/src-tauri/icons/Square284x284Logo.png differ diff --git a/frontend/src-tauri/icons/Square30x30Logo.png b/frontend/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 000000000..dc2fe4cfc Binary files /dev/null and b/frontend/src-tauri/icons/Square30x30Logo.png differ diff --git a/frontend/src-tauri/icons/Square310x310Logo.png b/frontend/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 000000000..b5fa0be56 Binary files /dev/null and b/frontend/src-tauri/icons/Square310x310Logo.png differ diff --git a/frontend/src-tauri/icons/Square44x44Logo.png b/frontend/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 000000000..097839b00 Binary files /dev/null and b/frontend/src-tauri/icons/Square44x44Logo.png differ diff --git a/frontend/src-tauri/icons/Square71x71Logo.png b/frontend/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 000000000..fb066fb32 Binary files /dev/null and b/frontend/src-tauri/icons/Square71x71Logo.png differ diff --git a/frontend/src-tauri/icons/Square89x89Logo.png b/frontend/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 000000000..00d893d7c Binary files /dev/null and b/frontend/src-tauri/icons/Square89x89Logo.png differ diff --git a/frontend/src-tauri/icons/StoreLogo.png b/frontend/src-tauri/icons/StoreLogo.png new file mode 100644 index 000000000..c56df3f8a Binary files /dev/null and b/frontend/src-tauri/icons/StoreLogo.png differ diff --git a/frontend/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/frontend/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..6a361221e Binary files /dev/null and b/frontend/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/frontend/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/frontend/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..475ea7dbc Binary files /dev/null and b/frontend/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/frontend/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/frontend/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..6a361221e Binary files /dev/null and b/frontend/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/frontend/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/frontend/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..a82e68b38 Binary files /dev/null and b/frontend/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/frontend/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/frontend/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..d563b2d25 Binary files /dev/null and b/frontend/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/frontend/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/frontend/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..a82e68b38 Binary files /dev/null and b/frontend/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/frontend/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/frontend/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..6c28ce599 Binary files /dev/null and b/frontend/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/frontend/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/frontend/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..c0cf472de Binary files /dev/null and b/frontend/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/frontend/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/frontend/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..6c28ce599 Binary files /dev/null and b/frontend/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..b5df806c1 Binary files /dev/null and b/frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..21a8c5bfb Binary files /dev/null and b/frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..b5df806c1 Binary files /dev/null and b/frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..224b0169c Binary files /dev/null and b/frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..516f2024e Binary files /dev/null and b/frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..224b0169c Binary files /dev/null and b/frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/frontend/src-tauri/icons/icon.icns b/frontend/src-tauri/icons/icon.icns index 7b281937e..983df8577 100644 Binary files a/frontend/src-tauri/icons/icon.icns and b/frontend/src-tauri/icons/icon.icns differ diff --git a/frontend/src-tauri/icons/icon.ico b/frontend/src-tauri/icons/icon.ico index 8ad57cac7..b058a5591 100644 Binary files a/frontend/src-tauri/icons/icon.ico and b/frontend/src-tauri/icons/icon.ico differ diff --git a/frontend/src-tauri/icons/icon.png b/frontend/src-tauri/icons/icon.png index 5edc6eae2..5819d1b89 100644 Binary files a/frontend/src-tauri/icons/icon.png and b/frontend/src-tauri/icons/icon.png differ diff --git a/frontend/src-tauri/icons/icon_orig.png b/frontend/src-tauri/icons/icon_orig.png new file mode 100644 index 000000000..5edc6eae2 Binary files /dev/null and b/frontend/src-tauri/icons/icon_orig.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-20x20@1x.png b/frontend/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 000000000..b440dda9d Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/frontend/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 000000000..44ec1c6bc Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-20x20@2x.png b/frontend/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 000000000..44ec1c6bc Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-20x20@3x.png b/frontend/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 000000000..e388901c8 Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-29x29@1x.png b/frontend/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 000000000..df4c10e2f Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/frontend/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 000000000..8a78c7b87 Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-29x29@2x.png b/frontend/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 000000000..8a78c7b87 Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-29x29@3x.png b/frontend/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 000000000..da7b0097b Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-40x40@1x.png b/frontend/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 000000000..44ec1c6bc Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/frontend/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 000000000..70f8711ff Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-40x40@2x.png b/frontend/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 000000000..70f8711ff Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-40x40@3x.png b/frontend/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 000000000..d1d0ee368 Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-512@2x.png b/frontend/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 000000000..346e4a702 Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-60x60@2x.png b/frontend/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 000000000..d1d0ee368 Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-60x60@3x.png b/frontend/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 000000000..5190cb21b Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-76x76@1x.png b/frontend/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 000000000..9d97d05bb Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-76x76@2x.png b/frontend/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 000000000..4085fefa3 Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/frontend/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/frontend/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 000000000..2ad07a090 Binary files /dev/null and b/frontend/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/frontend/src-tauri/src/commands/backend.rs b/frontend/src-tauri/src/commands/backend.rs index 0149ccd78..c465f7cc8 100644 --- a/frontend/src-tauri/src/commands/backend.rs +++ b/frontend/src-tauri/src/commands/backend.rs @@ -112,13 +112,37 @@ fn normalize_path(path: &PathBuf) -> PathBuf { // 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> { - // Configure logging to write outside src-tauri to prevent dev server restarts - let temp_dir = std::env::temp_dir(); - let log_dir = temp_dir.join("stirling-pdf-logs"); - std::fs::create_dir_all(&log_dir).ok(); // Create log directory if it doesn't exist + // Get platform-specific application data directory for Tauri mode + let app_data_dir = if cfg!(target_os = "macos") { + let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); + PathBuf::from(home).join("Library").join("Application Support").join("Stirling-PDF") + } else if cfg!(target_os = "windows") { + let appdata = std::env::var("APPDATA").unwrap_or_else(|_| std::env::temp_dir().to_string_lossy().to_string()); + PathBuf::from(appdata).join("Stirling-PDF") + } else { + let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); + PathBuf::from(home).join(".config").join("Stirling-PDF") + }; - // 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 java_options = vec![ "-Xmx2g", "-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 let java_command = format!( - "TAURI_PARENT_PID={} && \"{}\" {}", + "TAURI_PARENT_PID={} \"{}\" {}", std::process::id(), java_path.display(), java_options.join(" ") ); add_log(format!("🔧 Equivalent command: {}", java_command)); + add_log(format!("📁 Backend logs will be in: {}", log_dir.display())); + + // Additional macOS-specific checks + if cfg!(target_os = "macos") { + // Check if java executable has execute permissions + if let Ok(metadata) = std::fs::metadata(java_path) { + let permissions = metadata.permissions(); + add_log(format!("🔍 Java executable permissions: {:?}", permissions)); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = permissions.mode(); + add_log(format!("🔍 Java executable mode: 0o{:o}", mode)); + if mode & 0o111 == 0 { + add_log("⚠️ Java executable may not have execute permissions".to_string()); + } + } + } + + // Check if we can read the JAR file + if let Ok(metadata) = std::fs::metadata(jar_path) { + add_log(format!("📦 JAR file size: {} bytes", metadata.len())); + } else { + add_log("⚠️ Cannot read JAR file metadata".to_string()); + } + } let sidecar_command = app .shell() .command(java_path.to_str().unwrap()) .args(java_options) - .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()); diff --git a/frontend/src-tauri/src/commands/files.rs b/frontend/src-tauri/src/commands/files.rs index e77917e1f..7c397cfcf 100644 --- a/frontend/src-tauri/src/commands/files.rs +++ b/frontend/src-tauri/src/commands/files.rs @@ -1,15 +1,35 @@ use crate::utils::add_log; +use std::sync::Mutex; + +// Store the opened file path globally +static OPENED_FILE: Mutex> = Mutex::new(None); + +// Set the opened file path (called by macOS file open events) +pub fn set_opened_file(file_path: String) { + let mut opened_file = OPENED_FILE.lock().unwrap(); + *opened_file = Some(file_path.clone()); + add_log(format!("📂 File opened via file open event: {}", file_path)); +} // Command to get opened file path (if app was launched with a file) #[tauri::command] pub async fn get_opened_file() -> Result, 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 = std::env::args().collect(); // Look for a PDF file argument (skip the first arg which is the executable) for arg in args.iter().skip(1) { if arg.ends_with(".pdf") && std::path::Path::new(arg).exists() { - add_log(format!("📂 PDF file opened: {}", arg)); + add_log(format!("📂 PDF file opened via command line: {}", arg)); return Ok(Some(arg.clone())); } } @@ -17,3 +37,12 @@ pub async fn get_opened_file() -> Result, String> { 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(()) +} + diff --git a/frontend/src-tauri/src/commands/mod.rs b/frontend/src-tauri/src/commands/mod.rs index 97cd657d2..773f5d2dd 100644 --- a/frontend/src-tauri/src/commands/mod.rs +++ b/frontend/src-tauri/src/commands/mod.rs @@ -4,4 +4,4 @@ pub mod files; pub use backend::{start_backend, cleanup_backend}; pub use health::check_backend_health; -pub use files::get_opened_file; \ No newline at end of file +pub use files::{get_opened_file, clear_opened_file, set_opened_file}; \ No newline at end of file diff --git a/frontend/src-tauri/src/file_handler.rs b/frontend/src-tauri/src/file_handler.rs new file mode 100644 index 000000000..d432a8131 --- /dev/null +++ b/frontend/src-tauri/src/file_handler.rs @@ -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) { + 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 = 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>>> = Lazy::new(|| Mutex::new(None)); + + // Store files opened during launch + static LAUNCH_FILES: Lazy>> = 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) { + 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()); + } +} \ No newline at end of file diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 084c57f6d..f64b8bd0b 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -1,18 +1,30 @@ -use tauri::{RunEvent, WindowEvent}; +use tauri::{RunEvent, WindowEvent, Emitter}; mod utils; mod commands; +mod file_handler; -use commands::{start_backend, check_backend_health, get_opened_file, cleanup_backend}; -use utils::add_log; +use commands::{start_backend, check_backend_health, get_opened_file, clear_opened_file, cleanup_backend, set_opened_file}; +use utils::{add_log, get_tauri_logs}; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + // Initialize file handler early for macOS + file_handler::early_init(); + tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_fs::init()) - .setup(|_app| {Ok(())}) - .invoke_handler(tauri::generate_handler![start_backend, check_backend_health, get_opened_file]) + .setup(|app| { + add_log("🚀 Tauri app setup started".to_string()); + + // Initialize platform-specific file handler + file_handler::initialize_file_handler(&app.handle()); + + add_log("🔍 DEBUG: Setup completed".to_string()); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![start_backend, check_backend_health, get_opened_file, clear_opened_file, get_tauri_logs]) .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|app_handle, event| { @@ -28,7 +40,26 @@ pub fn run() { cleanup_backend(); // Allow the window to close } - _ => {} + #[cfg(target_os = "macos")] + RunEvent::Opened { urls } => { + add_log(format!("📂 Tauri file opened event: {:?}", urls)); + for url in urls { + let url_str = url.as_str(); + if url_str.starts_with("file://") { + let file_path = url_str.strip_prefix("file://").unwrap_or(url_str); + if file_path.ends_with(".pdf") { + add_log(format!("📂 Processing opened PDF: {}", file_path)); + set_opened_file(file_path.to_string()); + let _ = app_handle.emit("macos://open-file", file_path.to_string()); + } + } + } + } + _ => { + // Only log unhandled events in debug mode to reduce noise + // #[cfg(debug_assertions)] + // add_log(format!("🔍 DEBUG: Unhandled event: {:?}", event)); + } } }); } \ No newline at end of file diff --git a/frontend/src-tauri/src/utils/logging.rs b/frontend/src-tauri/src/utils/logging.rs index c5c0fccab..bda1913ad 100644 --- a/frontend/src-tauri/src/utils/logging.rs +++ b/frontend/src-tauri/src/utils/logging.rs @@ -1,20 +1,90 @@ use std::sync::Mutex; use std::collections::VecDeque; +use std::fs::OpenOptions; +use std::io::Write; +use std::path::PathBuf; // Store backend logs globally static BACKEND_LOGS: Mutex> = Mutex::new(VecDeque::new()); +// Get platform-specific log directory +fn get_log_directory() -> PathBuf { + if cfg!(target_os = "macos") { + // macOS: ~/Library/Logs/Stirling-PDF + let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); + PathBuf::from(home).join("Library").join("Logs").join("Stirling-PDF") + } else if cfg!(target_os = "windows") { + // Windows: %APPDATA%\Stirling-PDF\logs + let appdata = std::env::var("APPDATA").unwrap_or_else(|_| std::env::temp_dir().to_string_lossy().to_string()); + PathBuf::from(appdata).join("Stirling-PDF").join("logs") + } else { + // Linux: ~/.config/Stirling-PDF/logs + let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); + PathBuf::from(home).join(".config").join("Stirling-PDF").join("logs") + } +} + // Helper function to add log entry pub fn add_log(message: String) { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); - let mut logs = BACKEND_LOGS.lock().unwrap(); - logs.push_back(format!("{}: {}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(), message)); - // Keep only last 100 log entries - if logs.len() > 100 { - logs.pop_front(); + let log_entry = format!("{}: {}", timestamp, message); + + // Add to memory logs + { + let mut logs = BACKEND_LOGS.lock().unwrap(); + logs.push_back(log_entry.clone()); + // Keep only last 100 log entries + if logs.len() > 100 { + logs.pop_front(); + } } + // Write to file + write_to_log_file(&log_entry); + // Remove trailing newline if present let clean_message = message.trim_end_matches('\n').to_string(); println!("{}", clean_message); // Also print to console +} + +// Write log entry to file +fn write_to_log_file(log_entry: &str) { + let log_dir = get_log_directory(); + if let Err(e) = std::fs::create_dir_all(&log_dir) { + eprintln!("Failed to create log directory: {}", e); + return; + } + + let log_file = log_dir.join("tauri-backend.log"); + + match OpenOptions::new() + .create(true) + .append(true) + .open(&log_file) + { + Ok(mut file) => { + if let Err(e) = writeln!(file, "{}", log_entry) { + eprintln!("Failed to write to log file: {}", e); + } + } + Err(e) => { + eprintln!("Failed to open log file {:?}: {}", log_file, e); + } + } +} + +// Get current logs for debugging +pub fn get_logs() -> Vec { + 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, String> { + Ok(get_logs()) } \ No newline at end of file diff --git a/frontend/src-tauri/src/utils/mod.rs b/frontend/src-tauri/src/utils/mod.rs index 4f7f3c38d..258efca6c 100644 --- a/frontend/src-tauri/src/utils/mod.rs +++ b/frontend/src-tauri/src/utils/mod.rs @@ -1,3 +1,3 @@ pub mod logging; -pub use logging::add_log; \ No newline at end of file +pub use logging::{add_log, get_tauri_logs}; \ No newline at end of file diff --git a/frontend/src-tauri/stirling-pdf.desktop b/frontend/src-tauri/stirling-pdf.desktop new file mode 100644 index 000000000..9d6029377 --- /dev/null +++ b/frontend/src-tauri/stirling-pdf.desktop @@ -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 \ No newline at end of file diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index f36385303..89b24641a 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -13,21 +13,25 @@ "windows": [ { "title": "Stirling-PDF", - "width": 800, - "height": 600, + "width": 1024, + "height": 768, "resizable": true, "fullscreen": false } ] - }, "bundle": { "active": true, - "targets": "all", + "targets": ["deb", "rpm", "dmg", "msi"], "icon": [ - "icons/32x32.png", + "icons/icon.png", "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": [ "libs/*.jar", @@ -38,9 +42,15 @@ "ext": ["pdf"], "name": "PDF Document", "description": "Open PDF files with Stirling-PDF", - "role": "Editor" + "role": "Editor", + "mimeType": "application/pdf" } - ] + ], + "linux": { + "deb": { + "desktopTemplate": "stirling-pdf.desktop" + } + } }, "plugins": { "shell": { diff --git a/frontend/src/hooks/useOpenedFile.ts b/frontend/src/hooks/useOpenedFile.ts index 55343e489..a1ac024e9 100644 --- a/frontend/src/hooks/useOpenedFile.ts +++ b/frontend/src/hooks/useOpenedFile.ts @@ -7,13 +7,20 @@ export function useOpenedFile() { useEffect(() => { const checkForOpenedFile = async () => { + console.log('🔍 Checking for opened file...'); try { const filePath = await fileOpenService.getOpenedFile(); + console.log('🔍 fileOpenService.getOpenedFile() returned:', filePath); if (filePath) { console.log('✅ App opened with file:', 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) { console.error('❌ Failed to check for opened file:', error); @@ -23,6 +30,17 @@ export function useOpenedFile() { }; 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 }; diff --git a/frontend/src/services/fileOpenService.ts b/frontend/src/services/fileOpenService.ts index 2ee43a7b7..3280ea028 100644 --- a/frontend/src/services/fileOpenService.ts +++ b/frontend/src/services/fileOpenService.ts @@ -3,15 +3,19 @@ import { invoke } from '@tauri-apps/api/core'; export interface FileOpenService { getOpenedFile(): Promise; readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null>; + clearOpenedFile(): Promise; + onFileOpened(callback: (filePath: string) => void): () => void; // Returns unlisten function } class TauriFileOpenService implements FileOpenService { async getOpenedFile(): Promise { try { + console.log('🔍 Calling invoke(get_opened_file)...'); const result = await invoke('get_opened_file'); + console.log('🔍 invoke(get_opened_file) returned:', result); return result; } catch (error) { - console.error('Failed to get opened file:', error); + console.error('❌ Failed to get opened file:', error); return null; } } @@ -32,6 +36,85 @@ class TauriFileOpenService implements FileOpenService { return null; } } + + async clearOpenedFile(): Promise { + 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 { @@ -44,6 +127,18 @@ class WebFileOpenService implements FileOpenService { // In web mode, cannot read arbitrary file paths return null; } + + async clearOpenedFile(): Promise { + // 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 diff --git a/scripts/build-tauri-jlink.bat b/scripts/build-tauri-jlink.bat index fb81b8438..d3a73f058 100644 --- a/scripts/build-tauri-jlink.bat +++ b/scripts/build-tauri-jlink.bat @@ -28,8 +28,8 @@ if errorlevel 1 ( REM Find the built JAR(s) echo ▶ Listing all built JAR files in app\core\build\libs: -dir /b app\core\build\libs\Stirling-PDF-*.jar -for %%f in (app\core\build\libs\Stirling-PDF-*.jar) do set STIRLING_JAR=%%f +dir /b app\core\build\libs\stirling-pdf-*.jar +for %%f in (app\core\build\libs\stirling-pdf-*.jar) do set STIRLING_JAR=%%f if not exist "%STIRLING_JAR%" ( echo ❌ No Stirling-PDF JAR found in build/libs/ 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 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... if exist "frontend\src-tauri\runtime\jre" rmdir /s /q "frontend\src-tauri\runtime\jre" diff --git a/scripts/build-tauri-jlink.sh b/scripts/build-tauri-jlink.sh index d941773ec..5b576d091 100644 --- a/scripts/build-tauri-jlink.sh +++ b/scripts/build-tauri-jlink.sh @@ -62,13 +62,13 @@ fi print_step "Building Stirling-PDF JAR..." ./gradlew clean bootJar --no-daemon -if [ ! -f "stirling-pdf/build/libs/Stirling-PDF-"*.jar ]; then - print_error "Failed to build Stirling-PDF JAR" +if [ ! -f "app\core\build\libs\stirling-pdf-*.jar"*.jar ]; then + print_error "Failed to build stirling-pdf JAR" exit 1 fi # 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" # Create directories for Tauri @@ -98,7 +98,7 @@ if command -v jdeps &> /dev/null; then if [ -n "$REQUIRED_MODULES" ]; then print_success "jdeps detected modules: $REQUIRED_MODULES" # 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 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"