diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml new file mode 100644 index 00000000000..cd6206cd79e --- /dev/null +++ b/.github/workflows/multiOSReleases.yml @@ -0,0 +1,91 @@ +name: Test Installers Build + +on: + workflow_dispatch: + release: + types: [created] +permissions: + contents: write + packages: write +jobs: + build-installers: + strategy: + matrix: + include: + - os: windows-latest + platform: win + ext: exe + #- os: macos-latest + # platform: mac + # ext: dmg + #- os: ubuntu-latest + # platform: linux + # ext: deb + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: "21" + distribution: "temurin" + + - uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: 8.7 + + # Install Windows dependencies + - name: Install WiX Toolset + if: matrix.os == 'windows-latest' + run: | + curl -L -o wix.exe https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314.exe + .\wix.exe /install /quiet + + # Install Linux dependencies + - name: Install Linux Dependencies + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y fakeroot rpm + + # Get version number + - name: Get version number + id: versionNumber + run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT + shell: bash + + - name: Get version number mac + id: versionNumberMac + run: echo "versionNumberMac=$(./gradlew printMacVersion --quiet | tail -1)" >> $GITHUB_OUTPUT + shell: bash + + # Build installer + - name: Build Installer + run: ./gradlew build jpackage -x test --info + env: + DOCKER_ENABLE_SECURITY: false + STIRLING_PDF_DESKTOP_UI: true + + # Rename and collect artifacts based on OS + - name: Prepare artifacts + id: prepare + shell: bash + run: | + if [ "${{ matrix.os }}" = "windows-latest" ]; then + mv "build/jpackage/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.exe" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" + elif [ "${{ matrix.os }}" = "macos-latest" ]; then + mv "build/jpackage/Stirling-PDF-${{ steps.versionNumberMac.outputs.versionNumberMac }}.dmg" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" + else + mv "build/jpackage/stirling-pdf_${{ steps.versionNumber.outputs.versionNumber }}-1_amd64.deb" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" + fi + + # Upload installer as artifact for testing + - name: Upload Installer Artifact + uses: actions/upload-artifact@v4 + with: + name: Stirling-PDF-${{ matrix.platform }}-installer.{{ matrix.ext }} + path: Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }} + retention-days: 1 + if-no-files-found: error \ No newline at end of file diff --git a/.github/workflows/releaseArtifacts.yml b/.github/workflows/releaseArtifacts.yml index 8adfa0feed2..851fd6cabca 100644 --- a/.github/workflows/releaseArtifacts.yml +++ b/.github/workflows/releaseArtifacts.yml @@ -35,36 +35,37 @@ jobs: run: ./gradlew clean createExe env: DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }} + STIRLING_PDF_DESKTOP_UI: false - name: Get version number id: versionNumber run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT - name: Rename binarie - if: matrix.file_suffix != '' - run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe + run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe - name: Upload Assets binarie uses: actions/upload-artifact@v4 with: - path: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe - name: Stirling-PDF${{ matrix.file_suffix }}.exe + path: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe + name: Stirling-PDF-Server${{ matrix.file_suffix }}.exe overwrite: true retention-days: 1 if-no-files-found: error + - name: Upload binaries to release uses: softprops/action-gh-release@v2 with: - files: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe + files: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe - name: Rename jar binaries - run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar + run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF-Server${{ matrix.file_suffix }}.jar - name: Upload Assets jar binaries uses: actions/upload-artifact@v4 with: - path: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar - name: Stirling-PDF${{ matrix.file_suffix }}.jar + path: ./build/libs/Stirling-PDF-Server${{ matrix.file_suffix }}.jar + name: Stirling-PDF-Server${{ matrix.file_suffix }}.jar overwrite: true retention-days: 1 if-no-files-found: error @@ -72,4 +73,44 @@ jobs: - name: Upload jar binaries to release uses: softprops/action-gh-release@v2 with: - files: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar + files: ./build/libs/Stirling-PDF-Server${{ matrix.file_suffix }}.jar + + + push-ui: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: "17" + distribution: "temurin" + + - uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: 8.7 + + - name: Generate exe + run: ./gradlew clean createExe + env: + DOCKER_ENABLE_SECURITY: false + STIRLING_PDF_DESKTOP_UI: true + + - name: Get version number + id: versionNumber + run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT + + - name: Upload Assets binarie + uses: actions/upload-artifact@v4 + with: + path: ./build/launch4j/Stirling-PDF.exe + name: Stirling-PDF.exe + overwrite: true + retention-days: 1 + if-no-files-found: error + + - name: Upload binaries to release + uses: softprops/action-gh-release@v2 + with: + files: ./build/launch4j/Stirling-PDF.exe diff --git a/.gitignore b/.gitignore index b0bbfb9d338..a79a2712fc3 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,4 @@ out/ .pytest_cache .ipynb_checkpoints +**/jcef-bundle/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 82530a88565..08ef76644fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Main stage -FROM alpine:3.21.0 +FROM alpine:3.20.3 # Copy necessary files COPY scripts /scripts diff --git a/Dockerfile-fat b/Dockerfile-fat index e96f635ab5c..d34c7daa440 100644 --- a/Dockerfile-fat +++ b/Dockerfile-fat @@ -12,7 +12,7 @@ RUN DOCKER_ENABLE_SECURITY=true \ ./gradlew clean build # Main stage -FROM alpine:3.21.0 +FROM alpine:3.20.3 # Copy necessary files COPY scripts /scripts diff --git a/build.gradle b/build.gradle index d4ff54f1d50..6a0862a8207 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,7 @@ plugins { id "com.diffplug.spotless" version "6.25.0" id "com.github.jk1.dependency-license-report" version "2.9" //id "nebula.lint" version "19.0.3" + id("org.panteleyev.jpackageplugin") version "1.6.0" } @@ -26,7 +27,7 @@ ext { } group = "stirling.software" -version = "0.36.0" +version = "0.36.1" java { @@ -41,6 +42,9 @@ repositories { maven { url 'https://build.shibboleth.net/maven/releases' } + maven { url "https://build.shibboleth.net/maven/releases" } + maven { url "https://maven.pkg.github.com/jcefmaven/jcefmaven" } + } licenseReport { @@ -64,6 +68,12 @@ sourceSets { exclude "stirling/software/SPDF/model/User.java" exclude "stirling/software/SPDF/repository/**" } + + if (System.getenv("STIRLING_PDF_DESKTOP_UI") == "false") { + exclude "stirling/software/SPDF/UI/impl/**" + } + + } } } @@ -74,16 +84,153 @@ openApi { outputFileName = "SwaggerDoc.json" } +//0.11.5 to 2024.11.5 +def getMacVersion(String version) { + def currentYear = java.time.Year.now().getValue() + def versionParts = version.split("\\.", 2) + return "${currentYear}.${versionParts.length > 1 ? versionParts[1] : versionParts[0]}" +} + +jpackage { + input = "build/libs" + + appName = "Stirling-PDF" + appVersion = project.version + vendor = "Stirling-Software" + appDescription = "Stirling PDF - Your Local PDF Editor" + + mainJar = "Stirling-PDF-${project.version}.jar" + mainClass = "org.springframework.boot.loader.launch.JarLauncher" + + icon = "src/main/resources/static/favicon.ico" + + + + // JVM Options + javaOptions = [ + "-DBROWSER_OPEN=true", + "-DSTIRLING_PDF_DESKTOP_UI=true", + "-Djava.awt.headless=false", + "-Dapple.awt.UIElement=true", + "--add-opens", "java.base/java.lang=ALL-UNNAMED", + "--add-opens", "java.desktop/java.awt.event=ALL-UNNAMED", + "--add-opens", "java.desktop/sun.awt=ALL-UNNAMED" + + ] + + + verbose = true + + destination = "${projectDir}/build/jpackage" + + // Windows-specific configuration + windows { + launcherAsService = false + appVersion = project.version + winConsole = false + winDirChooser = true + winMenu = true + winShortcut = true + winPerUserInstall = true + winMenuGroup = "Stirling Software" + winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates + winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF" + winUpdateUrl = "https://github.com/Stirling-Tools/Stirling-PDF/releases" + type = "exe" + installDir = "C:/Program Files/Stirling-PDF" + } + + // macOS-specific configuration + mac { + appVersion = getMacVersion(project.version.toString()) + icon = "src/main/resources/static/favicon.icns" + type = "dmg" + macPackageIdentifier = "com.stirling.software.pdf" + macPackageName = "Stirling-PDF" + macAppCategory = "public.app-category.productivity" + macSign = false // Enable signing + macAppStore = false // Not targeting App Store initially + + //installDir = "Applications" + + // Add license and other documentation to DMG + /*macDmgContent = [ + "README.md", + "LICENSE", + "CHANGELOG.md" + ]*/ + + // Enable Mac-specific entitlements + //macEntitlements = "entitlements.plist" // You'll need to create this file + } + + // Linux-specific configuration + linux { + appVersion = project.version + icon = "src/main/resources/static/favicon.png" + type = "deb" // Can also use "rpm" for Red Hat-based systems + + // Debian package configuration + //linuxPackageName = "stirlingpdf" + linuxDebMaintainer = "support@stirlingpdf.com" + linuxMenuGroup = "Office;PDF;Productivity" + linuxAppCategory = "Office" + linuxAppRelease = "1" + linuxPackageDeps = true + + installDir = "/opt/Stirling-PDF" + + // RPM-specific settings + //linuxRpmLicenseType = "MIT" + } + + // Common additional options + //jLinkOptions = [ + // "--strip-debug", + // "--compress=2", + // "--no-header-files", + // "--no-man-pages" + //] + + // Add any additional modules required + /*addModules = [ + "java.base", + "java.desktop", + "java.logging", + "java.sql", + "java.xml", + "jdk.crypto.ec" + ]*/ + + // Add copyright and license information + copyright = "Copyright © 2024 Stirling Software" + licenseFile = "LICENSE" +} + + launch4j { icon = "${projectDir}/src/main/resources/static/favicon.ico" outfile="Stirling-PDF.exe" - headerType="console" + + if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') { + headerType = "gui" + } else { + headerType = "console" + } jarTask = tasks.bootJar errTitle="Encountered error, Do you have Java 21?" downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe" - variables=["BROWSER_OPEN=true"] + + if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') { + variables=["BROWSER_OPEN=true", "STIRLING_PDF_DESKTOP_UI=true"] + } else { + variables=["BROWSER_OPEN=true"] + } + + + jreMinVersion="17" mutexName="Stirling-PDF" @@ -123,6 +270,13 @@ configurations.all { exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat" } dependencies { + + if (System.getenv("STIRLING_PDF_DESKTOP_UI") != "false") { + implementation "me.friwi:jcefmaven:127.3.1" + implementation "org.openjfx:javafx-controls:21" + implementation "org.openjfx:javafx-swing:21" + } + //security updates implementation "org.springframework:spring-webmvc:6.2.0" @@ -271,7 +425,14 @@ jar { tasks.named("test") { useJUnitPlatform() } - task printVersion { - println project.version + doLast { + println project.version + } +} + +task printMacVersion { + doLast { + println getMacVersion(project.version.toString()) + } } diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index eddf7306c07..0d3299c843c 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -1,5 +1,6 @@ package stirling.software.SPDF; +import java.awt.*; import java.io.IOException; import java.net.ServerSocket; import java.nio.file.Files; @@ -8,6 +9,9 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Properties; + +import javax.swing.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,13 +23,16 @@ import org.springframework.scheduling.annotation.EnableScheduling; import io.github.pixee.security.SystemCommand; - import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.UI.WebBrowser; import stirling.software.SPDF.config.ConfigInitializer; import stirling.software.SPDF.model.ApplicationProperties; @SpringBootApplication @EnableScheduling +@Slf4j public class SPdfApplication { private static final Logger logger = LoggerFactory.getLogger(SPdfApplication.class); @@ -67,36 +74,19 @@ private static boolean isPortAvailable(int port) { } } - @PostConstruct - public void init() { - baseUrlStatic = this.baseUrl; - // Check if the BROWSER_OPEN environment variable is set to true - String browserOpenEnv = env.getProperty("BROWSER_OPEN"); - boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv); - if (browserOpen) { - try { - String url = baseUrl + ":" + getStaticPort(); - - String os = System.getProperty("os.name").toLowerCase(); - Runtime rt = Runtime.getRuntime(); - if (os.contains("win")) { - // For Windows - SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url); - } else if (os.contains("mac")) { - SystemCommand.runCommand(rt, "open " + url); - } else if (os.contains("nix") || os.contains("nux")) { - SystemCommand.runCommand(rt, "xdg-open " + url); - } - } catch (Exception e) { - logger.error("Error opening browser: {}", e.getMessage()); - } - } - logger.info("Running configs {}", applicationProperties.toString()); - } - public static void main(String[] args) throws IOException, InterruptedException { SpringApplication app = new SpringApplication(SPdfApplication.class); + + Properties props = new Properties(); + + if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) { + System.setProperty("java.awt.headless", "false"); + app.setHeadless(false); + props.put("java.awt.headless", "false"); + props.put("spring.main.web-application-type", "servlet"); + } + app.setAdditionalProfiles("default"); app.addInitializers(new ConfigInitializer()); Map propertyFiles = new HashMap<>(); @@ -120,14 +110,20 @@ public static void main(String[] args) throws IOException, InterruptedException } else { logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist."); } + Properties finalProps = new Properties(); if (!propertyFiles.isEmpty()) { - app.setDefaultProperties( + finalProps.putAll( Collections.singletonMap( "spring.config.additional-location", propertyFiles.get("spring.config.additional-location"))); } + if (!props.isEmpty()) { + finalProps.putAll(props); + } + app.setDefaultProperties(finalProps); + app.run(args); // Ensure directories are created @@ -147,6 +143,46 @@ private static void printStartupLogs() { logger.info("Navigate to {}", url); } + @Autowired(required = false) + private WebBrowser webBrowser; + + @PostConstruct + public void init() { + baseUrlStatic = this.baseUrl; + String url = baseUrl + ":" + getStaticPort(); + if (webBrowser != null + && Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) { + webBrowser.initWebUI(url); + } else { + String browserOpenEnv = env.getProperty("BROWSER_OPEN"); + boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv); + if (browserOpen) { + try { + String os = System.getProperty("os.name").toLowerCase(); + Runtime rt = Runtime.getRuntime(); + if (os.contains("win")) { + // For Windows + SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url); + } else if (os.contains("mac")) { + SystemCommand.runCommand(rt, "open " + url); + } else if (os.contains("nix") || os.contains("nux")) { + SystemCommand.runCommand(rt, "xdg-open " + url); + } + } catch (Exception e) { + logger.error("Error opening browser: {}", e.getMessage()); + } + } + } + logger.info("Running configs {}", applicationProperties.toString()); + } + + @PreDestroy + public void cleanup() { + if (webBrowser != null) { + webBrowser.cleanup(); + } + } + public static String getStaticBaseUrl() { return baseUrlStatic; } diff --git a/src/main/java/stirling/software/SPDF/UI/WebBrowser.java b/src/main/java/stirling/software/SPDF/UI/WebBrowser.java new file mode 100644 index 00000000000..b884888fe14 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/UI/WebBrowser.java @@ -0,0 +1,7 @@ +package stirling.software.SPDF.UI; + +public interface WebBrowser { + void initWebUI(String url); + + void cleanup(); +} diff --git a/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java b/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java new file mode 100644 index 00000000000..a5509e1b70a --- /dev/null +++ b/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java @@ -0,0 +1,354 @@ +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; + +@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()); + + // 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(); + + // Show the frame immediately but transparent + frame.setVisible(true); + }); + } catch (Exception e) { + log.error("Error initializing JCEF browser: ", e); + cleanup(); + } + }); + } + + private void configureCefSettings(CefAppBuilder builder) { + CefSettings settings = builder.getCefSettings(); + settings.cache_path = new File("jcef-bundle").getAbsolutePath(); + settings.root_cache_path = new File("jcef-bundle").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(1280, 768); + frame.setLocationRelativeTo(null); + + loadIcon(); + } + + private void setupLoadHandler() { + client.addLoadHandler( + new CefLoadHandlerAdapter() { + @Override + public void onLoadingStateChange( + CefBrowser browser, + boolean isLoading, + boolean canGoBack, + boolean canGoForward) { + if (!isLoading && !browserInitialized) { + browserInitialized = true; + SwingUtilities.invokeLater( + () -> { + if (loadingWindow != null) { + Timer timer = + new Timer( + 500, + e -> { + loadingWindow.dispose(); + loadingWindow = null; + + frame.dispose(); + frame.setOpacity(1.0f); + frame.setUndecorated(false); + frame.pack(); + frame.setSize(1280, 800); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + frame.requestFocus(); + frame.toFront(); + browser.getUIComponent() + .requestFocus(); + }); + timer.setRepeats(false); + timer.start(); + } + }); + } + } + }); + } + + 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(); + } +} diff --git a/src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java b/src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java new file mode 100644 index 00000000000..ad827dc5dc4 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java @@ -0,0 +1,114 @@ +package stirling.software.SPDF.UI.impl; + +import java.awt.*; +import java.io.InputStream; + +import javax.imageio.ImageIO; +import javax.swing.*; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class LoadingWindow extends JDialog { + private final JProgressBar progressBar; + private final JLabel statusLabel; + private final JPanel mainPanel; + private final JLabel brandLabel; + + public LoadingWindow(Frame parent, String initialUrl) { + super(parent, "Initializing Stirling-PDF", true); + + // Initialize components + mainPanel = new JPanel(); + mainPanel.setBackground(Color.WHITE); + mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 30, 20, 30)); + mainPanel.setLayout(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + + // Configure GridBagConstraints + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.insets = new Insets(5, 5, 5, 5); + gbc.weightx = 1.0; // Add horizontal weight + gbc.weighty = 0.0; // Add vertical weight + + // Add icon + try { + try (InputStream is = getClass().getResourceAsStream("/static/favicon.ico")) { + if (is != null) { + Image img = ImageIO.read(is); + if (img != null) { + Image scaledImg = img.getScaledInstance(48, 48, Image.SCALE_SMOOTH); + JLabel iconLabel = new JLabel(new ImageIcon(scaledImg)); + iconLabel.setHorizontalAlignment(SwingConstants.CENTER); + gbc.gridy = 0; + mainPanel.add(iconLabel, gbc); + } + } + } + } catch (Exception e) { + log.error("Failed to load icon", e); + } + // URL Label with explicit size + brandLabel = new JLabel(initialUrl); + brandLabel.setHorizontalAlignment(SwingConstants.CENTER); + brandLabel.setPreferredSize(new Dimension(300, 25)); + brandLabel.setText("Stirling-PDF"); + gbc.gridy = 1; + mainPanel.add(brandLabel, gbc); + + // Status label with explicit size + statusLabel = new JLabel("Initializing..."); + statusLabel.setHorizontalAlignment(SwingConstants.CENTER); + statusLabel.setPreferredSize(new Dimension(300, 25)); + gbc.gridy = 2; + mainPanel.add(statusLabel, gbc); + // Progress bar with explicit size + progressBar = new JProgressBar(0, 100); + progressBar.setStringPainted(true); + progressBar.setPreferredSize(new Dimension(300, 25)); + gbc.gridy = 3; + mainPanel.add(progressBar, gbc); + + // Set dialog properties + setContentPane(mainPanel); + setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); + setResizable(false); + setUndecorated(false); + + // Set size and position + setSize(400, 200); + setLocationRelativeTo(parent); + setAlwaysOnTop(true); + setProgress(0); + setStatus("Starting..."); + } + + public void setProgress(final int progress) { + SwingUtilities.invokeLater( + () -> { + try { + progressBar.setValue(Math.min(Math.max(progress, 0), 100)); + progressBar.setString(progress + "%"); + mainPanel.revalidate(); + mainPanel.repaint(); + } catch (Exception e) { + log.error("Error updating progress", e); + } + }); + } + + public void setStatus(final String status) { + log.info(status); + SwingUtilities.invokeLater( + () -> { + try { + statusLabel.setText(status != null ? status : ""); + mainPanel.revalidate(); + mainPanel.repaint(); + } catch (Exception e) { + log.error("Error updating status", e); + } + }); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index 3ce6f2bb8e4..c144007b8b2 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -260,6 +260,9 @@ public void init() { // Pdftohtml dependent endpoints addEndpointToGroup("Pdftohtml", "pdf-to-html"); + + // disabled for now while we resolve issues + disableEndpoint("pdf-to-pdfa"); } private void processEnvironmentConfigs() { diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index 2fcebcad8e5..3291021cbd3 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -76,6 +76,5 @@ private void initializeInternalApiUser() throws IllegalArgumentException, IOExce log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId()); } userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey()); - System.out.println(applicationProperties.getSecurity().getCustomGlobalAPIKey()); } } diff --git a/src/main/resources/static/favicon.icns b/src/main/resources/static/favicon.icns new file mode 100644 index 00000000000..7b281937e8f Binary files /dev/null and b/src/main/resources/static/favicon.icns differ