diff --git a/src/main/resources/static/css/fileSelect.css b/src/main/resources/static/css/fileSelect.css new file mode 100644 index 00000000..2cd2c682 --- /dev/null +++ b/src/main/resources/static/css/fileSelect.css @@ -0,0 +1,10 @@ +.custom-file-label { + padding-right: 90px; +} + +.selected-files { + margin-top: 10px; + max-height: 150px; + overflow-y: auto; + white-space: pre-wrap; +} \ No newline at end of file diff --git a/src/main/resources/static/js/darkmode.js b/src/main/resources/static/js/darkmode.js new file mode 100644 index 00000000..c91bfa68 --- /dev/null +++ b/src/main/resources/static/js/darkmode.js @@ -0,0 +1,76 @@ +var toggleCount = 0; +var lastToggleTime = Date.now(); + +function toggleDarkMode() { + var currentTime = Date.now(); + if (currentTime - lastToggleTime < 1000) { + toggleCount++; + } else { + toggleCount = 1; + } + lastToggleTime = currentTime; + + var lightModeStyles = document.getElementById("light-mode-styles"); + var darkModeStyles = document.getElementById("dark-mode-styles"); + var rainbowModeStyles = document.getElementById("rainbow-mode-styles"); + var darkModeIcon = document.getElementById("dark-mode-icon"); + + if (toggleCount >= 18) { + localStorage.setItem("dark-mode", "rainbow"); + lightModeStyles.disabled = true; + darkModeStyles.disabled = true; + rainbowModeStyles.disabled = false; + darkModeIcon.src = "rainbow.svg"; + } else if (localStorage.getItem("dark-mode") == "on") { + localStorage.setItem("dark-mode", "off"); + lightModeStyles.disabled = false; + darkModeStyles.disabled = true; + rainbowModeStyles.disabled = true; + darkModeIcon.src = "sun.svg"; + } else { + localStorage.setItem("dark-mode", "on"); + lightModeStyles.disabled = true; + darkModeStyles.disabled = false; + rainbowModeStyles.disabled = true; + darkModeIcon.src = "moon.svg"; + } +} + +document.addEventListener("DOMContentLoaded", function() { + var lightModeStyles = document.getElementById("light-mode-styles"); + var darkModeStyles = document.getElementById("dark-mode-styles"); + var rainbowModeStyles = document.getElementById("rainbow-mode-styles"); + var darkModeIcon = document.getElementById("dark-mode-icon"); + + if (localStorage.getItem("dark-mode") == "on") { + lightModeStyles.disabled = true; + darkModeStyles.disabled = false; + rainbowModeStyles.disabled = true; + darkModeIcon.src = "moon.svg"; + } else if (localStorage.getItem("dark-mode") == "off") { + lightModeStyles.disabled = false; + darkModeStyles.disabled = true; + rainbowModeStyles.disabled = true; + darkModeIcon.src = "sun.svg"; + } else if (localStorage.getItem("dark-mode") == "rainbow") { + lightModeStyles.disabled = true; + darkModeStyles.disabled = true; + rainbowModeStyles.disabled = false; + darkModeIcon.src = "rainbow.svg"; + } else { + if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { + darkModeStyles.disabled = false; + rainbowModeStyles.disabled = true; + darkModeIcon.src = "moon.svg"; + } else { + darkModeStyles.disabled = true; + rainbowModeStyles.disabled = true; + darkModeIcon.src = "sun.svg"; + } + } + + document.getElementById("dark-mode-toggle").addEventListener("click", function(event) { + event.preventDefault(); + toggleDarkMode(); + }); +}); \ No newline at end of file diff --git a/src/main/resources/static/js/downloader.js b/src/main/resources/static/js/downloader.js new file mode 100644 index 00000000..5dc1912f --- /dev/null +++ b/src/main/resources/static/js/downloader.js @@ -0,0 +1,242 @@ +function showErrorBanner(message, stackTrace) { + const errorContainer = document.getElementById("errorContainer"); + errorContainer.style.display = "block"; // Display the banner + document.querySelector("#errorContainer .alert-heading").textContent = "Error"; + document.querySelector("#errorContainer p").textContent = message; + document.querySelector("#traceContent").textContent = stackTrace; +} + +$(document).ready(function() { + $('form').submit(async function(event) { + event.preventDefault(); + + const url = this.action; + const files = $('#fileInput-input')[0].files; + const formData = new FormData(this); + const override = $('#override').val() || ''; + + $('#submitBtn').text('Processing...'); + + try { + if (override === 'multi' || files.length > 1 && override !== 'single') { + // Show the progress bar + $('#progressBarContainer').show(); + // Initialize the progress bar + //let progressBar = $('#progressBar'); + //progressBar.css('width', '0%'); + //progressBar.attr('aria-valuenow', 0); + //progressBar.attr('aria-valuemax', files.length); + + await submitMultiPdfForm(url, files); + } else { + const downloadDetails = await handleSingleDownload(url, formData); + const downloadOption = localStorage.getItem('downloadOption'); + + // Handle the download action according to the selected option + //handleDownloadAction(downloadOption, downloadDetails.blob, downloadDetails.filename); + + // Update the progress bar + //updateProgressBar(progressBar, 1); + + } + + $('#submitBtn').text('Submit'); + } catch (error) { + handleDownloadError(error); + $('#submitBtn').text('Submit'); + console.error(error); + } + }); +}); + +function handleDownloadAction(downloadOption, blob, filename) { + const url = URL.createObjectURL(blob); + + switch (downloadOption) { + case 'sameWindow': + // Open the file in the same window + window.location.href = url; + break; + case 'newWindow': + // Open the file in a new window + window.open(url, '_blank'); + break; + default: + // Download the file + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + break; + } +} + +async function handleSingleDownload(url, formData) { + try { + const response = await fetch(url, { method: 'POST', body: formData }); + const contentType = response.headers.get('content-type'); + + if (!response.ok) { + if (contentType && contentType.includes('application/json')) { + return handleJsonResponse(response); + console.error('Throwing error banner, response was not okay'); + } + throw new Error(`HTTP error! status: ${response.status}`); + } + + const contentDisposition = response.headers.get('Content-Disposition'); + let filename = getFilenameFromContentDisposition(contentDisposition); + + const blob = await response.blob(); + + if (contentType.includes('application/pdf') || contentType.includes('image/')) { + return handleResponse(blob, filename, true); + } else { + return handleResponse(blob, filename); + } + } catch (error) { + console.error('Error in handleSingleDownload:', error); + throw error; // Re-throw the error if you want it to be handled higher up. + } +} + +function getFilenameFromContentDisposition(contentDisposition) { + let filename; + + if (contentDisposition && contentDisposition.indexOf('attachment') !== -1) { + filename = decodeURIComponent(contentDisposition.split('filename=')[1].replace(/"/g, '')).trim(); + } else { + // If the Content-Disposition header is not present or does not contain the filename, use a default filename + filename = 'download'; + } + + return filename; +} + + + +async function handleJsonResponse(response) { + const json = await response.json(); + const errorMessage = JSON.stringify(json, null, 2); + if (errorMessage.toLowerCase().includes('the password is incorrect') || errorMessage.toLowerCase().includes('Password is not provided')) { + alert('[[#{error.pdfPassword}]]'); + } else { + showErrorBanner(json.error + ':' + json.message, json.trace); + } +} + + +async function handleResponse(blob, filename, considerViewOptions = false) { + if (!blob) return; + const downloadOption = localStorage.getItem('downloadOption'); + if (considerViewOptions) { + if (downloadOption === 'sameWindow') { + const url = URL.createObjectURL(blob); + window.location.href = url; + return; + } else if (downloadOption === 'newWindow') { + const url = URL.createObjectURL(blob); + window.open(url, '_blank'); + return; + } + } + downloadFile(blob, filename); + return { filename, blob }; +} + +function handleDownloadError(error) { + const errorMessage = error.message; + showErrorBanner(errorMessage); +} + +let urls = []; // An array to hold all the URLs + +function downloadFile(blob, filename) { + if (!(blob instanceof Blob)) { + console.error('Invalid blob passed to downloadFile function'); + return; + } + + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + urls.push(url); // Store the URL so it doesn't get garbage collected too soon + + return { filename, blob }; +} + + + +async function submitMultiPdfForm(url, files) { + const zipThreshold = parseInt(localStorage.getItem('zipThreshold'), 10) || 4; + const zipFiles = files.length > zipThreshold; + let jszip = null; + //let progressBar = $('#progressBar'); + //progressBar.css('width', '0%'); + //progressBar.attr('aria-valuenow', 0); + //progressBar.attr('aria-valuemax', Array.from(files).length); + if (zipFiles) { + jszip = new JSZip(); + } + + // Get existing form data + let formData = new FormData($('form')[0]); + formData.delete('fileInput'); + + const CONCURRENCY_LIMIT = 8; + const chunks = []; + for (let i = 0; i < Array.from(files).length; i += CONCURRENCY_LIMIT) { + chunks.push(Array.from(files).slice(i, i + CONCURRENCY_LIMIT)); + } + + for (const chunk of chunks) { + const promises = chunk.map(async file => { + let fileFormData = new FormData(); + fileFormData.append('fileInput', file); + + // Add other form data + for (let pair of formData.entries()) { + fileFormData.append(pair[0], pair[1]); + } + + try { + const downloadDetails = await handleSingleDownload(url, fileFormData); + console.log(downloadDetails); + if (zipFiles) { + jszip.file(downloadDetails.filename, downloadDetails.blob); + } else { + downloadFile(downloadDetails.blob, downloadDetails.filename); + } + //updateProgressBar(progressBar, Array.from(files).length); + } catch (error) { + handleDownloadError(error); + console.error(error); + } + }); + await Promise.all(promises); + } + + if (zipFiles) { + try { + const content = await jszip.generateAsync({ type: "blob" }); + downloadFile(content, "files.zip"); + } catch (error) { + console.error('Error generating ZIP file: ' + error); + } + } +} + + + +function updateProgressBar(progressBar, files) { + let progress = ((progressBar.attr('aria-valuenow') / files.length) * 100) + (100 / files.length); + progressBar.css('width', progress + '%'); + progressBar.attr('aria-valuenow', parseInt(progressBar.attr('aria-valuenow')) + 1); +} +window.addEventListener('unload', () => { + for (const url of urls) { + URL.revokeObjectURL(url); + } +}); diff --git a/src/main/resources/static/js/errorBanner.js b/src/main/resources/static/js/errorBanner.js new file mode 100644 index 00000000..9d151407 --- /dev/null +++ b/src/main/resources/static/js/errorBanner.js @@ -0,0 +1,50 @@ +var traceVisible = false; + +function toggletrace() { + var traceDiv = document.getElementById("trace"); + if (!traceVisible) { + traceDiv.style.maxHeight = "500px"; + traceVisible = true; + } else { + traceDiv.style.maxHeight = "0px"; + traceVisible = false; + } + adjustContainerHeight(); +} + +function copytrace() { + var flip = false + if (!traceVisible) { + toggletrace() + flip = true + } + var traceContent = document.getElementById("traceContent"); + var range = document.createRange(); + range.selectNode(traceContent); + window.getSelection().removeAllRanges(); + window.getSelection().addRange(range); + document.execCommand("copy"); + window.getSelection().removeAllRanges(); + if (flip) { + toggletrace() + } +} + +function dismissError() { + var errorContainer = document.getElementById("errorContainer"); + errorContainer.style.display = "none"; + errorContainer.style.height = "0"; +} + +function adjustContainerHeight() { + var errorContainer = document.getElementById("errorContainer"); + var traceDiv = document.getElementById("trace"); + if (traceVisible) { + errorContainer.style.height = errorContainer.scrollHeight - traceDiv.scrollHeight + traceDiv.offsetHeight + "px"; + } else { + errorContainer.style.height = "auto"; + } +} +function showHelp() { + $('#helpModal').modal('show'); +} \ No newline at end of file diff --git a/src/main/resources/static/js/favourites.js b/src/main/resources/static/js/favourites.js new file mode 100644 index 00000000..7372ad30 --- /dev/null +++ b/src/main/resources/static/js/favourites.js @@ -0,0 +1,37 @@ +function updateFavoritesDropdown() { + var dropdown = document.querySelector('#favoritesDropdown'); + dropdown.innerHTML = ''; // Clear the current favorites + + + + var hasFavorites = false; + + for (var i = 0; i < localStorage.length; i++) { + var key = localStorage.key(i); + if (localStorage.getItem(key) === 'favorite') { + // Find the corresponding navbar entry + var navbarEntry = document.querySelector(`a[href='${key}']`); + if (navbarEntry) { + // Create a new dropdown entry + var dropdownItem = document.createElement('a'); + dropdownItem.className = 'dropdown-item'; + dropdownItem.href = navbarEntry.href; + dropdownItem.innerHTML = navbarEntry.innerHTML; + dropdown.appendChild(dropdownItem); + hasFavorites = true; + } + } + } + + // Show or hide the default item based on whether there are any favorites + if (!hasFavorites) { + var defaultItem = document.createElement('a'); + defaultItem.className = 'dropdown-item'; + defaultItem.textContent = noFavourites; + dropdown.appendChild(defaultItem); + } +} +document.addEventListener('DOMContentLoaded', function() { + + updateFavoritesDropdown(); +}); \ No newline at end of file diff --git a/src/main/resources/static/js/fileInput.js b/src/main/resources/static/js/fileInput.js new file mode 100644 index 00000000..16111d24 --- /dev/null +++ b/src/main/resources/static/js/fileInput.js @@ -0,0 +1,45 @@ +document.addEventListener('DOMContentLoaded', function() { + const fileInput = document.getElementById(elementID); + + // Prevent default behavior for drag events + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + fileInput.addEventListener(eventName, preventDefaults, false); + }); + + function preventDefaults(e) { + e.preventDefault(); + e.stopPropagation(); + } + + // Add drop event listener + fileInput.addEventListener('drop', handleDrop, false); + + function handleDrop(e) { + const dt = e.dataTransfer; + const files = dt.files; + fileInput.files = files; + handleFileInputChange(fileInput) + } +}); + +$(elementID).on("change", function() { + handleFileInputChange(this); +}); + +function handleFileInputChange(inputElement) { + const files = $(inputElement).get(0).files; + const fileNames = Array.from(files).map(f => f.name); + const selectedFilesContainer = $(inputElement).siblings(".selected-files"); + selectedFilesContainer.empty(); + fileNames.forEach(fileName => { + selectedFilesContainer.append("
" + fileName + "
"); + }); + console.log("fileNames.length=" + fileNames.length) + if (fileNames.length === 1) { + $(inputElement).siblings(".custom-file-label").addClass("selected").html(fileNames[0]); + } else if (fileNames.length > 1) { + $(inputElement).siblings(".custom-file-label").addClass("selected").html(fileNames.length + " " + filesSelected); + } else { + $(inputElement).siblings(".custom-file-label").addClass("selected").html(pdfPrompt); + } +} \ No newline at end of file diff --git a/src/main/resources/static/js/githubVersion.js b/src/main/resources/static/js/githubVersion.js new file mode 100644 index 00000000..35433f76 --- /dev/null +++ b/src/main/resources/static/js/githubVersion.js @@ -0,0 +1,41 @@ +function compareVersions(version1, version2) { + const v1 = version1.split('.'); + const v2 = version2.split('.'); + + for (let i = 0; i < v1.length || i < v2.length; i++) { + const n1 = parseInt(v1[i]) || 0; + const n2 = parseInt(v2[i]) || 0; + + if (n1 > n2) { + return 1; + } else if (n1 < n2) { + return -1; + } + } + + return 0; +} + +async function getLatestReleaseVersion() { + const url = "https://api.github.com/repos/Frooodle/Stirling-PDF/releases/latest"; + const response = await fetch(url); + const data = await response.json(); + return data.tag_name.substring(1); +} + + +async function checkForUpdate() { + const latestVersion = await getLatestReleaseVersion(); + console.log("latestVersion=" + latestVersion) + console.log("currentVersion=" + currentVersion) + console.log("compareVersions(latestVersion, currentVersion) > 0)=" + compareVersions(latestVersion, currentVersion)) + if (latestVersion != null && latestVersion != "" && compareVersions(latestVersion, currentVersion) > 0) { + document.getElementById("update-btn").style.display = "block"; + console.log("visible") + } else { + document.getElementById("update-btn").style.display = "none"; + console.log("hidden") + } +} + +checkForUpdate(); \ No newline at end of file diff --git a/src/main/resources/static/js/languageSelection.js b/src/main/resources/static/js/languageSelection.js new file mode 100644 index 00000000..e9d141f5 --- /dev/null +++ b/src/main/resources/static/js/languageSelection.js @@ -0,0 +1,46 @@ +document.addEventListener('DOMContentLoaded', function() { + const defaultLocale = document.documentElement.lang || 'en_GB'; + const storedLocale = localStorage.getItem('languageCode') || defaultLocale; + const dropdownItems = document.querySelectorAll('.lang_dropdown-item'); + + for (let i = 0; i < dropdownItems.length; i++) { + const item = dropdownItems[i]; + item.classList.remove('active'); + if (item.dataset.languageCode === storedLocale) { + item.classList.add('active'); + } + item.addEventListener('click', handleDropdownItemClick); + } +}); + +function handleDropdownItemClick(event) { + event.preventDefault(); + const languageCode = this.dataset.languageCode; + localStorage.setItem('languageCode', languageCode); + + const currentUrl = window.location.href; + if (currentUrl.indexOf('?lang=') === -1) { + window.location.href = currentUrl + '?lang=' + languageCode; + } else { + window.location.href = currentUrl.replace(/\?lang=\w{2,}/, '?lang=' + languageCode); + } +} + +$(document).ready(function() { + $(".nav-item.dropdown").each(function() { + var $dropdownMenu = $(this).find(".dropdown-menu"); + if ($dropdownMenu.children().length <= 2 && $dropdownMenu.children("hr.dropdown-divider").length === $dropdownMenu.children().length) { + $(this).prev('.nav-item.nav-item-separator').remove(); + $(this).remove(); + } + }); + + //Sort languages by alphabet + var list = $('.dropdown-menu[aria-labelledby="languageDropdown"]').children("a"); + list.sort(function(a, b) { + var A = $(a).text().toUpperCase(); + var B = $(b).text().toUpperCase(); + return (A < B) ? -1 : (A > B) ? 1 : 0; + }) + .appendTo('.dropdown-menu[aria-labelledby="languageDropdown"]'); +}); \ No newline at end of file diff --git a/src/main/resources/static/js/merge.js b/src/main/resources/static/js/merge.js new file mode 100644 index 00000000..523be4a8 --- /dev/null +++ b/src/main/resources/static/js/merge.js @@ -0,0 +1,63 @@ +document.getElementById("fileInput-input").addEventListener("change", function() { + var files = this.files; + var list = document.getElementById("selectedFiles"); + list.innerHTML = ""; + for (var i = 0; i < files.length; i++) { + var item = document.createElement("li"); + item.className = "list-group-item"; + item.innerHTML = ` +
+
${files[i].name}
+
+ + +
+
+ `; + list.appendChild(item); + } + + var moveUpButtons = document.querySelectorAll(".move-up"); + for (var i = 0; i < moveUpButtons.length; i++) { + moveUpButtons[i].addEventListener("click", function(event) { + event.preventDefault(); + var parent = this.closest(".list-group-item"); + var grandParent = parent.parentNode; + if (parent.previousElementSibling) { + grandParent.insertBefore(parent, parent.previousElementSibling); + updateFiles(); + } + }); + } + + var moveDownButtons = document.querySelectorAll(".move-down"); + for (var i = 0; i < moveDownButtons.length; i++) { + moveDownButtons[i].addEventListener("click", function(event) { + event.preventDefault(); + var parent = this.closest(".list-group-item"); + var grandParent = parent.parentNode; + if (parent.nextElementSibling) { + grandParent.insertBefore(parent.nextElementSibling, parent); + updateFiles(); + } + }); + } + + function updateFiles() { + var dataTransfer = new DataTransfer(); + var liElements = document.querySelectorAll("#selectedFiles li"); + + for (var i = 0; i < liElements.length; i++) { + var fileNameFromList = liElements[i].querySelector(".filename").innerText; + var fileFromFiles; + for (var j = 0; j < files.length; j++) { + var file = files[j]; + if (file.name === fileNameFromList) { + dataTransfer.items.add(file); + break; + } + } + } + document.getElementById("fileInput-input").files = dataTransfer.files; + } +}); \ No newline at end of file diff --git a/src/main/resources/static/js/settings.js b/src/main/resources/static/js/settings.js new file mode 100644 index 00000000..272b555b --- /dev/null +++ b/src/main/resources/static/js/settings.js @@ -0,0 +1,42 @@ +// Get the download option from local storage, or set it to 'sameWindow' if it doesn't exist +var downloadOption = localStorage.getItem('downloadOption') + || 'sameWindow'; + +// Set the selected option in the dropdown +document.getElementById('downloadOption').value = downloadOption; + + +// Save the selected option to local storage when the dropdown value changes +document.getElementById('downloadOption').addEventListener( + 'change', + function() { + downloadOption = this.value; + localStorage.setItem('downloadOption', + downloadOption); + }); + + +// Get the zipThreshold value from local storage, or set it to 0 if it doesn't exist +var zipThreshold = parseInt(localStorage.getItem('zipThreshold'), 10) || 4; + +// Set the value of the slider and the display span +document.getElementById('zipThreshold').value = zipThreshold; +document.getElementById('zipThresholdValue').textContent = zipThreshold; + + + +// Save the selected value to local storage when the slider value changes +document.getElementById('zipThreshold').addEventListener('input', function() { + zipThreshold = this.value; + document.getElementById('zipThresholdValue').textContent = zipThreshold; + localStorage.setItem('zipThreshold', zipThreshold); +}); + + +var boredWaiting = localStorage.getItem('boredWaiting') || 'disabled'; +document.getElementById('boredWaiting').checked = boredWaiting === 'enabled'; + +document.getElementById('boredWaiting').addEventListener('change', function() { + boredWaiting = this.checked ? 'enabled' : 'disabled'; + localStorage.setItem('boredWaiting', boredWaiting); +}); \ No newline at end of file diff --git a/src/main/resources/templates/fragments/common.html b/src/main/resources/templates/fragments/common.html index eb9ba053..e08f7b04 100644 --- a/src/main/resources/templates/fragments/common.html +++ b/src/main/resources/templates/fragments/common.html @@ -34,84 +34,7 @@ - + @@ -168,214 +91,7 @@ document.addEventListener("DOMContentLoaded", function () { - +
@@ -394,65 +110,12 @@ document.addEventListener("DOMContentLoaded", function () { - - - - - + + + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/errorBannerPerPage.html b/src/main/resources/templates/fragments/errorBannerPerPage.html index 2516b929..e1365513 100644 --- a/src/main/resources/templates/fragments/errorBannerPerPage.html +++ b/src/main/resources/templates/fragments/errorBannerPerPage.html @@ -54,56 +54,5 @@
- +
\ No newline at end of file diff --git a/src/main/resources/templates/fragments/navbar.html b/src/main/resources/templates/fragments/navbar.html index c2f88dff..987b6d0e 100644 --- a/src/main/resources/templates/fragments/navbar.html +++ b/src/main/resources/templates/fragments/navbar.html @@ -1,78 +1,11 @@
- + + @@ -263,48 +196,7 @@ function compareVersions(version1, version2) {
- - +
@@ -363,73 +255,7 @@ function compareVersions(version1, version2) { - + diff --git a/src/main/resources/templates/merge-pdfs.html b/src/main/resources/templates/merge-pdfs.html index e10952d5..0574f3ad 100644 --- a/src/main/resources/templates/merge-pdfs.html +++ b/src/main/resources/templates/merge-pdfs.html @@ -27,72 +27,7 @@ - +