Lots of changes (#70)
Image extraction and conversion to formats Multi parallel file execution for all forms so you can input multiple files quickly Any file at all pdf using libreoffice, super powerful Sadly makes docker image larger but worth it OCR PDF using ocr my pdf Works awesomely for adding text to a image Improved compression using ocr my pdf app Settings page with custom download options such as - open in same window - open in new window - download - download as zip Update detection in settings page it should show notification if there is a update (very hidden) UI cleanups Add other image formats to PDF to Image Various fies to icons, and pdf.js usage
This commit is contained in:
@@ -28,7 +28,6 @@
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" th:text="#{addImage.submit}"></button>
|
||||
</form>
|
||||
<th:block th:insert="~{fragments/common :: filelist}"></th:block>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,16 +14,28 @@
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2 th:text="#{compress.header}"></h2>
|
||||
<form action="#" th:action="@{compress-pdf}" th:object="${rotateForm}" method="post" enctype="multipart/form-data">
|
||||
<p th:text="#{processTimeWarning}"></p>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false)}"></div>
|
||||
<div class="form-group">
|
||||
<label for="imageCompressionLevel" th:text="#{compress.compressLevel}"></label>
|
||||
<input type="number" class="form-control" id="imageCompressionLevel" name="imageCompressionLevel" step="1" value="1" min="1" max="100" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" th:text="#{compress.submit}"></button>
|
||||
</form>
|
||||
<th:block th:insert="~{fragments/common :: filelist}"></th:block>
|
||||
<form action="#" th:action="@{/compress-pdf}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false)}"></div>
|
||||
<div>
|
||||
<label for="optimizeLevel" th:text="#{compress.selectText.1}"></label>
|
||||
<select name="optimizeLevel" id="optimizeLevel">
|
||||
<option value="0" th:text="#{compress.selectText.2}"></option>
|
||||
<option value="1" selected th:text="#{compress.selectText.3}"></option>
|
||||
<option value="2" th:text="#{compress.selectText.4}"></option>
|
||||
<option value="3" th:text="#{compress.selectText.5}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" name="fastWebView" id="fastWebView">
|
||||
<label for="fastWebView" th:text="#{compress.selectText.6}"></label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" name="jbig2Lossy" id="jbig2Lossy">
|
||||
<label for="jbig2Lossy" th:text="#{compress.selectText.7}"></label>
|
||||
</div>
|
||||
<button type="submit" th:text="#{compress.submit}"></button>
|
||||
</form>
|
||||
<p class="mt-3" th:text="#{compress.credit}"></p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
36
src/main/resources/templates/convert/file-to-pdf.html
Normal file
36
src/main/resources/templates/convert/file-to-pdf.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{fileToPDF.title})}"></th:block>
|
||||
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
<br> <br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2 th:text="#{fileToPDF.header}"></h2>
|
||||
<p th:text="#{processTimeWarning}">
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{file-to-pdf}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false)}"></div>
|
||||
<br>
|
||||
<button type="submit" class="btn btn-primary" th:text="#{fileToPDF.submit}"></button>
|
||||
|
||||
</form>
|
||||
<p class="mt-3" th:text="#{fileToPDF.credit}"></p>
|
||||
<p class="mt-3" th:text="#{fileToPDF.supportedFileTypes}"></p>
|
||||
<p th:utext="#{fileToPDF.fileTypesList}"></p>
|
||||
<a href="https://help.libreoffice.org/latest/en-US/text/shared/guide/supported_formats.html">https://help.libreoffice.org/latest/en-US/text/shared/guide/supported_formats.html</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -23,7 +23,6 @@
|
||||
<button type="submit" class="btn btn-primary" th:text="#{imageToPDF.submit}"></button>
|
||||
|
||||
</form>
|
||||
<th:block th:insert="~{fragments/common :: filelist}"></th:block>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" th:text="#{pdfToImage.submit}"></button>
|
||||
</form>
|
||||
<th:block th:insert="~{fragments/common :: filelist}"></th:block>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{xlsToPdf.title})}"></th:block>
|
||||
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
<br> <br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2 th:text="#{xlsToPdf.header}"></h2>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{xlsx-to-pdf}">
|
||||
<div class="custom-file">
|
||||
<input type="file" class="custom-file-input" id="fileInput" name="fileInput" required>
|
||||
<label class="custom-file-label" for="fileInput" th:text="#{xlsToPdf.selectText.1}"></label>
|
||||
</div>
|
||||
<br> <br>
|
||||
<button type="submit" class="btn btn-primary" th:text="#{imageToPDF.submit}"></button>
|
||||
|
||||
</form>
|
||||
<th:block th:insert="~{fragments/common :: filelist}"></th:block>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
36
src/main/resources/templates/extract-images.html
Normal file
36
src/main/resources/templates/extract-images.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{extractImages.title})}"></th:block>
|
||||
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
<br> <br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2 th:text="#{extractImages.header}"></h2>
|
||||
|
||||
<form id="multiPdfForm" th:action="@{extract-images}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false)}"></div>
|
||||
<div class="form-group">
|
||||
<label th:text="#{extractImages.selectText}"></label>
|
||||
<select class="form-control" name="format">
|
||||
<option value="png">PNG</option>
|
||||
<option value="jpg">JPG</option>
|
||||
<option value="gif">GIF</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" th:text="#{extractImages.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -9,6 +9,9 @@
|
||||
<!-- jQuery -->
|
||||
<script src="js/jquery.min.js"></script>
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="js/jszip.min.js"></script>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<script src="js/popper.min.js"></script>
|
||||
<script src="js/bootstrap.min.js"></script>
|
||||
@@ -16,122 +19,367 @@
|
||||
<link rel="stylesheet" href="css/bootstrap-icons.css">
|
||||
|
||||
<!-- PDF.js -->
|
||||
<script src="pdfjs/pdf.min.js"></script>
|
||||
<link href="pdfjs/pdf_viewer.min.css" rel="stylesheet">
|
||||
<script src="pdfjs/pdf.js"></script>
|
||||
|
||||
<!-- Custom -->
|
||||
<link rel="stylesheet" href="css/general.css">
|
||||
<link rel="stylesheet" th:href="@{css/dark-mode.css}" id="dark-mode-styles">
|
||||
|
||||
<script>
|
||||
function toggleDarkMode() {
|
||||
var checkbox = document.getElementById("toggle-dark-mode");
|
||||
var darkModeStyles = document.getElementById("dark-mode-styles");
|
||||
if (checkbox.checked) {
|
||||
localStorage.setItem("dark-mode", "on");
|
||||
darkModeStyles.disabled = false;
|
||||
} else {
|
||||
localStorage.setItem("dark-mode", "off");
|
||||
darkModeStyles.disabled = true;
|
||||
}
|
||||
}
|
||||
$(document).ready(function() {
|
||||
var darkModeStyles = document.getElementById("dark-mode-styles");
|
||||
var checkbox = document.getElementById("toggle-dark-mode");
|
||||
if (localStorage.getItem("dark-mode") == "on") {
|
||||
darkModeStyles.disabled = false;
|
||||
checkbox.checked = true;
|
||||
}
|
||||
if (localStorage.getItem("dark-mode") == "off") {
|
||||
darkModeStyles.disabled = true;
|
||||
checkbox.checked = false;
|
||||
}
|
||||
function toggleDarkMode() {
|
||||
var checkbox = document.getElementById("toggle-dark-mode");
|
||||
var darkModeStyles = document.getElementById("dark-mode-styles");
|
||||
if (checkbox.checked) {
|
||||
localStorage.setItem("dark-mode", "on");
|
||||
darkModeStyles.disabled = false;
|
||||
} else {
|
||||
localStorage.setItem("dark-mode", "off");
|
||||
darkModeStyles.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var darkModeStyles = document.getElementById("dark-mode-styles");
|
||||
var checkbox = document.getElementById("toggle-dark-mode");
|
||||
|
||||
// Check if the user has already set a preference
|
||||
if (localStorage.getItem("dark-mode") == "on") {
|
||||
darkModeStyles.disabled = false;
|
||||
checkbox.checked = true;
|
||||
} else if (localStorage.getItem("dark-mode") == "off") {
|
||||
darkModeStyles.disabled = true;
|
||||
checkbox.checked = false;
|
||||
} else {
|
||||
// Check the OS's default dark mode setting
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
darkModeStyles.disabled = false;
|
||||
checkbox.checked = true;
|
||||
} else {
|
||||
darkModeStyles.disabled = true;
|
||||
checkbox.checked = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<th:block th:fragment="filelist">
|
||||
<div id="fileList"></div>
|
||||
<div id="fileList2"></div>
|
||||
<script>
|
||||
var input = document.getElementById("fileInput");
|
||||
var output = document.getElementById("fileList");
|
||||
|
||||
input.addEventListener("change", function() {
|
||||
var files = input.files;
|
||||
var fileNames = "";
|
||||
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
fileNames += (i + 1) + ". " + files[i].name + "<br>";
|
||||
}
|
||||
|
||||
output.innerHTML = fileNames;
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
var input2 = document.getElementById("fileInput2");
|
||||
var output2 = document.getElementById("fileList2");
|
||||
if (input2 != null && !input2) {
|
||||
input2.addEventListener("change", function() {
|
||||
var files = input2.files;
|
||||
var fileNames = "";
|
||||
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
fileNames += (i + 1) + ". " + files[i].name + "<br>";
|
||||
}
|
||||
|
||||
output2.innerHTML = fileNames;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<script>
|
||||
if (dropContainer) {
|
||||
dropContainer.ondragover = dropContainer.ondragenter = function(evt) {
|
||||
evt.preventDefault();
|
||||
};
|
||||
|
||||
dropContainer.ondrop = function(evt) {
|
||||
if (fileInput) {
|
||||
fileInput.files = evt.dataTransfer.files;
|
||||
|
||||
const dT = new DataTransfer();
|
||||
dT.items.add(evt.dataTransfer.files[0]);
|
||||
dT.items.add(evt.dataTransfer.files[3]);
|
||||
fileInput.files = dT.files;
|
||||
|
||||
evt.preventDefault();
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</th:block>
|
||||
|
||||
<th:block th:fragment="fileSelector(name, multiple)">
|
||||
<div class="custom-file-chooser">
|
||||
<div class="custom-file">
|
||||
<input type="file" class="custom-file-input" th:name="${name}" th:id="${name}+'-input'" th:multiple="${multiple}">
|
||||
<label class="custom-file-label" th:for="${name}+'-input'" th:text="#{pdfPrompt}"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom-file">
|
||||
<input type="file" class="custom-file-input" th:name="${name}" th:id="${name}+'-input'" multiple>
|
||||
<label class="custom-file-label" th:for="${name}+'-input'" th:text="#{pdfPrompt}"></label>
|
||||
</div>
|
||||
<div class="selected-files"></div>
|
||||
</div>
|
||||
<br>
|
||||
<div id="progressBarContainer" style="display: none; position: relative;">
|
||||
<div class="progress" style="height: 1rem;">
|
||||
<div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
$('form').submit(function(event) {
|
||||
console.log("start download code")
|
||||
var files = $('#fileInput-input')[0].files;
|
||||
var url = this.action;
|
||||
console.log(url)
|
||||
event.preventDefault(); // Prevent the default form handling behavior
|
||||
/* Check if ${multiple} is false */
|
||||
var multiple = [[${multiple}]] || false;
|
||||
if (!multiple && files.length > 1) {
|
||||
console.log("multi parallel download")
|
||||
submitMultiPdfForm(event,url);
|
||||
} else {
|
||||
console.log("start single download")
|
||||
|
||||
// Get the selected download option from localStorage
|
||||
const downloadOption = localStorage.getItem('downloadOption');
|
||||
|
||||
var formData = new FormData($('form')[0]);
|
||||
|
||||
// Send the request to the server using the fetch() API
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(response => {
|
||||
if (!response) {
|
||||
throw new Error('Received null response for file ' + i);
|
||||
}
|
||||
console.log("load single download")
|
||||
|
||||
|
||||
// Extract the filename from the Content-Disposition header, if present
|
||||
let filename = null;
|
||||
const contentDispositionHeader = response.headers.get('Content-Disposition');
|
||||
console.log(contentDispositionHeader)
|
||||
if (contentDispositionHeader && contentDispositionHeader.indexOf('attachment') !== -1) {
|
||||
filename = contentDispositionHeader.split('filename=')[1].replace(/"/g, '');
|
||||
} else {
|
||||
// If the Content-Disposition header is not present or does not contain the filename, use a default filename
|
||||
filename = 'download';
|
||||
}
|
||||
console.log("filename=" + filename)
|
||||
|
||||
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
console.log("contentType=" + contentType)
|
||||
// Check if the response is a PDF or an image
|
||||
if (contentType.includes('pdf') || contentType.includes('image')) {
|
||||
response.blob().then(blob => {
|
||||
console.log("pdf/image")
|
||||
|
||||
// Perform the appropriate action based on the download option
|
||||
if (downloadOption === 'sameWindow') {
|
||||
console.log("same window")
|
||||
|
||||
// Open the file in the same window
|
||||
window.location.href = URL.createObjectURL(blob);
|
||||
} else if (downloadOption === 'newWindow') {
|
||||
console.log("new window")
|
||||
|
||||
// Open the file in a new window
|
||||
window.open(URL.createObjectURL(blob), '_blank');
|
||||
} else {
|
||||
console.log("else save")
|
||||
|
||||
// Download the file
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
});
|
||||
} else if (contentType.includes('json')) {
|
||||
// Handle the JSON response
|
||||
response.json().then(data => {
|
||||
// Format the error message
|
||||
const errorMessage = JSON.stringify(data, null, 2);
|
||||
|
||||
// Display the error message in an alert
|
||||
alert(`An error occurred: ${errorMessage}`);
|
||||
});
|
||||
|
||||
} else {
|
||||
response.blob().then(blob => {
|
||||
console.log("else save 2 zip")
|
||||
|
||||
// For ZIP files or other file types, just download the file
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log("error listener")
|
||||
|
||||
// Extract the error message and stack trace from the response
|
||||
const errorMessage = error.message;
|
||||
const stackTrace = error.stack;
|
||||
|
||||
// Create an error message to display to the user
|
||||
const message = `${errorMessage}\n\n${stackTrace}`;
|
||||
|
||||
// Display the error message to the user
|
||||
alert(message);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function submitMultiPdfForm(event,url) {
|
||||
// Get the selected PDF files
|
||||
var files = $('#fileInput-input')[0].files;
|
||||
|
||||
// Get the existing form data
|
||||
var formData = new FormData($('form')[0]);
|
||||
formData.delete('fileInput');
|
||||
|
||||
// Show the progress bar
|
||||
$('#progressBarContainer').show();
|
||||
|
||||
// Initialize the progress bar
|
||||
var progressBar = $('#progressBar');
|
||||
progressBar.css('width', '0%');
|
||||
progressBar.attr('aria-valuenow', 0);
|
||||
progressBar.attr('aria-valuemax', files.length);
|
||||
|
||||
// Check the flag in localStorage
|
||||
const zipFiles = localStorage.getItem('zipParallelFiles') === 'true';
|
||||
|
||||
// Initialize JSZip instance if needed
|
||||
let jszip = null;
|
||||
if (zipFiles) {
|
||||
jszip = new JSZip();
|
||||
}
|
||||
|
||||
|
||||
// Submit each PDF file in parallel
|
||||
var promises = [];
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
var fileFormData = new FormData();
|
||||
fileFormData.append('fileInput', files[i]);
|
||||
for (var pair of formData.entries()) {
|
||||
fileFormData.append(pair[0], pair[1]);
|
||||
}
|
||||
console.log(fileFormData);
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
body: fileFormData
|
||||
}).then(function(response) {
|
||||
if (!response) {
|
||||
throw new Error('Received null response for file ' + i);
|
||||
}
|
||||
console.log('Received response for file ' + i + ': ' + response);
|
||||
|
||||
var contentDisposition = response.headers.get('content-disposition');
|
||||
var fileName = contentDisposition.split('filename=')[1].replace(/"/g, '');
|
||||
|
||||
response.blob().then(function (blob) {
|
||||
if (zipFiles) {
|
||||
// Add the file to the ZIP archive
|
||||
jszip.file(fileName, blob);
|
||||
resolve();
|
||||
} else {
|
||||
// Download the file directly
|
||||
var url = window.URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
}).catch(function(error) {
|
||||
console.error('Error submitting request for file ' + i + ': ' + error);
|
||||
|
||||
// Set default values or fallbacks for error properties
|
||||
var status = error && error.status || 500;
|
||||
var statusText = error && error.statusText || 'Internal Server Error';
|
||||
var message = error && error.message || 'An error occurred while processing your request.';
|
||||
|
||||
// Reject the Promise to signal that the request has failed
|
||||
reject();
|
||||
// Redirect to error page with Spring Boot error parameters
|
||||
var url = '/error?status=' + status + '&error=' + encodeURIComponent(statusText) + '&message=' + encodeURIComponent(message);
|
||||
window.location.href = url;
|
||||
});
|
||||
});
|
||||
|
||||
// Update the progress bar as each request finishes
|
||||
promise.then(function() {
|
||||
var 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);
|
||||
});
|
||||
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
// Wait for all requests to finish
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
console.error('Error while uploading files: ' + error);
|
||||
}
|
||||
|
||||
// Update the progress bar
|
||||
progressBar.css('width', '100%');
|
||||
progressBar.attr('aria-valuenow', files.length);
|
||||
|
||||
// After all requests are finished, download the ZIP file if needed
|
||||
if (zipFiles) {
|
||||
jszip.generateAsync({ type: "blob" }).then(function (content) {
|
||||
var url = window.URL.createObjectURL(content);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = "files.zip";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<script th:inline="javascript">
|
||||
$([[${"#"+name+"-input"}]]).on("change", function() {
|
||||
const files = $(this).get(0).files;
|
||||
const fileNames = Array.from(files).map(f => f.name).join(", ");
|
||||
if (fileNames) {
|
||||
$(this).siblings(".custom-file-label").addClass("selected").html(fileNames);
|
||||
} else {
|
||||
$(this).siblings(".custom-file-label").addClass("selected").html([[#{pdfPrompt}]]);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('fileInput-input');
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
$([[${"#"+name+"-input"}]]).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("<div>" + fileName + "</div>");
|
||||
});
|
||||
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 + " files selected");
|
||||
} else {
|
||||
$(inputElement).siblings(".custom-file-label").addClass("selected").html([[#{pdfPrompt}]]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.custom-file-label {
|
||||
padding-right: 90px;
|
||||
}
|
||||
padding-right: 90px;
|
||||
}
|
||||
|
||||
.selected-files {
|
||||
margin-top: 10px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
</th:block>
|
||||
@@ -1,4 +1,5 @@
|
||||
<th:block th:fragment="errorBanner">
|
||||
|
||||
<style>
|
||||
#github-button,
|
||||
#discord-button {
|
||||
@@ -17,7 +18,7 @@
|
||||
background-color: #005b7f;
|
||||
}
|
||||
</style>
|
||||
<br th:if="${message}">
|
||||
<br th:if="${message}">
|
||||
<div id="errorContainer" th:if="${message}" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<h4 class="alert-heading" th:text="'Error: ' + ${status} + ' ' + ${error}"></h4>
|
||||
<p th:text="${message} + ' for path: ' + ${path}"></p>
|
||||
@@ -36,39 +37,53 @@
|
||||
<a href="https://discord.gg/Cn8pWhQRxZ" id="discord-button" target="_blank">Join our Discord server</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggletrace() {
|
||||
var traceDiv = document.getElementById("trace");
|
||||
if (traceDiv.style.maxHeight === "0px") {
|
||||
traceDiv.style.maxHeight = "500px";
|
||||
} else {
|
||||
traceDiv.style.maxHeight = "0px";
|
||||
<script>
|
||||
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();
|
||||
}
|
||||
adjustContainerHeight();
|
||||
}
|
||||
|
||||
function copytrace() {
|
||||
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();
|
||||
}
|
||||
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 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");
|
||||
errorContainer.style.height = errorContainer.scrollHeight - traceDiv.scrollHeight + traceDiv.offsetHeight + "px";
|
||||
}
|
||||
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";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</th:block>
|
||||
</th:block>
|
||||
@@ -1,6 +1,7 @@
|
||||
<div th:fragment="footer">
|
||||
<footer id="footer" class="text-center py-3">
|
||||
<a href="https://github.com/Frooodle/Stirling-PDF" target="_blank" class="mx-1"><img src="images/github.svg"></img></a>
|
||||
<a href="https://hub.docker.com/r/frooodle/s-pdf" target="_blank" class="mx-1"><img src="images/docker.svg"></img></a>
|
||||
<a href="https://hub.docker.com/r/frooodle/s-pdf" target="_blank" class="mx-1"><img src="images/docker.svg"></img></a>
|
||||
<a href="https://discord.gg/Cn8pWhQRxZ" target="_blank" class="mx-1"><img src="images/discord.svg"></img></a>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -1,133 +1,271 @@
|
||||
<div th:fragment="navbar">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<div class="container">
|
||||
<div th:fragment="navbar" class="mx-auto">
|
||||
<script>
|
||||
document
|
||||
.addEventListener(
|
||||
'DOMContentLoaded',
|
||||
function() {
|
||||
// Get the dropdown items
|
||||
var dropdownItems = document
|
||||
.querySelectorAll('.lang_dropdown-item');
|
||||
|
||||
<a class="navbar-brand" href="#" th:href="@{/}">Stirling PDF</a>
|
||||
// Loop through the dropdown items
|
||||
for (var i = 0; i < dropdownItems.length; i++) {
|
||||
dropdownItems[i].classList
|
||||
.remove('active');
|
||||
if (dropdownItems[i].dataset.languageCode === localStorage
|
||||
.getItem('languageCode')) {
|
||||
dropdownItems[i].classList
|
||||
.add('active');
|
||||
}
|
||||
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse"
|
||||
data-target="#navbarNav" aria-controls="navbarNav"
|
||||
aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
// Add a click event listener to each dropdown item
|
||||
dropdownItems[i]
|
||||
.addEventListener(
|
||||
'click',
|
||||
function(event) {
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
// Prevent the default link behavior
|
||||
event
|
||||
.preventDefault();
|
||||
|
||||
<ul class="navbar-nav">
|
||||
// Get the language code
|
||||
var languageCode = this.dataset.languageCode;
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" th:href="@{merge-pdfs}" th:classappend="${currentPage}=='merge-pdfs' ? 'active' : ''" th:text="#{home.merge.title}"></a>
|
||||
</li>
|
||||
// Save the language code to local storage
|
||||
localStorage
|
||||
.setItem(
|
||||
'languageCode',
|
||||
languageCode);
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" th:href="@{split-pdfs}" th:classappend="${currentPage}=='split-pdfs' ? 'active' : ''" th:text="#{home.split.title}"></a>
|
||||
</li>
|
||||
// Get the current URL
|
||||
var currentUrl = window.location.href;
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" th:href="@{pdf-organizer}" th:classappend="${currentPage}=='pdf-organizer' ? 'active' : ''" th:text="#{home.pdfOrganiser.title}"></a>
|
||||
</li>
|
||||
// Check if the URL already contains a "?lang" query parameter
|
||||
if (currentUrl
|
||||
.indexOf('?lang=') === -1) {
|
||||
// Update the URL with the "?lang" query parameter
|
||||
window.location.href = currentUrl
|
||||
+ '?lang='
|
||||
+ languageCode;
|
||||
} else {
|
||||
// Replace the "?lang" query parameter with the new value
|
||||
window.location.href = currentUrl
|
||||
.replace(
|
||||
/\?lang=\w{2,}/,
|
||||
'?lang='
|
||||
+ languageCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" th:href="@{rotate-pdf}" th:classappend="${currentPage}=='rotate-pdf' ? 'active' : ''" th:text="#{home.rotate.title}"></a>
|
||||
</li>
|
||||
<script th:inline="javascript">
|
||||
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);
|
||||
}
|
||||
|
||||
<li class="nav-item dropdown" th:classappend="${currentPage}=='pdf-to-img' OR ${currentPage}=='img-to-pdf' OR ${currentPage}=='xlsx-to-pdf' ? 'active' : ''">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" th:text="#{navbar.convert}"></a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="#" th:href="@{pdf-to-img}" th:classappend="${currentPage}=='pdf-to-img' ? 'active' : ''" th:text="#{home.pdfToImage.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{img-to-pdf}" th:classappend="${currentPage}=='img-to-pdf' ? 'active' : ''" th:text="#{home.imageToPdf.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{xlsx-to-pdf}" th:classappend="${currentPage}=='xlsx-to-pdf' ? 'active' : ''" th:text="#{home.xlsToPdf.title}"></a>
|
||||
</div>
|
||||
</li>
|
||||
const currentVersion = /*[[${@appVersion}]]*/ ''; // Replace with your current version number
|
||||
|
||||
<li class="nav-item dropdown" th:classappend="${currentPage}=='add-password' OR ${currentPage}=='remove-password' OR ${currentPage}=='change-permissions' OR ${currentPage}=='add-watermark' OR ${currentPage}=='remove-watermark' ? 'active' : ''">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" th:text="#{navbar.security}"></a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="#" th:href="@{add-password}" th:classappend="${currentPage}=='add-password' ? 'active' : ''" th:text="#{home.addPassword.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{remove-password}" th:classappend="${currentPage}=='remove-password' ? 'active' : ''" th:text="#{home.removePassword.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{change-permissions}" th:classappend="${currentPage}=='change-permissions' ? 'active' : ''" th:text="#{home.permissions.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{add-watermark}" th:classappend="${currentPage}=='add-watermark' ? 'active' : ''" th:text="#{home.watermark.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{change-metadata}" th:classappend="${currentPage}=='change-metadata' ? 'active' : ''" th:text="#{home.changeMetadata.title}"></a>
|
||||
</div>
|
||||
</li>
|
||||
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.visibility = "visible";
|
||||
console.log("visible")
|
||||
} else {
|
||||
document.getElementById("update-btn").style.visibility = "hidden";
|
||||
console.log("hidden")
|
||||
}
|
||||
}
|
||||
|
||||
<li class="nav-item dropdown" th:classappend="${currentPage}=='remove-pages' OR ${currentPage}=='add-image' OR ${currentPage}=='compress-pdf' ? 'active' : ''">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" th:text="#{navbar.other}"></a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="#" th:href="@{add-image}" th:classappend="${currentPage}=='add-image' ? 'active' : ''" th:text="#{home.addImage.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{compress-pdf}" th:classappend="${currentPage}=='compress-pdf' ? 'active' : ''" th:text="#{home.compressPdfs.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{remove-pages}" th:classappend="${currentPage}=='remove-pages' ? 'active' : ''" th:text="#{home.removePages.title}"></a>
|
||||
</div>
|
||||
</li>
|
||||
checkForUpdate();
|
||||
</script>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" th:href="@{add-image}" th:classappend="${currentPage}=='add-image' ? 'active' : ''"></a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" th:href="@{compress-pdf}" th:classappend="${currentPage}=='compress-pdf' ? 'active' : ''"></a>
|
||||
</li>
|
||||
|
||||
<input type="checkbox" id="toggle-dark-mode" checked="true" th:onclick="javascript:toggleDarkMode()">
|
||||
<a class="nav-link" href="#" for="toggle-dark-mode" th:text="#{navbar.darkmode}"></a>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<div class="container ">
|
||||
|
||||
<a class="navbar-brand" href="#" th:href="@{/}">Stirling PDF</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
|
||||
<ul class="navbar-nav mr-auto flex-nowrap">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="#" th:href="@{merge-pdfs}" th:classappend="${currentPage}=='merge-pdfs' ? 'active' : ''" th:text="#{home.merge.title}"></a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="#" th:href="@{split-pdfs}" th:classappend="${currentPage}=='split-pdfs' ? 'active' : ''" th:text="#{home.split.title}"></a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="#" th:href="@{pdf-organizer}" th:classappend="${currentPage}=='pdf-organizer' ? 'active' : ''" th:text="#{home.pdfOrganiser.title}"></a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="#" th:href="@{rotate-pdf}" th:classappend="${currentPage}=='rotate-pdf' ? 'active' : ''" th:text="#{home.rotate.title}"></a></li>
|
||||
|
||||
<li class="nav-item dropdown" th:classappend="${currentPage}=='pdf-to-img' OR ${currentPage}=='img-to-pdf' OR ${currentPage}=='file-to-pdf' OR ${currentPage}=='xlsx-to-pdf' ? 'active' : ''"><a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false" th:text="#{navbar.convert}"></a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="#" th:href="@{pdf-to-img}" th:classappend="${currentPage}=='pdf-to-img' ? 'active' : ''" th:text="#{home.pdfToImage.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{img-to-pdf}" th:classappend="${currentPage}=='img-to-pdf' ? 'active' : ''" th:text="#{home.imageToPdf.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{file-to-pdf}" th:classappend="${currentPage}=='file-to-pdf' ? 'active' : ''" th:text="#{home.fileToPDF.title}"></a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
|
||||
<li class="nav-item dropdown" th:classappend="${currentPage}=='add-password' OR ${currentPage}=='remove-password' OR ${currentPage}=='change-permissions' OR ${currentPage}=='add-watermark' OR ${currentPage}=='remove-watermark' ? 'active' : ''">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" th:text="#{navbar.security}"></a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="#" th:href="@{add-password}" th:classappend="${currentPage}=='add-password' ? 'active' : ''" th:text="#{home.addPassword.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{remove-password}" th:classappend="${currentPage}=='remove-password' ? 'active' : ''" th:text="#{home.removePassword.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{change-permissions}" th:classappend="${currentPage}=='change-permissions' ? 'active' : ''" th:text="#{home.permissions.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{add-watermark}" th:classappend="${currentPage}=='add-watermark' ? 'active' : ''" th:text="#{home.watermark.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{change-metadata}" th:classappend="${currentPage}=='change-metadata' ? 'active' : ''" th:text="#{home.changeMetadata.title}"></a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
|
||||
<li class="nav-item dropdown" th:classappend="${currentPage}=='remove-pages' OR ${currentPage}=='add-image' OR ${currentPage}=='ocr-pdf' OR ${currentPage}=='extract-images' OR ${currentPage}=='compress-pdf' ? 'active' : ''"><a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false" th:text="#{navbar.other}"></a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="#" th:href="@{ocr-pdf}" th:classappend="${currentPage}=='ocr-pdf' ? 'active' : ''" th:text="#{home.ocr.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{add-image}" th:classappend="${currentPage}=='add-image' ? 'active' : ''" th:text="#{home.addImage.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{compress-pdf}" th:classappend="${currentPage}=='compress-pdf' ? 'active' : ''" th:text="#{home.compressPdfs.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{remove-pages}" th:classappend="${currentPage}=='remove-pages' ? 'active' : ''" th:text="#{home.removePages.title}"></a>
|
||||
<a class="dropdown-item" href="#" th:href="@{extract-images}" th:classappend="${currentPage}=='extract-images' ? 'active' : ''" th:text="#{home.extractImages.title}"></a>
|
||||
</div></li>
|
||||
|
||||
|
||||
|
||||
<input type="checkbox" id="toggle-dark-mode" checked="true" th:onclick="javascript:toggleDarkMode()">
|
||||
<a class="nav-link" href="#" for="toggle-dark-mode" th:text="#{navbar.darkmode}" ></a>
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="languageDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="bi bi-globe2"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="languageDropdown">
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="en_US">English (US)</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="en_GB">English (UK)</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="ar_AR">العربية</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="de_DE">Deutsch</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="fr_FR">Français</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Get the dropdown items
|
||||
var dropdownItems = document.querySelectorAll('.lang_dropdown-item');
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="languageDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-globe2" viewBox="0 0 20 20">
|
||||
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855-.143.268-.276.56-.395.872.705.157 1.472.257 2.282.287V1.077zM4.249 3.539c.142-.384.304-.744.481-1.078a6.7 6.7 0 0 1 .597-.933A7.01 7.01 0 0 0 3.051 3.05c.362.184.763.349 1.198.49zM3.509 7.5c.036-1.07.188-2.087.436-3.008a9.124 9.124 0 0 1-1.565-.667A6.964 6.964 0 0 0 1.018 7.5h2.49zm1.4-2.741a12.344 12.344 0 0 0-.4 2.741H7.5V5.091c-.91-.03-1.783-.145-2.591-.332zM8.5 5.09V7.5h2.99a12.342 12.342 0 0 0-.399-2.741c-.808.187-1.681.301-2.591.332zM4.51 8.5c.035.987.176 1.914.399 2.741A13.612 13.612 0 0 1 7.5 10.91V8.5H4.51zm3.99 0v2.409c.91.03 1.783.145 2.591.332.223-.827.364-1.754.4-2.741H8.5zm-3.282 3.696c.12.312.252.604.395.872.552 1.035 1.218 1.65 1.887 1.855V11.91c-.81.03-1.577.13-2.282.287zm.11 2.276a6.696 6.696 0 0 1-.598-.933 8.853 8.853 0 0 1-.481-1.079 8.38 8.38 0 0 0-1.198.49 7.01 7.01 0 0 0 2.276 1.522zm-1.383-2.964A13.36 13.36 0 0 1 3.508 8.5h-2.49a6.963 6.963 0 0 0 1.362 3.675c.47-.258.995-.482 1.565-.667zm6.728 2.964a7.009 7.009 0 0 0 2.275-1.521 8.376 8.376 0 0 0-1.197-.49 8.853 8.853 0 0 1-.481 1.078 6.688 6.688 0 0 1-.597.933zM8.5 11.909v3.014c.67-.204 1.335-.82 1.887-1.855.143-.268.276-.56.395-.872A12.63 12.63 0 0 0 8.5 11.91zm3.555-.401c.57.185 1.095.409 1.565.667A6.963 6.963 0 0 0 14.982 8.5h-2.49a13.36 13.36 0 0 1-.437 3.008zM14.982 7.5a6.963 6.963 0 0 0-1.362-3.675c-.47.258-.995.482-1.565.667.248.92.4 1.938.437 3.008h2.49zM11.27 2.461c.177.334.339.694.482 1.078a8.368 8.368 0 0 0 1.196-.49 7.01 7.01 0 0 0-2.275-1.52c.218.283.418.597.597.932zm-.488 1.343a7.765 7.765 0 0 0-.395-.872C9.835 1.897 9.17 1.282 8.5 1.077V4.09c.81-.03 1.577-.13 2.282-.287z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="languageDropdown">
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="en_US">English (US)</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="en_GB">English (UK)</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="ar_AR">العربية</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="de_DE">Deutsch</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="fr_FR">Français</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<!-- Settings Button -->
|
||||
<button type="button" class="btn btn-secondary float-right" data-toggle="modal" data-target="#settingsModal">
|
||||
<span class="glyphicon glyphicon-cog"></span><span th:text="#{navbar.settings}"></span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
// Loop through the dropdown items
|
||||
for (var i = 0; i < dropdownItems.length; i++) {
|
||||
dropdownItems[i].classList.remove('active');
|
||||
if (dropdownItems[i].dataset.languageCode === localStorage.getItem('languageCode')) {
|
||||
dropdownItems[i].classList.add('active');
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
// Add a click event listener to each dropdown item
|
||||
dropdownItems[i].addEventListener('click', function(event) {
|
||||
</div>
|
||||
</nav>
|
||||
<div th:insert="~{fragments/errorBanner.html :: errorBanner}"></div>
|
||||
|
||||
// Prevent the default link behavior
|
||||
event.preventDefault();
|
||||
<div class="modal fade" id="settingsModal" tabindex="-1" role="dialog" aria-labelledby="settingsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content dark-card">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="settingsModalLabel" th:text="#{settings.title}"></h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<p class="mb-0" th:text="#{settings.appVersion} + ' ' + ${@appVersion}"></p>
|
||||
<a href="https://github.com/Frooodle/Stirling-PDF/releases" target="_blank">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="update-btn" th:text="#{settings.update}"></button>
|
||||
</a>
|
||||
|
||||
// Get the language code
|
||||
var languageCode = this.dataset.languageCode;
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="downloadOption" th:text="#{settings.downloadOption.title}"></label>
|
||||
<select class="form-control" id="downloadOption">
|
||||
<option value="sameWindow" th:text="#{settings.downloadOption.1}"></option>
|
||||
<option value="newWindow" th:text="#{settings.downloadOption.2}"></option>
|
||||
<option value="downloadFile" th:text="#{settings.downloadOption.3}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="zipParallelFiles">
|
||||
<label class="custom-control-label" for="zipParallelFiles" th:text="#{settings.zip}"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal" th:text="#{close}"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Save the language code to local storage
|
||||
localStorage.setItem('languageCode', languageCode);
|
||||
|
||||
// Get the current URL
|
||||
var currentUrl = window.location.href;
|
||||
<script>
|
||||
// Get the download option from local storage, or set it to 'sameWindow' if it doesn't exist
|
||||
var downloadOption = localStorage.getItem('downloadOption')
|
||||
|| 'sameWindow';
|
||||
|
||||
// Check if the URL already contains a "?lang" query parameter
|
||||
if (currentUrl.indexOf('?lang=') === -1) {
|
||||
// Update the URL with the "?lang" query parameter
|
||||
window.location.href = currentUrl + '?lang=' + languageCode;
|
||||
} else {
|
||||
// Replace the "?lang" query parameter with the new value
|
||||
window.location.href = currentUrl.replace(/\?lang=\w{2,}/, '?lang=' + languageCode);
|
||||
}
|
||||
// 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 zipParallelFiles flag from local storage, or set it to false if it doesn't exist
|
||||
var zipParallelFiles = localStorage.getItem('zipParallelFiles') === 'true';
|
||||
|
||||
// Set the checked state of the checkbox
|
||||
document.getElementById('zipParallelFiles').checked = zipParallelFiles;
|
||||
|
||||
// Save the checked state of the checkbox to local storage when it changes
|
||||
document.getElementById('zipParallelFiles').addEventListener('change', function () {
|
||||
zipParallelFiles = this.checked;
|
||||
localStorage.setItem('zipParallelFiles', zipParallelFiles);
|
||||
});
|
||||
</script>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div th:insert="~{fragments/errorBanner.html :: errorBanner}"></div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.addImage.title}, cardText=#{home.addImage.desc}, cardLink='add-image')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.watermark.title}, cardText=#{home.watermark.desc}, cardLink='add-watermark')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.xlsToPdf.title}, cardText=#{home.xlsToPdf.desc}, cardLink='xlsx-to-pdf')}"></div>
|
||||
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.fileToPDF.title}, cardText=#{home.fileToPDF.desc}, cardLink='file-to-pdf')}"></div>
|
||||
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.removePages.title}, cardText=#{home.removePages.desc}, cardLink='remove-pages')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.addPassword.title}, cardText=#{home.addPassword.desc}, cardLink='add-password')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.removePassword.title}, cardText=#{home.removePassword.desc}, cardLink='remove-password')}"></div>
|
||||
@@ -60,6 +60,10 @@
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.changeMetadata.title}, cardText=#{home.changeMetadata.desc}, cardLink='change-metadata')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.permissions.title}, cardText=#{home.permissions.desc}, cardLink='change-permissions')}"></div>
|
||||
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.ocr.title}, cardText=#{home.ocr.desc}, cardLink='ocr-pdf')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.extractImages.title}, cardText=#{home.extractImages.desc}, cardLink='extract-images')}"></div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,94 +28,103 @@
|
||||
<button type="submit" class="btn btn-primary" th:text="#{merge.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
<style>
|
||||
|
||||
.list-group-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filename {
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.arrows {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.move-up span,
|
||||
.move-down span {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
<script>
|
||||
document
|
||||
.getElementById(
|
||||
"fileInput")
|
||||
.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 d-flex justify-content-between align-items-center";
|
||||
item.textContent = files[i].name;
|
||||
item.innerHTML += '<div><button class="btn btn-secondary move-up"><i class="fas fa-arrow-up"></i></button> <button class="btn btn-secondary move-down"><i class="fas fa-arrow-down"></i></button></div>';
|
||||
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.parentNode.parentNode;
|
||||
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.parentNode.parentNode;
|
||||
var grandParent = parent.parentNode;
|
||||
if (parent.nextElementSibling) {
|
||||
grandParent
|
||||
.insertBefore(
|
||||
parent.nextElementSibling,
|
||||
parent);
|
||||
updateFiles();
|
||||
}
|
||||
});
|
||||
}
|
||||
document.getElementById("fileInput").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 = `
|
||||
<div class="d-flex justify-content-between align-items-center w-100">
|
||||
<div class="filename">${files[i].name}</div>
|
||||
<div class="arrows d-flex">
|
||||
<button class="btn btn-secondary move-up"><span>↑</span></button>
|
||||
<button class="btn btn-secondary move-down"><span>↓</span></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
list.appendChild(item);
|
||||
}
|
||||
|
||||
function updateFiles() {
|
||||
var dataTransfer = new DataTransfer();
|
||||
var liElements = document
|
||||
.querySelectorAll("#selectedFiles li");
|
||||
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").files = dataTransfer.files;
|
||||
}
|
||||
});
|
||||
|
||||
for (var i = 0; i < liElements.length; i++) {
|
||||
var fileNameFromList = liElements[i].innerText
|
||||
.replace(
|
||||
"\nMove Up Move Down",
|
||||
"");
|
||||
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").files = dataTransfer.files;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
46
src/main/resources/templates/ocr-pdf.html
Normal file
46
src/main/resources/templates/ocr-pdf.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{ocr.title})}"></th:block>
|
||||
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
<br> <br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2 th:text="#{ocr.header}"></h2>
|
||||
|
||||
<form action="#" th:action="@{/ocr-pdf}" method="post" enctype="multipart/form-data" class="mb-3">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false)}"></div>
|
||||
<div class="form-group">
|
||||
<label for="languages" class="form-label" th:text="#{ocr.selectText.1}"></label>
|
||||
<div id="languages">
|
||||
<div th:each="language: ${languages}">
|
||||
<input type="checkbox" class="form-check-input" th:name="languages" th:value="${language}" th:id="${'language-' + language}" />
|
||||
<label class="form-check-label" th:for="${'language-' + language}" th:text="${language}"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="form-group">
|
||||
<input type="checkbox" class="form-check-input" name="sidecar" id="sidecar" />
|
||||
<label class="form-check-label" for="sidecar" th:text="#{ocr.selectText.2}"></label>
|
||||
</div> -->
|
||||
<button type="submit" class="btn btn-primary" th:text="#{ocr.submit}"></button>
|
||||
</form>
|
||||
<p th:text="#{ocr.credit}"></p>
|
||||
<p th:text="#{ocr.help}"></p>
|
||||
<a href="https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md">https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -22,7 +22,6 @@
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" th:text="#{pdfOrganiser.submit}"></button>
|
||||
</form>
|
||||
<th:block th:insert="~{fragments/common :: filelist}"></th:block>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" th:text="#{pageRemover.submit}"></button>
|
||||
</form>
|
||||
<th:block th:insert="~{fragments/common :: filelist}"></th:block>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="col-md-6">
|
||||
<h2 th:text="#{remove-watermark.header}"></h2>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" action="remove-text">
|
||||
<form method="post" enctype="multipart/form-data" action="remove-watermark">
|
||||
<div class="form-group">
|
||||
<label th:text="#{remove-watermark.selectText.1}"></label>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false)}"></div>
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
<br>
|
||||
<button type="submit" class="btn btn-primary" th:text="#{split.submit}"></button>
|
||||
</form>
|
||||
<th:block th:insert="~{fragments/common :: filelist}"></th:block>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user