Compare commits

..

No commits in common. "def0552f244fdeef75b69b05c5342b4c46c091d7" and "9f5f333f5769fe81c49a1a19a4771b24cc143e71" have entirely different histories.

20 changed files with 119 additions and 120 deletions

View File

@ -134,7 +134,7 @@ Stirling-PDF currently supports 39 languages!
| Hungarian (Magyar) (hu_HU) | ![89%](https://geps.dev/progress/89) | | Hungarian (Magyar) (hu_HU) | ![89%](https://geps.dev/progress/89) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![81%](https://geps.dev/progress/81) | | Indonesian (Bahasa Indonesia) (id_ID) | ![81%](https://geps.dev/progress/81) |
| Irish (Gaeilge) (ga_IE) | ![92%](https://geps.dev/progress/92) | | Irish (Gaeilge) (ga_IE) | ![92%](https://geps.dev/progress/92) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) | | Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) |
| Japanese (日本語) (ja_JP) | ![89%](https://geps.dev/progress/89) | | Japanese (日本語) (ja_JP) | ![89%](https://geps.dev/progress/89) |
| Korean (한국어) (ko_KR) | ![92%](https://geps.dev/progress/92) | | Korean (한국어) (ko_KR) | ![92%](https://geps.dev/progress/92) |
| Norwegian (Norsk) (no_NB) | ![86%](https://geps.dev/progress/86) | | Norwegian (Norsk) (no_NB) | ![86%](https://geps.dev/progress/86) |

View File

@ -29,7 +29,7 @@ ext {
} }
group = "stirling.software" group = "stirling.software"
version = "0.45.6" version = "0.45.5"
java { java {
// 17 is lowest but we support and recommend 21 // 17 is lowest but we support and recommend 21
@ -516,7 +516,7 @@ dependencies {
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
implementation "io.micrometer:micrometer-core:1.14.6" implementation "io.micrometer:micrometer-core:1.14.5"
implementation group: "com.google.zxing", name: "core", version: "3.5.3" implementation group: "com.google.zxing", name: "core", version: "3.5.3"
// https://mvnrepository.com/artifact/org.commonmark/commonmark // https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark:0.24.0" implementation "org.commonmark:commonmark:0.24.0"

View File

@ -109,6 +109,33 @@ public class AppConfig {
return (rateLimit != null) ? Boolean.valueOf(rateLimit) : false; return (rateLimit != null) ? Boolean.valueOf(rateLimit) : false;
} }
@Bean(name = "uploadLimit")
public long uploadLimit() {
String maxUploadSize =
applicationProperties.getSystem().getFileUploadLimit() != null
? applicationProperties.getSystem().getFileUploadLimit()
: "";
if (maxUploadSize.isEmpty()) {
return 0;
} else if (!new Regex("^[1-9][0-9]{0,2}[KMGkmg][Bb]$").matches(maxUploadSize)) {
log.error(
"Invalid maxUploadSize format. Expected format: [1-9][0-9]{0,2}[KMGkmg][Bb], but got: {}",
maxUploadSize);
return 0;
} else {
String unit = maxUploadSize.replaceAll("[1-9][0-9]{0,2}", "").toUpperCase();
String number = maxUploadSize.replaceAll("[KMGkmg][Bb]", "");
long size = Long.parseLong(number);
return switch (unit) {
case "KB" -> size * 1024;
case "MB" -> size * 1024 * 1024;
case "GB" -> size * 1024 * 1024 * 1024;
default -> 0;
};
}
}
@Bean(name = "RunningInDocker") @Bean(name = "RunningInDocker")
public boolean runningInDocker() { public boolean runningInDocker() {
return Files.exists(Paths.get("/.dockerenv")); return Files.exists(Paths.get("/.dockerenv"));

View File

@ -0,0 +1,30 @@
package stirling.software.SPDF.controller.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
@Component
@ControllerAdvice
public class GlobalUploadLimitWebController {
@Autowired() private long uploadLimit;
@ModelAttribute("uploadLimit")
public long populateUploadLimit() {
return uploadLimit;
}
@ModelAttribute("uploadLimitReadable")
public String populateReadableLimit() {
return humanReadableByteCount(uploadLimit);
}
private String humanReadableByteCount(long bytes) {
if (bytes < 1024) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(1024));
String pre = "KMGTPE".charAt(exp - 1) + "B";
return String.format("%.1f %s", bytes / Math.pow(1024, exp), pre);
}
}

View File

@ -1,55 +0,0 @@
package stirling.software.SPDF.controller.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
import java.util.regex.Pattern;
@Service
@Slf4j
public class UploadLimitService {
@Autowired
private ApplicationProperties applicationProperties;
public long getUploadLimit() {
String maxUploadSize =
applicationProperties.getSystem().getFileUploadLimit() != null
? applicationProperties.getSystem().getFileUploadLimit()
: "";
if (maxUploadSize.isEmpty()) {
return 0;
} else if (!Pattern.compile("^[1-9][0-9]{0,2}[KMGkmg][Bb]$").matcher(maxUploadSize).matches()) {
log.error(
"Invalid maxUploadSize format. Expected format: [1-9][0-9]{0,2}[KMGkmg][Bb], but got: {}",
maxUploadSize);
return 0;
} else {
String unit = maxUploadSize.replaceAll("[1-9][0-9]{0,2}", "").toUpperCase();
String number = maxUploadSize.replaceAll("[KMGkmg][Bb]", "");
long size = Long.parseLong(number);
return switch (unit) {
case "KB" -> size * 1024;
case "MB" -> size * 1024 * 1024;
case "GB" -> size * 1024 * 1024 * 1024;
default -> 0;
};
}
}
//TODO: why do this server side not client?
public String getReadableUploadLimit() {
return humanReadableByteCount(getUploadLimit());
}
private String humanReadableByteCount(long bytes) {
if (bytes < 1024) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(1024));
String pre = "KMGTPE".charAt(exp - 1) + "B";
return String.format("%.1f %s", bytes / Math.pow(1024, exp), pre);
}
}

View File

@ -10,9 +10,9 @@ multiPdfPrompt=Scegli 2 o più PDF
multiPdfDropPrompt=Scegli (o trascina e rilascia) uno o più PDF multiPdfDropPrompt=Scegli (o trascina e rilascia) uno o più PDF
imgPrompt=Scegli immagine/i imgPrompt=Scegli immagine/i
genericSubmit=Invia genericSubmit=Invia
uploadLimit=Dimensione massima del file: uploadLimit=Maximum file size:
uploadLimitExceededSingular=è troppo grande. La dimensione massima consentita è uploadLimitExceededSingular=is too large. Maximum allowed size is
uploadLimitExceededPlural=sono troppo grandi. La dimensione massima consentita è uploadLimitExceededPlural=are too large. Maximum allowed size is
processTimeWarning=Nota: Questo processo potrebbe richiedere fino a un minuto in base alla dimensione dei file processTimeWarning=Nota: Questo processo potrebbe richiedere fino a un minuto in base alla dimensione dei file
pageOrderPrompt=Ordine delle pagine (inserisci una lista di numeri separati da virgola): pageOrderPrompt=Ordine delle pagine (inserisci una lista di numeri separati da virgola):
pageSelectionPrompt=Selezione pagina personalizzata (inserisci un elenco separato da virgole di numeri di pagina 1,5,6 o funzioni come 2n+1) : pageSelectionPrompt=Selezione pagina personalizzata (inserisci un elenco separato da virgole di numeri di pagina 1,5,6 o funzioni come 2n+1) :
@ -93,7 +93,7 @@ legal.terms=Termini e Condizioni
legal.accessibility=Accessibilità legal.accessibility=Accessibilità
legal.cookie=Informativa sui cookie legal.cookie=Informativa sui cookie
legal.impressum=Informazioni legali legal.impressum=Informazioni legali
legal.showCookieBanner=Preferenze sui cookie legal.showCookieBanner=Cookie Preferences
############### ###############
# Pipeline # # Pipeline #

View File

@ -553,7 +553,7 @@
{ {
"moduleName": "io.micrometer:micrometer-core", "moduleName": "io.micrometer:micrometer-core",
"moduleUrl": "https://github.com/micrometer-metrics/micrometer", "moduleUrl": "https://github.com/micrometer-metrics/micrometer",
"moduleVersion": "1.14.6", "moduleVersion": "1.14.5",
"moduleLicense": "The Apache Software License, Version 2.0", "moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
}, },

View File

@ -132,9 +132,7 @@
} }
} catch (error) { } catch (error) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
if(showGameBtn){ showGameBtn.style.display = 'none';
showGameBtn.style.display = 'none';
}
submitButton.textContent = originalButtonText; submitButton.textContent = originalButtonText;
submitButton.disabled = false; submitButton.disabled = false;
handleDownloadError(error); handleDownloadError(error);

View File

@ -170,7 +170,7 @@ function setupFileInput(chooser) {
inputContainer.querySelector('#fileInputText').innerHTML = window.fileInput.loading; inputContainer.querySelector('#fileInputText').innerHTML = window.fileInput.loading;
async function checkZipFile() { async function checkZipFile() {
const hasZipFiles = allFiles.some(file => file.type && zipTypes.includes(file.type)); const hasZipFiles = allFiles.some(file => zipTypes.includes(file.type));
// Only change to extractPDF message if we actually have zip files // Only change to extractPDF message if we actually have zip files
if (hasZipFiles) { if (hasZipFiles) {

View File

@ -255,12 +255,5 @@ document.addEventListener('DOMContentLoaded', function () {
}); });
}, 500); }, 500);
Array.from(document.querySelectorAll('.feature-group-header')).forEach((header) => {
const parent = header.parentNode;
header.onclick = () => {
expandCollapseToggle(parent);
};
});
showFavoritesOnly(); showFavoritesOnly();
}); });

