From 774b500159e643cd06d8090eacd78832dc524388 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 8 Aug 2025 14:19:19 +0100 Subject: [PATCH] get updates advanced (#4124) # Description of Changes This pull request introduces a comprehensive update to the application's update notification and modal system, enhancing both the backend logic and the user interface for update alerts. The changes include a new modal dialog for update details, improved internationalization (i18n) support, dynamic fetching of update information, and context-aware download links. These improvements make update notifications clearer, more informative, and tailored to the user's installation type. **Key changes:** **1. Update Notification and Modal System Overhaul** - Added a new modal dialog (`showUpdateModal`) that displays detailed update information, including current, latest, and latest stable versions, update priority, breaking changes, migration guides, and a list of available updates. The modal dynamically fetches and displays full update details and adapts to dark mode. ([[app/core/src/main/resources/static/js/githubVersion.jsR206-R387](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aR206-R387)]) - Enhanced the update button logic to reflect update priority visually (e.g., urgent/normal/minor), store summary data, and trigger the modal on click. ([[app/core/src/main/resources/static/js/githubVersion.jsL74-R190](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aL74-R190)]) - Improved the update check process to use a new summary API endpoint and handle missing or failed update data gracefully. [[1]](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aL19-R108)], [[2]](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aL74-R190)]) **2. Context-Aware Download Links** - Introduced `getDownloadUrl()` to generate download links based on the user's machine type and security configuration, ensuring only relevant installers or jars are offered. ([[app/core/src/main/resources/static/js/githubVersion.jsL19-R108](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aL19-R108)]) **3. Internationalization (i18n) Enhancements** - Added new i18n keys for all update-related modal and notification strings in `messages_en_GB.properties`. ([[app/core/src/main/resources/messages_en_GB.propertiesR369-R400](diffhunk://#diff-ee1c6999a33498cfa3abba4a384e73a8b8269856899438de80560c965079a9fdR369-R400)]) - Injected all necessary i18n constants into the frontend via `navbar.html` for use in the modal and notifications. ([[app/core/src/main/resources/templates/fragments/navbar.htmlR14-R51](diffhunk://#diff-e7ef383033ea52a00c96e71d5d2c1ff08829078fa5c84c8e48e1bf8f48861ec6R14-R51)]) **4. General UI and Code Improvements** - Ensured update button styling is reset before applying new styles and improved accessibility by hiding the settings modal when the update modal is shown. [[1]](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aR138)], [[2]](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aR206-R387)]) These changes collectively provide a more robust, user-friendly, and maintainable update notification experience. --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Reece Browne Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com> Co-authored-by: a --- .../main/resources/messages_en_GB.properties | 32 ++ .../main/resources/static/js/githubVersion.js | 314 +++++++++++++++++- .../resources/templates/fragments/navbar.html | 35 ++ build.gradle | 2 +- 4 files changed, 368 insertions(+), 15 deletions(-) diff --git a/app/core/src/main/resources/messages_en_GB.properties b/app/core/src/main/resources/messages_en_GB.properties index 37be2c06a..f619b7b6e 100644 --- a/app/core/src/main/resources/messages_en_GB.properties +++ b/app/core/src/main/resources/messages_en_GB.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Settings settings.update=Update available settings.updateAvailable={0} is the current installed version. A new version ({1}) is available. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=App Version: settings.downloadOption.title=Choose download option (For single file non zip downloads): settings.downloadOption.1=Open in same window diff --git a/app/core/src/main/resources/static/js/githubVersion.js b/app/core/src/main/resources/static/js/githubVersion.js index 2aef90d8c..ffc22ed08 100644 --- a/app/core/src/main/resources/static/js/githubVersion.js +++ b/app/core/src/main/resources/static/js/githubVersion.js @@ -16,21 +16,96 @@ function compareVersions(version1, version2) { return 0; } -async function getLatestReleaseVersion() { - const url = "https://api.github.com/repos/Stirling-Tools/Stirling-PDF/releases/latest"; +function getDownloadUrl() { + // Only show download for non-Docker installations + if (machineType === 'Docker' || machineType === 'Kubernetes') { + return null; + } + + const baseUrl = 'https://files.stirlingpdf.com/'; + + // Determine file based on machine type and security + if (machineType === 'Server-jar') { + return baseUrl + (activeSecurity ? 'Stirling-PDF-with-login.jar' : 'Stirling-PDF.jar'); + } + + // Client installations + if (machineType.startsWith('Client-')) { + const os = machineType.replace('Client-', ''); // win, mac, unix + const type = activeSecurity ? '-server-security' : '-server'; + + if (os === 'unix') { + return baseUrl + os + type + '.jar'; + } else if (os === 'win') { + return baseUrl + os + '-installer.exe'; + } else if (os === 'mac') { + return baseUrl + os + '-installer.dmg'; + } + } + + return null; +} + +// Function to get translated priority text +function getTranslatedPriority(priority) { + switch(priority?.toLowerCase()) { + case 'urgent': return updatePriorityUrgent; + case 'normal': return updatePriorityNormal; + case 'minor': return updatePriorityMinor; + case 'low': return updatePriorityLow; + default: return priority?.toUpperCase() || 'NORMAL'; + } +} + +async function getUpdateSummary() { + // Map Java License enum to API types + let type = 'normal'; + if (licenseType === 'PRO') { + type = 'pro'; + } else if (licenseType === 'ENTERPRISE') { + type = 'enterprise'; + } + const url = `https://supabase.stirling.com/functions/v1/updates?from=${currentVersion}&type=${type}&login=${activeSecurity}&summary=true`; + console.log("Fetching update summary from:", url); try { const response = await fetch(url); + console.log("Response status:", response.status); if (response.status === 200) { const data = await response.json(); - return data.tag_name ? data.tag_name.substring(1) : ""; + return data; } else { - // If the status is not 200, try to get the version from build.gradle - return await getCurrentVersionFromBypass(); + console.error("Failed to fetch update summary from Supabase:", response.status); + return null; } } catch (error) { - console.error("Failed to fetch latest version from GitHub:", error); - // If an error occurs, try to get the version from build.gradle - return await getCurrentVersionFromBypass(); + console.error("Failed to fetch update summary from Supabase:", error); + return null; + } +} + +async function getFullUpdateInfo() { + // Map Java License enum to API types + let type = 'normal'; + if (licenseType === 'PRO') { + type = 'pro'; + } else if (licenseType === 'ENTERPRISE') { + type = 'enterprise'; + } + const url = `https://supabase.stirling.com/functions/v1/updates?from=${currentVersion}&type=${type}&login=${activeSecurity}&summary=false`; + console.log("Fetching full update info from:", url); + try { + const response = await fetch(url); + console.log("Full update response status:", response.status); + if (response.status === 200) { + const data = await response.json(); + return data; + } else { + console.error("Failed to fetch full update info from Supabase:", response.status); + return null; + } + } catch (error) { + console.error("Failed to fetch full update info from Supabase:", error); + return null; } } @@ -60,6 +135,7 @@ async function checkForUpdate() { var updateLinkLegacy = document.getElementById("update-link-legacy") || null; if (updateBtn !== null) { updateBtn.style.display = "none"; + updateBtn.classList.remove("btn-danger", "btn-warning", "btn-outline-primary"); } if (updateLink !== null) { updateLink.style.display = "none"; @@ -71,19 +147,47 @@ async function checkForUpdate() { } } - const latestVersion = await getLatestReleaseVersion(); - console.log("latestVersion=" + latestVersion); + const updateSummary = await getUpdateSummary(); + if (!updateSummary) { + console.log("No update summary available"); + return; + } + + console.log("updateSummary=", updateSummary); console.log("currentVersion=" + currentVersion); - console.log("compareVersions(latestVersion, currentVersion) > 0)=" + compareVersions(latestVersion, currentVersion)); - if (latestVersion && compareVersions(latestVersion, currentVersion) > 0) { + console.log("latestVersion=" + updateSummary.latest_version); + + if (updateSummary.latest_version && compareVersions(updateSummary.latest_version, currentVersion) > 0) { + const priority = updateSummary.max_priority || 'normal'; + if (updateBtn != null) { - document.getElementById("update-btn").style.display = "block"; + // Style button based on priority + if (priority === 'urgent') { + updateBtn.classList.add("btn-danger"); + updateBtn.innerHTML = urgentUpdateAvailable; + } else if (priority === 'normal') { + updateBtn.classList.add("btn-warning"); + updateBtn.innerHTML = updateAvailableText; + } else { + updateBtn.classList.add("btn-outline-primary"); + updateBtn.innerHTML = updateAvailableText; + } + + // Store summary for initial display + updateBtn.setAttribute('data-update-summary', JSON.stringify(updateSummary)); + updateBtn.style.display = "block"; + + // Add click handler for update details modal + updateBtn.onclick = function(e) { + e.preventDefault(); + showUpdateModal(); + }; } if (updateLink !== null) { document.getElementById("update-link").style.display = "flex"; } if (updateLinkLegacy !== null) { - document.getElementById("app-update").innerHTML = updateAvailable.replace("{0}", '' + currentVersion + '').replace("{1}", '' + latestVersion + ''); + document.getElementById("app-update").innerHTML = updateAvailable.replace("{0}", '' + currentVersion + '').replace("{1}", '' + updateSummary.latest_version + ''); if (updateLinkLegacy.classList.contains("visually-hidden")) { updateLinkLegacy.classList.remove("visually-hidden"); } @@ -99,6 +203,188 @@ async function checkForUpdate() { } } +async function showUpdateModal() { + // Close settings modal if open + const settingsModal = bootstrap.Modal.getInstance(document.getElementById('settingsModal')); + if (settingsModal) { + settingsModal.hide(); + } + + // Get summary data from button + const updateBtn = document.getElementById("update-btn"); + const summaryData = JSON.parse(updateBtn.getAttribute('data-update-summary')); + + // Utility function to escape HTML special characters + function escapeHtml(str) { + if (typeof str !== 'string') return str; + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/'); + } + + // Create initial modal with loading state + const initialModalHtml = ` + + `; + + // Remove existing modal if present + const existingModal = document.getElementById('updateModal'); + if (existingModal) { + existingModal.remove(); + } + + // Add modal to body + document.body.insertAdjacentHTML('beforeend', initialModalHtml); + + // Show modal + const modal = new bootstrap.Modal(document.getElementById('updateModal')); + modal.show(); + + // Fetch full update info + const fullUpdateInfo = await getFullUpdateInfo(); + + // Update modal with full information + const modalBody = document.getElementById('updateModalBody'); + if (fullUpdateInfo && fullUpdateInfo.new_versions) { + const storedMode = localStorage.getItem("dark-mode"); + const isDarkMode = storedMode === "on" || + (storedMode === null && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches); + const darkClasses = isDarkMode ? { + accordionItem: 'bg-dark border-secondary text-light', + accordionButton: 'bg-dark text-light border-secondary', + accordionBody: 'bg-dark text-light' + } : { + accordionItem: '', + accordionButton: '', + accordionBody: '' + }; + + const detailedVersionsHtml = ` +
+
${updateAvailableUpdates}
+
+ ${fullUpdateInfo.new_versions.map((version, index) => ` +
+

+ +

+
+
+
${version.announcement.title}
+

${version.announcement.message}

+ ${version.compatibility.breaking_changes ? ` + + ` : ''} +
+
+
+ `).join('')} +
+
+ `; + + // Remove loading spinner and add detailed info + const spinner = document.getElementById('loadingSpinner'); + if (spinner) { + spinner.parentElement.remove(); + } + modalBody.insertAdjacentHTML('beforeend', detailedVersionsHtml); + + } else { + // Remove loading spinner if failed to load + const spinner = document.getElementById('loadingSpinner'); + if (spinner) { + spinner.parentElement.innerHTML = `

${updateUnableToLoadDetails}

`; + } + } +} + document.addEventListener("DOMContentLoaded", (event) => { checkForUpdate(); }); diff --git a/app/core/src/main/resources/templates/fragments/navbar.html b/app/core/src/main/resources/templates/fragments/navbar.html index e5aea9345..833d4fd91 100644 --- a/app/core/src/main/resources/templates/fragments/navbar.html +++ b/app/core/src/main/resources/templates/fragments/navbar.html @@ -11,9 +11,44 @@ diff --git a/build.gradle b/build.gradle index ec786e2ed..39672cf24 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,7 @@ repositories { allprojects { group = 'stirling.software' - version = '1.1.1' + version = '1.1.2' configurations.configureEach { exclude group: 'commons-logging', module: 'commons-logging'