View File

@ -241,5 +241,10 @@ document.addEventListener('DOMContentLoaded', async function () {
console.error('Material Symbols Rounded font failed to load.'); console.error('Material Symbols Rounded font failed to load.');
}); });
Array.from(document.querySelectorAll('.feature-group-header')).forEach((header) => {
const parent = header.parentNode;
header.onclick = () => {
expandCollapseToggle(parent);
};
});
}); });

View File

@ -57,15 +57,11 @@ function initLanguageSettings() {
function sortLanguageDropdown() { function sortLanguageDropdown() {
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const dropdownMenu = document.getElementById('languageSelection'); const dropdownMenu = document.querySelector('.dropdown-menu .dropdown-item.lang_dropdown-item').parentElement;
if (dropdownMenu) { if (dropdownMenu) {
const items = Array.from(dropdownMenu.children).filter((child) => child.querySelector('a')); const items = Array.from(dropdownMenu.children).filter((child) => child.matches('a'));
items items
.sort((wrapperA, wrapperB) => { .sort((a, b) => a.dataset.bsLanguageCode.localeCompare(b.dataset.bsLanguageCode))
const a = wrapperA.querySelector('a');
const b = wrapperB.querySelector('a');
return a.dataset.bsLanguageCode.localeCompare(b.dataset.bsLanguageCode);
})
.forEach((node) => dropdownMenu.appendChild(node)); .forEach((node) => dropdownMenu.appendChild(node));
} }
}); });

View File

@ -21,10 +21,12 @@ export class DeletePageCommand extends Command {
this.pagesContainer.removeChild(this.element); this.pagesContainer.removeChild(this.element);
if (this.pagesContainer.childElementCount === 0) { if (this.pagesContainer.childElementCount === 0) {
const filenameInput = document.getElementById("filename-input"); const filenameInput = document.getElementById("filename-input");
const filenameParagraph = document.getElementById("filename");
const downloadBtn = document.getElementById("export-button"); const downloadBtn = document.getElementById("export-button");
filenameInput.disabled = true; filenameInput.disabled = true;
filenameInput.value = ""; filenameInput.value = "";
filenameParagraph.innerText = "";
downloadBtn.disabled = true; downloadBtn.disabled = true;
} }
@ -41,10 +43,13 @@ export class DeletePageCommand extends Command {
} }
const filenameInput = document.getElementById("filename-input"); const filenameInput = document.getElementById("filename-input");
const filenameParagraph = document.getElementById("filename");
const downloadBtn = document.getElementById("export-button"); const downloadBtn = document.getElementById("export-button");
filenameInput.disabled = false; filenameInput.disabled = false;
filenameInput.value = this.filenameInputValue; filenameInput.value = this.filenameInputValue;
if (this.filenameParagraph)
filenameParagraph.innerText = this.filenameParagraphText;
downloadBtn.disabled = false; downloadBtn.disabled = false;
} }
@ -58,10 +63,12 @@ export class DeletePageCommand extends Command {
this.pagesContainer.removeChild(this.element); this.pagesContainer.removeChild(this.element);
if (this.pagesContainer.childElementCount === 0) { if (this.pagesContainer.childElementCount === 0) {
const filenameInput = document.getElementById("filename-input"); const filenameInput = document.getElementById("filename-input");
const filenameParagraph = document.getElementById("filename");
const downloadBtn = document.getElementById("export-button"); const downloadBtn = document.getElementById("export-button");
filenameInput.disabled = true; filenameInput.disabled = true;
filenameInput.value = ""; filenameInput.value = "";
filenameParagraph.innerText = "";
downloadBtn.disabled = true; downloadBtn.disabled = true;
} }

View File

@ -112,10 +112,10 @@ function setAsDefault(value) {
function adjustVisibleElements() { function adjustVisibleElements() {
const container = document.querySelector('.recent-features'); const container = document.querySelector('.recent-features');
if(!container) return;
const subElements = Array.from(container.children); const subElements = Array.from(container.children);
let totalWidth = 0; let totalWidth = 0;
const containerWidth = container.offsetWidth;
subElements.forEach((element) => { subElements.forEach((element) => {
totalWidth += 12 * parseFloat(getComputedStyle(document.documentElement).fontSize); totalWidth += 12 * parseFloat(getComputedStyle(document.documentElement).fontSize);

View File

@ -26,6 +26,7 @@ window.addEventListener("keydown", (event) => {
function undoDraw() { function undoDraw() {
const data = signaturePad.toData(); const data = signaturePad.toData();
if (data && data.length > 0) { if (data && data.length > 0) {
const removed = data.pop(); const removed = data.pop();
undoData.push(removed); undoData.push(removed);
@ -34,6 +35,7 @@ function undoDraw() {
} }
function redoDraw() { function redoDraw() {
if (undoData.length > 0) { if (undoData.length > 0) {
const data = signaturePad.toData(); const data = signaturePad.toData();
data.push(undoData.pop()); data.push(undoData.pop());
@ -50,18 +52,24 @@ function addDraggableFromPad() {
} }
function getCroppedCanvasDataUrl(canvas) { function getCroppedCanvasDataUrl(canvas) {
let originalCtx = canvas.getContext('2d', { willReadFrequently: true }); let originalCtx = canvas.getContext('2d');
let originalWidth = canvas.width; let originalWidth = canvas.width;
let originalHeight = canvas.height; let originalHeight = canvas.height;
let imageData = originalCtx.getImageData(0, 0, originalWidth, originalHeight); let imageData = originalCtx.getImageData(0, 0, originalWidth, originalHeight);
let minX = originalWidth + 1, maxX = -1, minY = originalHeight + 1, maxY = -1; let minX = originalWidth + 1,
maxX = -1,
minY = originalHeight + 1,
maxY = -1,
x = 0,
y = 0,
currentPixelColorValueIndex;
for (let y = 0; y < originalHeight; y++) { for (y = 0; y < originalHeight; y++) {
for (let x = 0; x < originalWidth; x++) { for (x = 0; x < originalWidth; x++) {
let idx = (y * originalWidth + x) * 4; currentPixelColorValueIndex = (y * originalWidth + x) * 4;
let alpha = imageData.data[idx + 3]; let currentPixelAlphaValue = imageData.data[currentPixelColorValueIndex + 3];
if (alpha > 0) { if (currentPixelAlphaValue > 0) {
if (minX > x) minX = x; if (minX > x) minX = x;
if (maxX < x) maxX = x; if (maxX < x) maxX = x;
if (minY > y) minY = y; if (minY > y) minY = y;
@ -73,14 +81,14 @@ function getCroppedCanvasDataUrl(canvas) {
let croppedWidth = maxX - minX; let croppedWidth = maxX - minX;
let croppedHeight = maxY - minY; let croppedHeight = maxY - minY;
if (croppedWidth < 0 || croppedHeight < 0) return null; if (croppedWidth < 0 || croppedHeight < 0) return null;
let cutImageData = originalCtx.getImageData(minX, minY, croppedWidth, croppedHeight); let cuttedImageData = originalCtx.getImageData(minX, minY, croppedWidth, croppedHeight);
let croppedCanvas = document.createElement('canvas'); let croppedCanvas = document.createElement('canvas'),
let croppedCtx = croppedCanvas.getContext('2d'); croppedCtx = croppedCanvas.getContext('2d');
croppedCanvas.width = croppedWidth; croppedCanvas.width = croppedWidth;
croppedCanvas.height = croppedHeight; croppedCanvas.height = croppedHeight;
croppedCtx.putImageData(cutImageData, 0, 0); croppedCtx.putImageData(cuttedImageData, 0, 0);
return croppedCanvas.toDataURL(); return croppedCanvas.toDataURL();
} }
@ -106,20 +114,10 @@ function resizeCanvas() {
signaturePad.clear(); signaturePad.clear();
} }
const debounce = (fn, delay = 100) => { new IntersectionObserver((entries, observer) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
};
const debouncedResize = debounce(resizeCanvas, 200);
new IntersectionObserver((entries) => {
if (entries.some((entry) => entry.intersectionRatio > 0)) { if (entries.some((entry) => entry.intersectionRatio > 0)) {
debouncedResize(); resizeCanvas();
} }
}).observe(signaturePadCanvas); }).observe(signaturePadCanvas);
new ResizeObserver(debouncedResize).observe(signaturePadCanvas); new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);

View File

@ -240,8 +240,8 @@
window.stirlingPDF.sessionExpired = /*[[#{session.expired}]]*/ ''; window.stirlingPDF.sessionExpired = /*[[#{session.expired}]]*/ '';
window.stirlingPDF.refreshPage = /*[[#{session.refreshPage}]]*/ 'Refresh Page'; window.stirlingPDF.refreshPage = /*[[#{session.refreshPage}]]*/ 'Refresh Page';
window.stirlingPDF.error = /*[[#{error}]]*/ "Error"; window.stirlingPDF.error = /*[[#{error}]]*/ "Error";
window.stirlingPDF.uploadLimitReadable = /*[[${@uploadLimitService.getReadableUploadLimit()}]]*/ 'Unlimited'; window.stirlingPDF.uploadLimit = /*[[${uploadLimit}]]*/ 0;
window.stirlingPDF.uploadLimit = /*[[${@uploadLimitService.getUploadLimit()}]]*/ 0; window.stirlingPDF.uploadLimitReadable = /*[[${uploadLimitReadable}]]*/ 'Unlimited';
window.stirlingPDF.uploadLimitExceededSingular = /*[[#{uploadLimitExceededSingular}]]*/ 'is too large. Maximum allowed size is'; window.stirlingPDF.uploadLimitExceededSingular = /*[[#{uploadLimitExceededSingular}]]*/ 'is too large. Maximum allowed size is';
window.stirlingPDF.uploadLimitExceededPlural = /*[[#{uploadLimitExceededPlural}]]*/ 'are too large. Maximum allowed size is'; window.stirlingPDF.uploadLimitExceededPlural = /*[[#{uploadLimitExceededPlural}]]*/ 'are too large. Maximum allowed size is';
})(); })();
@ -292,10 +292,10 @@
</div> </div>
</div> </div>
<div class="selected-files flex-wrap"></div> <div class="selected-files flex-wrap"></div>
<div class="text-muted small mt-0 text-end w-100" th:if="${@uploadLimitService.getUploadLimit() != 0}"> <div class="text-muted small mt-0 text-end w-100" th:if="${uploadLimit != 0}">
<span th:text="#{uploadLimit}">Maximum file size: </span> <span th:text="#{uploadLimit}">Maximum file size: </span>
<span th:text="${@uploadLimitService.getReadableUploadLimit()}"></span> <span th:text="${uploadLimitReadable}"></span>
</div> </div>
</div> </div>
<div class="progressBarContainer" style="display: none; position: relative;"> <div class="progressBarContainer" style="display: none; position: relative;">
<div class="progress" style="height: 1rem;"> <div class="progress" style="height: 1rem;">

View File

@ -143,7 +143,7 @@
</a> </a>
<div class="dropdown-menu dropdown-menu-tp" aria-labelledby="languageDropdown"> <div class="dropdown-menu dropdown-menu-tp" aria-labelledby="languageDropdown">
<div class="dropdown-menu-wrapper px-xl-2 px-2"> <div class="dropdown-menu-wrapper px-xl-2 px-2">
<div id="languageSelection" class="scrollable-y lang_dropdown-mw scalable-languages-container"> <div class="scrollable-y lang_dropdown-mw scalable-languages-container">
<th:block th:insert="~{fragments/languages :: langs}"></th:block> <th:block th:insert="~{fragments/languages :: langs}"></th:block>
</div> </div>
</div> </div>

View File

@ -143,7 +143,7 @@
</button> </button>
<div class="dropdown-menu" aria-labelledby="languageDropdown"> <div class="dropdown-menu" aria-labelledby="languageDropdown">
<!-- Here's where the fragment will be included --> <!-- Here's where the fragment will be included -->
<div id="languageSelection" class="scrollable-y" > <div class="scrollable-y">
<th:block th:replace="~{fragments/languages :: langs}"></th:block> <th:block th:replace="~{fragments/languages :: langs}"></th:block>
</div> </div>
</div> </div>

View File

@ -27,7 +27,7 @@
</div> </div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script> <script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script type="module" th:src="@{'/js/pages/add-image.js'}"></script> <script type="module" th:src="@{'/js/pages/add-image.js'}"></script>
<div class="show-on-file-selected"> <div class="tab-group show-on-file-selected">
<div <div
th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=false, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}"> th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=false, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
</div> </div>

View File

@ -43,13 +43,13 @@
</div> </div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script> <script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<div class="tab-group show-on-file-selected"> <div class="tab-group show-on-file-selected">
<div class="tab-container"th:data-title="#{sign.upload}"> <div class="tab-container" th:title="#{sign.upload}" th:data-title="#{sign.upload}">
<div <div
th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=false, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}"> th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=false, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
</div> </div>
</div> </div>
<div class="tab-container drawing-pad-container" th:data-title="#{sign.draw}"> <div class="tab-container drawing-pad-container" th:title="#{sign.draw}" th:data-title="#{sign.draw}">
<canvas id="drawing-pad-canvas"></canvas> <canvas id="drawing-pad-canvas"></canvas>
<br> <br>
<button id="clear-signature" class="btn btn-outline-danger mt-2" onclick="signaturePad.clear()" <button id="clear-signature" class="btn btn-outline-danger mt-2" onclick="signaturePad.clear()"
@ -62,7 +62,7 @@
onclick="redoDraw()"></button> onclick="redoDraw()"></button>
</div> </div>
<div class="tab-container" th:data-title="#{sign.saved}"> <div class="tab-container" th:title="#{sign.saved}" th:data-title="#{sign.saved}">
<div class="saved-signatures-section" th:if="${not #lists.isEmpty(signatures)}"> <div class="saved-signatures-section" th:if="${not #lists.isEmpty(signatures)}">
<!-- Preview Modal --> <!-- Preview Modal -->
@ -134,7 +134,7 @@
</div> </div>
</div> </div>
<div class="tab-container" th:data-title="#{sign.text}"> <div class="tab-container" th:title="#{sign.text}" th:data-title="#{sign.text}">
<label class="form-check-label" for="sigText" th:text="#{text}"></label> <label class="form-check-label" for="sigText" th:text="#{text}"></label>
<textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea> <textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea>
<label th:text="#{font}"></label> <label th:text="#{font}"></label>