mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-09-13 04:08:22 +02:00
refactor: move modules under app/ directory and update file paths (#3938)
# Description of Changes - **What was changed:** - Renamed top-level directories: `stirling-pdf` → `app/core`, `common` → `app/common`, `proprietary` → `app/proprietary`. - Updated all path references in `.gitattributes`, GitHub workflows (`.github/workflows/*`), scripts (`.github/scripts/*`), `.gitignore`, Dockerfiles, license files, and template settings to reflect the new structure. - Added a new CI job `check-generateOpenApiDocs` to generate and upload OpenAPI documentation. - Removed redundant `@Autowired` annotations from `TempFileShutdownHook` and `UnlockPDFFormsController`. - Minor formatting and comment adjustments in YAML templates and resource files. - **Why the change was made:** - To introduce a clear `app/` directory hierarchy for core, common, and proprietary modules, improving organization and maintainability. - To ensure continuous integration and Docker builds continue to work seamlessly with the reorganized structure. - To automate OpenAPI documentation generation as part of the CI pipeline. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title='<3')}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,474 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{account.title})}"></th:block>
|
||||
<link rel="stylesheet" th:href="@{/css/modern-tables.css}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
|
||||
<div class="data-container">
|
||||
<div class="data-panel">
|
||||
<div class="data-header">
|
||||
<h1 class="data-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">settings_account_box</span>
|
||||
</span>
|
||||
<span th:text="#{account.accountSettings}">User Settings</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="data-body">
|
||||
<div th:if="${messageType}" class="alert alert-danger data-mb-3">
|
||||
<span th:text="#{${messageType}}">Default message if not found</span>
|
||||
</div>
|
||||
|
||||
<div th:if="${error}" class="alert alert-danger data-mb-3" role="alert">
|
||||
<span th:text="${error}">Error Message</span>
|
||||
</div>
|
||||
|
||||
<!-- Admin Settings Banner (for admins only) -->
|
||||
<div th:if="${role == 'ROLE_ADMIN'}" class="data-panel data-mb-3" style="background-color: var(--md-sys-color-secondary-container);">
|
||||
<div class="data-body" style="display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.5rem; background-color: var(--md-sys-color-secondary-container);">
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<span class="material-symbols-rounded" style="font-size: 2rem; color: var(--md-sys-color-secondary);">
|
||||
admin_panel_settings
|
||||
</span>
|
||||
<div>
|
||||
<h4 style="margin: 0; color: var(--md-sys-color-secondary);" th:text="#{account.adminTitle}">Administrator Tools</h4>
|
||||
<p style="margin: 0.25rem 0 0 0; color: var(--md-sys-color-secondary);" th:text="#{account.adminNotif}">You have admin privileges. Access system settings and user management.</p>
|
||||
</div>
|
||||
</div>
|
||||
<a class="data-btn" th:href="@{'/adminSettings'}" role="button" target="_blank"
|
||||
style="background-color: var(--md-sys-color-secondary); color: var(--md-sys-color-on-secondary); display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.625rem 1.25rem; border-radius: 0.5rem; font-weight: 500; border: none; cursor: pointer; text-decoration: none;">
|
||||
<span class="material-symbols-rounded">admin_panel_settings</span>
|
||||
<span th:text="#{account.adminSettings}">Admin Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Management Buttons -->
|
||||
<th:block th:if="not ${oAuth2Login} or not ${saml2Login}">
|
||||
<div class="data-section-title">Account Management</div>
|
||||
<div class="data-actions data-actions-start data-mb-3">
|
||||
<button class="data-btn data-btn-primary" data-bs-toggle="modal" data-bs-target="#changeUsernameModal">
|
||||
<span class="material-symbols-rounded">edit</span>
|
||||
<span th:text="#{account.changeUsername}">Change Username</span>
|
||||
</button>
|
||||
<button class="data-btn data-btn-primary" data-bs-toggle="modal" data-bs-target="#changePasswordModal">
|
||||
<span class="material-symbols-rounded">key</span>
|
||||
<span th:text="#{account.changePassword}">Change Password</span>
|
||||
</button>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<!-- API Key Section -->
|
||||
<div class="data-section-title" th:text="#{account.yourApiKey}">API Key</div>
|
||||
<div class="data-panel data-mb-3">
|
||||
<div class="data-header">
|
||||
<h5 class="data-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">key</span>
|
||||
</span>
|
||||
<span th:text="#{account.yourApiKey}">API Key</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="data-body">
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<input class="data-form-control" id="apiKey" th:placeholder="#{account.yourApiKey}" readonly style="flex: 1;">
|
||||
<button class="data-btn data-btn-secondary" id="copyBtn" type="button" onclick="copyToClipboard()" title="Copy to clipboard">
|
||||
<span class="material-symbols-rounded">content_copy</span>
|
||||
</button>
|
||||
<button class="data-btn data-btn-secondary" id="showBtn" type="button" onclick="showApiKey()" title="Show/hide API key">
|
||||
<span class="material-symbols-rounded" id="eyeIcon">visibility</span>
|
||||
</button>
|
||||
<button class="data-btn data-btn-secondary" id="refreshBtn" type="button" onclick="refreshApiKey()" title="Refresh API key">
|
||||
<span class="material-symbols-rounded">refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Sync Section -->
|
||||
<div class="data-section-title" th:text="#{account.syncTitle}">Sync browser settings with Account</div>
|
||||
<div class="data-panel data-mb-3">
|
||||
<div class="data-header">
|
||||
<h5 class="data-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">sync</span>
|
||||
</span>
|
||||
<span th:text="#{account.settingsCompare}">Settings Comparison</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="data-body">
|
||||
<div class="table-responsive">
|
||||
<table id="settingsTable" class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" th:text="#{account.property}">Property</th>
|
||||
<th scope="col" th:text="#{account.accountSettings}">Account Setting</th>
|
||||
<th scope="col" th:text="#{account.webBrowserSettings}">Web Browser Setting</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- This will be dynamically populated by JavaScript -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="data-actions data-mt-3">
|
||||
<button id="syncToBrowser" class="data-btn data-btn-primary">
|
||||
<span class="material-symbols-rounded">cloud_download</span>
|
||||
<span th:text="#{account.syncToBrowser}">Sync Account -> Browser</span>
|
||||
</button>
|
||||
<button id="syncToAccount" class="data-btn data-btn-secondary">
|
||||
<span class="material-symbols-rounded">cloud_upload</span>
|
||||
<span th:text="#{account.syncToAccount}">Sync Account <- Browser</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Username Modal -->
|
||||
<div class="modal fade" id="changeUsernameModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<form id="formsavechangeusername" th:action="@{'/api/v1/user/change-username'}" method="post" class="modal-content data-modal">
|
||||
<div class="data-modal-header">
|
||||
<h5 class="data-modal-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">edit</span>
|
||||
</span>
|
||||
<span th:text="#{account.changeUsername}">Change Username</span>
|
||||
</h5>
|
||||
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="data-modal-body">
|
||||
<div class="data-form-group">
|
||||
<label for="newUsername" class="data-form-label" th:text="#{account.newUsername}">New Username</label>
|
||||
<input type="text" class="data-form-control" name="newUsername" id="newUsername" th:placeholder="#{account.newUsername}">
|
||||
<span id="usernameError" style="display: none; color: var(--md-sys-color-error);" th:text="#{invalidUsernameMessage}">Invalid username!</span>
|
||||
</div>
|
||||
<div class="data-form-group">
|
||||
<label for="currentPasswordChangeUsername" class="data-form-label" th:text="#{password}">Password</label>
|
||||
<input type="password" class="data-form-control" name="currentPasswordChangeUsername" id="currentPasswordChangeUsername" th:placeholder="#{password}">
|
||||
</div>
|
||||
<div class="data-form-actions">
|
||||
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
<span th:text="#{cancel}">Cancel</span>
|
||||
</button>
|
||||
<button type="submit" class="data-btn data-btn-primary">
|
||||
<span class="material-symbols-rounded">check</span>
|
||||
<span th:text="#{account.changeUsername}">Change Username</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Password Modal -->
|
||||
<div class="modal fade" id="changePasswordModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<form id="formsavechangepassword" th:action="@{'/api/v1/user/change-password'}" method="post" class="modal-content data-modal">
|
||||
<div class="data-modal-header">
|
||||
<h5 class="data-modal-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">key</span>
|
||||
</span>
|
||||
<span th:text="#{account.changePassword}">Change Password</span>
|
||||
</h5>
|
||||
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="data-modal-body">
|
||||
<div class="data-form-group">
|
||||
<label for="currentPassword" class="data-form-label" th:text="#{account.oldPassword}">Old Password</label>
|
||||
<input type="password" class="data-form-control" name="currentPassword" id="currentPassword" th:placeholder="#{account.oldPassword}">
|
||||
</div>
|
||||
<div class="data-form-group">
|
||||
<label for="newPassword" class="data-form-label" th:text="#{account.newPassword}">New Password</label>
|
||||
<input type="password" class="data-form-control" name="newPassword" id="newPassword" th:placeholder="#{account.newPassword}">
|
||||
</div>
|
||||
<div class="data-form-group">
|
||||
<label for="confirmNewPassword" class="data-form-label" th:text="#{account.confirmNewPassword}">Confirm New Password</label>
|
||||
<input type="password" class="data-form-control" name="confirmNewPassword" id="confirmNewPassword" th:placeholder="#{account.confirmNewPassword}">
|
||||
<span id="confirmPasswordError" style="display: none; color: var(--md-sys-color-error);" th:text="#{confirmPasswordErrorMessage}">New Password and Confirm New Password must match.</span>
|
||||
</div>
|
||||
<div class="data-form-actions">
|
||||
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
<span th:text="#{cancel}">Cancel</span>
|
||||
</button>
|
||||
<button type="submit" class="data-btn data-btn-primary">
|
||||
<span class="material-symbols-rounded">check</span>
|
||||
<span th:text="#{account.changePassword}">Change Password</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript for validation -->
|
||||
<script th:inline="javascript">
|
||||
jQuery.validator.addMethod("usernamePattern", function(value, element) {
|
||||
// Regular expression for user name: Min. 3 characters, max. 50 characters
|
||||
const regexUsername = /^[a-zA-Z0-9](?!.*[-@._+]{2,})([a-zA-Z0-9@._+-]{1,48})[a-zA-Z0-9]$/;
|
||||
|
||||
// Regular expression for email addresses: Max. 320 characters, with RFC-like validation
|
||||
const regexEmail = /^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.[A-Za-z]{2,})$/;
|
||||
|
||||
// Check if the field is optional or meets the requirements
|
||||
return this.optional(element) || regexUsername.test(value) || regexEmail.test(value);
|
||||
}, /*[[#{invalidUsernameMessage}]]*/ "Invalid username format");
|
||||
|
||||
$(document).ready(function() {
|
||||
$.validator.addMethod("passwordMatch", function(value, element) {
|
||||
return $('#newPassword').val() === $('#confirmNewPassword').val();
|
||||
}, /*[[#{confirmPasswordErrorMessage}]]*/ "New Password and Confirm New Password must match.");
|
||||
|
||||
$('#formsavechangepassword').validate({
|
||||
rules: {
|
||||
currentPassword: {
|
||||
required: true
|
||||
},
|
||||
newPassword: {
|
||||
required: true
|
||||
},
|
||||
confirmNewPassword: {
|
||||
required: true,
|
||||
passwordMatch: true
|
||||
}
|
||||
},
|
||||
errorPlacement: function(error, element) {
|
||||
if (element.attr("name") === "newPassword" || element.attr("name") === "confirmNewPassword") {
|
||||
$("#confirmPasswordError").text(error.text()).show();
|
||||
} else {
|
||||
error.insertAfter(element);
|
||||
}
|
||||
},
|
||||
success: function(label, element) {
|
||||
if ($(element).attr("name") === "newPassword" || $(element).attr("name") === "confirmNewPassword") {
|
||||
$("#confirmPasswordError").hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$('#formsavechangeusername').validate({
|
||||
rules: {
|
||||
newUsername: {
|
||||
required: true,
|
||||
usernamePattern: true
|
||||
},
|
||||
currentPasswordChangeUsername: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
messages: {
|
||||
newUsername: {
|
||||
usernamePattern: /*[[#{invalidUsernameMessage}]]*/ "Invalid username format"
|
||||
},
|
||||
},
|
||||
errorPlacement: function(error, element) {
|
||||
if (element.attr("name") === "newUsername") {
|
||||
$("#usernameError").text(error.text()).show();
|
||||
} else {
|
||||
error.insertAfter(element);
|
||||
}
|
||||
},
|
||||
success: function(label, element) {
|
||||
if ($(element).attr("name") === "newUsername") {
|
||||
$("#usernameError").hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- JavaScript for API Key -->
|
||||
<script th:inline="javascript">
|
||||
function copyToClipboard() {
|
||||
const apiKeyElement = document.getElementById("apiKey");
|
||||
apiKeyElement.select();
|
||||
document.execCommand("copy");
|
||||
}
|
||||
|
||||
function showApiKey() {
|
||||
const apiKeyElement = document.getElementById("apiKey");
|
||||
const copyBtn = document.getElementById("copyBtn");
|
||||
const eyeIcon = document.getElementById("eyeIcon");
|
||||
if (apiKeyElement.type === "password") {
|
||||
apiKeyElement.type = "text";
|
||||
eyeIcon.textContent = "visibility_off";
|
||||
copyBtn.disabled = false; // Enable copy button when API key is visible
|
||||
} else {
|
||||
apiKeyElement.type = "password";
|
||||
eyeIcon.textContent = "visibility";
|
||||
copyBtn.disabled = true; // Disable copy button when API key is hidden
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async function() {
|
||||
try {
|
||||
/*<![CDATA[*/
|
||||
const urlGetApiKey = /*[[@{/api/v1/user/get-api-key}]]*/ "/api/v1/user/get-api-key";
|
||||
/*]]>*/
|
||||
let response = await window.fetchWithCsrf(urlGetApiKey, { method: 'POST' });
|
||||
if (response.status === 200) {
|
||||
let apiKey = await response.text();
|
||||
manageUIState(apiKey);
|
||||
} else {
|
||||
manageUIState(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('There was an error:', error);
|
||||
}
|
||||
finally {
|
||||
showApiKey();
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshApiKey() {
|
||||
try {
|
||||
/*<![CDATA[*/
|
||||
const urlUpdateApiKey = /*[[@{/api/v1/user/update-api-key}]]*/ "/api/v1/user/update-api-key";
|
||||
/*]]>*/
|
||||
let response = await window.fetchWithCsrf(urlUpdateApiKey, { method: 'POST' });
|
||||
if (response.status === 200) {
|
||||
let apiKey = await response.text();
|
||||
manageUIState(apiKey);
|
||||
document.getElementById("apiKey").type = 'text';
|
||||
document.getElementById("copyBtn").disabled = false;
|
||||
} else {
|
||||
alert('Error refreshing API key.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('There was an error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function manageUIState(apiKey) {
|
||||
const apiKeyElement = document.getElementById("apiKey");
|
||||
const showBtn = document.getElementById("showBtn");
|
||||
const copyBtn = document.getElementById("copyBtn");
|
||||
|
||||
if (apiKey && apiKey.trim().length > 0) {
|
||||
apiKeyElement.value = apiKey;
|
||||
showBtn.disabled = false;
|
||||
copyBtn.disabled = false;
|
||||
} else {
|
||||
apiKeyElement.value = "";
|
||||
showBtn.disabled = true;
|
||||
copyBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- JavaScript for Settings Sync -->
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener("DOMContentLoaded", async function() {
|
||||
const settingsTableBody = document.querySelector("#settingsTable tbody");
|
||||
|
||||
// Helper function to check if a key should be ignored
|
||||
function shouldIgnoreKey(key) {
|
||||
return key === 'debug' ||
|
||||
key === '0' ||
|
||||
key === '1' ||
|
||||
key.includes('pdfjs') ||
|
||||
key.includes('clientSubmissionOrder') ||
|
||||
key.includes('lastSubmitTime') ||
|
||||
key.includes('lastClientId') ||
|
||||
|
||||
|
||||
key.includes('posthog') || key.includes('ssoRedirectAttempts') || key.includes('lastRedirectAttempt') || key.includes('surveyVersion') ||
|
||||
key.includes('pageViews');
|
||||
}
|
||||
|
||||
/*<![CDATA[*/
|
||||
var accountSettingsString = /*[[${settings}]]*/ {};
|
||||
/*]]>*/
|
||||
var accountSettings = JSON.parse(accountSettingsString);
|
||||
|
||||
let allKeys = new Set([...Object.keys(accountSettings), ...Object.keys(localStorage)]);
|
||||
|
||||
allKeys.forEach(key => {
|
||||
if(shouldIgnoreKey(key)) return; // Using our helper function
|
||||
|
||||
const accountValue = accountSettings[key] || '-';
|
||||
const browserValue = localStorage.getItem(key) || '-';
|
||||
|
||||
const row = settingsTableBody.insertRow();
|
||||
const propertyCell = row.insertCell(0);
|
||||
const accountCell = row.insertCell(1);
|
||||
const browserCell = row.insertCell(2);
|
||||
|
||||
propertyCell.textContent = key;
|
||||
accountCell.textContent = accountValue;
|
||||
browserCell.textContent = browserValue;
|
||||
});
|
||||
|
||||
document.getElementById('syncToBrowser').addEventListener('click', function() {
|
||||
// First, clear the local storage
|
||||
localStorage.clear();
|
||||
|
||||
// Then, set the account settings to local storage
|
||||
for (let key in accountSettings) {
|
||||
if(!shouldIgnoreKey(key)) { // Using our helper function
|
||||
localStorage.setItem(key, accountSettings[key]);
|
||||
}
|
||||
}
|
||||
location.reload(); // Refresh the page after sync
|
||||
});
|
||||
|
||||
document.getElementById('syncToAccount').addEventListener('click', async function() {
|
||||
/*<![CDATA[*/
|
||||
const urlUpdateUserSettings = /*[[@{/api/v1/user/updateUserSettings}]]*/ "/api/v1/user/updateUserSettings";
|
||||
/*]]>*/
|
||||
|
||||
let settings = {};
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if(!shouldIgnoreKey(key)) { // Using our helper function
|
||||
settings[key] = localStorage.getItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await window.fetchWithCsrf(urlUpdateUserSettings, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(settings)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error syncing settings to account');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Error syncing settings to account');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,458 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{adminUserSettings.title}, header=#{adminUserSettings.header})}"></th:block>
|
||||
<link rel="stylesheet" th:href="@{/css/modern-tables.css}">
|
||||
<style>
|
||||
.active-user {
|
||||
color: var(--md-sys-color-tertiary);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
|
||||
<div class="data-container">
|
||||
<div class="data-panel">
|
||||
<div class="data-header">
|
||||
<h1 class="data-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">manage_accounts</span>
|
||||
</span>
|
||||
<span th:text="#{adminUserSettings.header}">Admin User Control Settings</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="data-body">
|
||||
<!-- User Stats Banner -->
|
||||
<div class="data-panel data-mb-3" style="background-color: var(--md-sys-color-primary-container);">
|
||||
<div class="data-body" style="padding: 1.25rem;">
|
||||
<div style="display: flex; flex-wrap: wrap; justify-content: space-around; align-items: center; gap: 1.5rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||||
<span class="material-symbols-rounded" style="font-size: 2.25rem; color: var(--md-sys-color-primary);">
|
||||
group
|
||||
</span>
|
||||
<div>
|
||||
<div style="color: var(--md-sys-color-primary); font-size: 0.875rem; font-weight: 500;" th:text="#{adminUserSettings.totalUsers}">Total Users</div>
|
||||
<div style="color: var(--md-sys-color-primary); font-size: 1.5rem; font-weight: 700;">
|
||||
<span th:text="${totalUsers}"></span>
|
||||
<span th:if="${@runningProOrHigher}" th:text="'/' + ${maxPaidUsers}" style="font-size: 1rem;"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||||
<span class="material-symbols-rounded" style="font-size: 2.25rem; color: var(--md-sys-color-primary);">
|
||||
check_circle
|
||||
</span>
|
||||
<div>
|
||||
<div style="color: var(--md-sys-color-primary); font-size: 0.875rem; font-weight: 500;" th:text="#{adminUserSettings.activeUsers}">Active Users</div>
|
||||
<div style="color: var(--md-sys-color-primary); font-size: 1.5rem; font-weight: 700;" th:text="${activeUsers}"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||||
<span class="material-symbols-rounded" style="font-size: 2.25rem; color: var(--md-sys-color-primary);">
|
||||
person_off
|
||||
</span>
|
||||
<div>
|
||||
<div style="color: var(--md-sys-color-primary); font-size: 0.875rem; font-weight: 500;" th:text="#{adminUserSettings.disabledUsers}">Disabled Users</div>
|
||||
<div style="color: var(--md-sys-color-primary); font-size: 1.5rem; font-weight: 700;" th:text="${disabledUsers}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert Messages -->
|
||||
<div th:if="${addMessage}" th:class="${#strings.contains(addMessage, 'Successfully') or #strings.contains(addMessage, 'Created') ? 'alert alert-success data-mb-3' : 'alert alert-danger data-mb-3'}">
|
||||
<span th:text="#{${addMessage}}">Default message if not found</span>
|
||||
</div>
|
||||
|
||||
<div th:if="${changeMessage}" th:class="${#strings.contains(changeMessage, 'Successfully') or #strings.contains(changeMessage, 'Created') ? 'alert alert-success data-mb-3' : 'alert alert-danger data-mb-3'}">
|
||||
<span th:text="#{${changeMessage}}">Default message if not found</span>
|
||||
</div>
|
||||
|
||||
<div th:if="${deleteMessage}" th:class="${#strings.contains(deleteMessage, 'Successfully') or #strings.contains(deleteMessage, 'Created') ? 'alert alert-success data-mb-3' : 'alert alert-danger data-mb-3'}">
|
||||
<span th:text="#{${deleteMessage}}">Default message if not found</span>
|
||||
</div>
|
||||
|
||||
<!-- Admin Actions -->
|
||||
<div class="data-section-title">User Management</div>
|
||||
<div class="data-actions data-mb-3">
|
||||
<button
|
||||
th:data-bs-toggle="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? null : 'modal'"
|
||||
th:data-bs-target="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? null : '#addUserModal'"
|
||||
th:class="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? 'data-btn data-btn-danger' : 'data-btn data-btn-primary'"
|
||||
th:title="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? #{adminUserSettings.maxUsersReached} : #{adminUserSettings.addUser}">
|
||||
<span class="material-symbols-rounded">person_add</span>
|
||||
<span th:text="#{adminUserSettings.addUser}">Add New User</span>
|
||||
</button>
|
||||
|
||||
<a th:href="@{'/teams'}" class="data-btn data-btn-secondary" th:title="#{adminUserSettings.teams}">
|
||||
<span class="material-symbols-rounded">group</span>
|
||||
<span th:text="#{adminUserSettings.teams}">Manage Teams</span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#changeUserRoleModal"
|
||||
class="data-btn data-btn-secondary"
|
||||
th:title="#{adminUserSettings.changeUserRole}">
|
||||
<span class="material-symbols-rounded">edit</span>
|
||||
<span th:text="#{adminUserSettings.changeUserRole}">Change User's Role</span>
|
||||
</button>
|
||||
|
||||
<a th:href="@{'/usage'}" th:if="${@runningEE}" class="data-btn data-btn-secondary" th:title="#{adminUserSettings.usage}">
|
||||
<span class="material-symbols-rounded">analytics</span>
|
||||
<span th:text="#{adminUserSettings.usage}">Usage Statistics</span>
|
||||
</a>
|
||||
|
||||
<a href="/audit" th:if="${@runningEE}" class="data-btn data-btn-secondary" title="Audit Dashboard">
|
||||
<span class="material-symbols-rounded">security</span>
|
||||
<span>Audit Dashboard</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Users Table -->
|
||||
<div class="table-responsive">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col" th:title="#{username}" class="text-overflow" th:text="#{username}">Username</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.team}" class="text-overflow" th:text="#{adminUserSettings.team}">Team</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.role}" class="text-overflow" th:text="#{adminUserSettings.role}">Roles</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.authenticated}" class="text-overflow" th:text="#{adminUserSettings.authenticated}">Authenticated</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.lastRequest}" class="text-overflow" th:text="#{adminUserSettings.lastRequest}">Last Request</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.actions}" class="text-overflow" th:text="#{adminUserSettings.actions}">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="user : ${users}">
|
||||
<td th:text="${user.id}" th:title="${user.id}" class="text-overflow"></td>
|
||||
<td th:text="${user.username}" th:title="${user.username}" class="text-overflow" th:classappend="${userSessions[user.username] ? 'active-user' : ''}"></td>
|
||||
<td th:text="${user.team != null ? user.team.name : '—'}" th:title="${user.team != null ? user.team.name : '—'}" class="text-overflow"></td>
|
||||
<td>
|
||||
<span class="data-badge" style="background-color: var(--md-sys-color-secondary-container); color: var(--md-sys-color-secondary); padding: 0.25rem 0.5rem; border-radius: 1rem; font-size: 0.875rem; display: inline-flex; align-items: center; gap: 0.25rem;">
|
||||
<span class="material-symbols-rounded" style="font-size: 1rem;">shield</span>
|
||||
<span th:text="#{${user.roleName}}" th:title="#{${user.roleName}}" class="text-overflow">Role</span>
|
||||
</span>
|
||||
</td>
|
||||
<td th:text="${user.authenticationType}" th:title="${user.authenticationType}"></td>
|
||||
<td th:text="${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}" th:title="${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}"></td>
|
||||
<td>
|
||||
<div class="data-action-cell">
|
||||
<form th:if="${user.username != currentUsername}" th:action="@{'/api/v1/user/admin/deleteUser/' + ${user.username}}" method="post" onsubmit="return confirmDeleteUser()" style="display: inline;">
|
||||
<button type="submit" th:title="#{adminUserSettings.deleteUser}" class="data-icon-btn data-icon-btn-danger">
|
||||
<span class="material-symbols-rounded">person_remove</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<a th:if="${user.username == currentUsername}" th:title="#{adminUserSettings.editOwnProfil}" th:href="@{'/account'}" class="data-icon-btn data-icon-btn-primary">
|
||||
<span class="material-symbols-rounded">edit</span>
|
||||
</a>
|
||||
|
||||
<form th:action="@{'/api/v1/user/admin/changeUserEnabled/' + ${user.username}}" method="post" onsubmit="return confirmChangeUserStatus()" style="display: inline;">
|
||||
<input type="hidden" name="enabled" th:value="!${user.enabled}" />
|
||||
<button type="submit" th:if="${user.enabled}" th:title="#{adminUserSettings.enabledUser}" class="data-icon-btn data-icon-btn-primary">
|
||||
<span class="material-symbols-rounded">person</span>
|
||||
</button>
|
||||
<button type="submit" th:unless="${user.enabled}" th:title="#{adminUserSettings.disabledUser}" class="data-icon-btn data-icon-btn-danger">
|
||||
<span class="material-symbols-rounded">person_off</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p th:if="${!@runningProOrHigher}" class="data-mt-3" th:text="#{enterpriseEdition.ssoAdvert}"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change User Role Modal -->
|
||||
<div class="modal fade" id="changeUserRoleModal" tabindex="-1" aria-labelledby="changeUserRoleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<form th:action="@{'/api/v1/user/admin/changeRole'}" method="post" class="modal-content data-modal">
|
||||
<div class="data-modal-header">
|
||||
<h5 class="data-modal-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">edit</span>
|
||||
</span>
|
||||
<span th:text="#{adminUserSettings.changeUserRole}">Change User's Role</span>
|
||||
</h5>
|
||||
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="data-modal-body">
|
||||
<div class="data-mb-2">
|
||||
<button class="data-btn data-btn-secondary" data-toggle="tooltip" data-placement="auto" th:title="#{downgradeCurrentUserLongMessage}" style="padding: 0.25rem 0.5rem;">
|
||||
<span class="material-symbols-rounded">help</span>
|
||||
<span th:text="#{help}">Help</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group">
|
||||
<label for="username" class="data-form-label" th:text="#{username}">Username</label>
|
||||
<select name="username" id="username" class="data-form-control" required>
|
||||
<option value="" disabled selected th:text="#{selectFilter}">-- Select --</option>
|
||||
<option th:each="user : ${users}" th:if="${user.username != currentUsername}" th:value="${user.username}" th:text="${user.username}">Username</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group">
|
||||
<label for="role" class="data-form-label" th:text="#{adminUserSettings.role}">Role</label>
|
||||
<select name="role" id="role" class="data-form-control" required>
|
||||
<option value="" disabled selected th:text="#{selectFilter}">-- Select --</option>
|
||||
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">Role</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group">
|
||||
<label for="team" class="data-form-label" th:text="#{adminUserSettings.team}">Team</label>
|
||||
<select name="teamId" id="team" class="data-form-control" required>
|
||||
<option value="" th:text="#{selectFilter}">-- Select --</option>
|
||||
<option th:each="team : ${teams}" th:value="${team.id}" th:text="${team.name}"></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="data-form-actions">
|
||||
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
<span th:text="#{cancel}">Cancel</span>
|
||||
</button>
|
||||
<button type="submit" class="data-btn data-btn-primary">
|
||||
<span class="material-symbols-rounded">check</span>
|
||||
<span th:text="#{adminUserSettings.submit}">Save User</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add User Modal -->
|
||||
<div class="modal fade" id="addUserModal" tabindex="-1" style="z-index: 10000;" aria-labelledby="addUserModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<form id="formsaveuser" th:action="@{'/api/v1/user/admin/saveUser'}" method="post" class="modal-content data-modal">
|
||||
<div class="data-modal-header">
|
||||
<h5 class="data-modal-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">person_add</span>
|
||||
</span>
|
||||
<span th:text="#{adminUserSettings.addUser}">Add New User</span>
|
||||
</h5>
|
||||
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="data-modal-body">
|
||||
<div class="data-mb-2">
|
||||
<button class="data-btn data-btn-secondary" data-toggle="tooltip" data-placement="auto" th:title="#{adminUserSettings.usernameInfo}" style="padding: 0.25rem 0.5rem;">
|
||||
<span class="material-symbols-rounded">help</span>
|
||||
<span th:text="#{help}">Help</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group">
|
||||
<label for="username" class="data-form-label" th:text="#{username}">Username</label>
|
||||
<input type="text" class="data-form-control" name="username" id="username" th:title="#{adminUserSettings.usernameInfo}" required>
|
||||
<span id="usernameError" style="display: none; color: var(--md-sys-color-error);" th:text="#{invalidUsernameMessage}">Invalid username!</span>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group" id="passwordContainer">
|
||||
<label for="password" class="data-form-label" th:text="#{password}">Password</label>
|
||||
<input type="password" class="data-form-control" name="password" id="password" required>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group">
|
||||
<label for="role" class="data-form-label" th:text="#{adminUserSettings.role}">Role</label>
|
||||
<select name="role" class="data-form-control" id="role" required>
|
||||
<option value="" disabled selected th:text="#{selectFilter}">-- Select --</option>
|
||||
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">Role</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group">
|
||||
<label for="team" class="data-form-label" th:text="#{adminUserSettings.team}">Team</label>
|
||||
<select name="teamId" class="data-form-control" required>
|
||||
<option value="" th:text="#{selectFilter}">-- Select --</option>
|
||||
<option th:each="team : ${teams}" th:value="${team.id}" th:text="${team.name}"></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group">
|
||||
<label for="authType" class="data-form-label">Authentication Type</label>
|
||||
<select id="authType" name="authType" class="data-form-control" required>
|
||||
<option value="web" selected>WEB</option>
|
||||
<option value="sso">SSO</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="data-form-group" id="checkboxContainer">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="forceChange" name="forceChange">
|
||||
<label class="form-check-label" for="forceChange" th:text="#{adminUserSettings.forceChange}">Force user to change username/password on login</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-form-actions">
|
||||
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
<span th:text="#{cancel}">Cancel</span>
|
||||
</button>
|
||||
<button type="submit" class="data-btn data-btn-primary">
|
||||
<span class="material-symbols-rounded">check</span>
|
||||
<span th:text="#{adminUserSettings.submit}">Save User</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Team Modal -->
|
||||
<div class="modal fade" id="addTeamModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<form th:action="@{'/api/v1/team/create'}" method="post" class="modal-content data-modal">
|
||||
<div class="data-modal-header">
|
||||
<h5 class="data-modal-title">
|
||||
<span class="data-icon">
|
||||
<span class="material-symbols-rounded">group_add</span>
|
||||
</span>
|
||||
<span th:text="#{adminUserSettings.createTeam}">Create Team</span>
|
||||
</h5>
|
||||
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="data-modal-body">
|
||||
<div class="data-form-group">
|
||||
<label for="teamName" class="data-form-label" th:text="#{adminUserSettings.teamName}">Team Name</label>
|
||||
<input type="text" name="name" id="teamName" class="data-form-control" required />
|
||||
</div>
|
||||
<div class="data-form-actions">
|
||||
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
<span th:text="#{cancel}">Cancel</span>
|
||||
</button>
|
||||
<button type="submit" class="data-btn data-btn-primary">
|
||||
<span class="material-symbols-rounded">check</span>
|
||||
<span th:text="#{adminUserSettings.submit}">Create</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script th:inline="javascript">
|
||||
const delete_confirm_text = /*[[#{adminUserSettings.confirmDeleteUser}]]*/ 'Should the user be deleted?';
|
||||
const change_confirm_text = /*[[#{adminUserSettings.confirmChangeUserStatus}]]*/ 'Should the user be disabled/enabled?';
|
||||
|
||||
function confirmDeleteUser() {
|
||||
return confirm(delete_confirm_text);
|
||||
}
|
||||
|
||||
function confirmChangeUserStatus() {
|
||||
return confirm(change_confirm_text);
|
||||
}
|
||||
|
||||
jQuery.validator.addMethod("usernamePattern", function(value, element) {
|
||||
// Regular expression for user name: Min. 3 characters, max. 50 characters
|
||||
const regexUsername = /^[a-zA-Z0-9](?!.*[-@._+]{2,})([a-zA-Z0-9@._+-]{1,48})[a-zA-Z0-9]$/;
|
||||
|
||||
// Regular expression for email addresses: Max. 320 characters, with RFC-like validation
|
||||
const regexEmail = /^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.[A-Za-z]{2,})$/;
|
||||
|
||||
// Check if the field is optional or meets the requirements
|
||||
return this.optional(element) || regexUsername.test(value) || regexEmail.test(value);
|
||||
}, /*[[#{invalidUsernameMessage}]]*/ "Invalid username format");
|
||||
|
||||
$(document).ready(function() {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
|
||||
$('#formsaveuser').validate({
|
||||
rules: {
|
||||
username: {
|
||||
required: true,
|
||||
usernamePattern: true
|
||||
},
|
||||
password: {
|
||||
required: true
|
||||
},
|
||||
role: {
|
||||
required: true
|
||||
},
|
||||
authType: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
messages: {
|
||||
username: {
|
||||
usernamePattern: /*[[#{invalidUsernameMessage}]]*/ "Invalid username format"
|
||||
},
|
||||
},
|
||||
errorPlacement: function(error, element) {
|
||||
if (element.attr("name") === "username") {
|
||||
$("#usernameError").text(error.text()).show();
|
||||
} else if (element.attr("name") !== "role" && element.attr("name") !== "authType") {
|
||||
error.insertAfter(element);
|
||||
}
|
||||
},
|
||||
success: function(label, element) {
|
||||
if ($(element).attr("name") === "username") {
|
||||
$("#usernameError").hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$('#username').on('input', function() {
|
||||
var usernameInput = $(this);
|
||||
var isValid = usernameInput[0].checkValidity();
|
||||
var errorSpan = $('#usernameError');
|
||||
|
||||
if (isValid) {
|
||||
usernameInput.removeClass('invalid').addClass('valid');
|
||||
errorSpan.hide();
|
||||
} else {
|
||||
usernameInput.removeClass('valid').addClass('invalid');
|
||||
errorSpan.show();
|
||||
}
|
||||
});
|
||||
|
||||
$('#authType').on('change', function() {
|
||||
var authType = $(this).val();
|
||||
var passwordField = $('#password');
|
||||
var passwordFieldContainer = $('#passwordContainer');
|
||||
var checkboxContainer = $('#checkboxContainer');
|
||||
|
||||
if (authType === 'sso') {
|
||||
passwordField.removeAttr('required');
|
||||
passwordField.prop('disabled', true).val('');
|
||||
passwordFieldContainer.slideUp('fast');
|
||||
checkboxContainer.slideUp('fast');
|
||||
} else {
|
||||
passwordField.prop('disabled', false);
|
||||
passwordField.attr('required', 'required');
|
||||
passwordFieldContainer.slideDown('fast');
|
||||
checkboxContainer.slideDown('fast');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{autoSplitPDF.title}, header=#{autoSplitPDF.header})}">
|
||||
</th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<svg class="material-symbols-rounded tool-header-icon advance">
|
||||
<use xlink:href="/images/split-auto.svg#icon-split-auto"></use>
|
||||
</svg>
|
||||
<span class="tool-header-text" th:text="#{autoSplitPDF.header}"></span>
|
||||
</div>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/misc/auto-split-pdf'}">
|
||||
<p th:text="#{autoSplitPDF.formPrompt}"></p>
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" name="duplexMode" id="duplexMode">
|
||||
<label for="duplexMode" th:text=#{autoSplitPDF.duplexMode}></label>
|
||||
</div>
|
||||
<p>
|
||||
<a class="btn btn-outline-primary" data-bs-toggle="collapse" href="#info" role="button"
|
||||
aria-expanded="false" aria-controls="info" th:text="#{info}"></a>
|
||||
</p>
|
||||
<div class="collapse" id="info">
|
||||
<!-- Added a brief description -->
|
||||
<p th:text="#{autoSplitPDF.description}"></p>
|
||||
<ul>
|
||||
<li th:text="#{autoSplitPDF.selectText.1}"></li>
|
||||
<li th:text="#{autoSplitPDF.selectText.2}"></li>
|
||||
<li th:text="#{autoSplitPDF.selectText.3}"></li>
|
||||
<li th:text="#{autoSplitPDF.selectText.4}"></li>
|
||||
</ul>
|
||||
<p><a th:href="@{'/files/Auto%20Splitter%20Divider%20(with%20instructions).pdf'}" download
|
||||
th:text="#{autoSplitPDF.dividerDownload2}"></a></p>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{autoSplitPDF.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,88 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{changeCreds.title}, header=#{changeCreds.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-9 bg-card">
|
||||
|
||||
<!-- User Settings Title -->
|
||||
<h2 class="text-center" th:text="#{changeCreds.header}">User Settings</h2>
|
||||
<hr>
|
||||
<th:block th:if="${messageType}">
|
||||
<div class="alert alert-danger">
|
||||
<span th:text="#{${messageType}}">Default message if not found</span>
|
||||
</div>
|
||||
</th:block>
|
||||
<!-- At the top of the user settings -->
|
||||
<h3 class="text-center"><span th:text="#{welcome} + ' ' + ${username}">User</span>!</h3>
|
||||
|
||||
<!-- Change Username Form -->
|
||||
<h4 th:text="#{changeCreds.changePassword}">Change password</h4>
|
||||
<form th:action="@{'api/v1/user/change-password-on-login'}" method="post" id="formsavechangecreds">
|
||||
<div class="mb-3">
|
||||
<label for="currentPassword" th:text="#{changeCreds.oldPassword}">Old Password</label>
|
||||
<input type="password" class="form-control" name="currentPassword" id="currentPassword" th:placeholder="#{changeCreds.oldPassword}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="newPassword" th:text="#{changeCreds.newPassword}">New Password</label>
|
||||
<input type="password" class="form-control" name="newPassword" id="newPassword" th:placeholder="#{changeCreds.newPassword}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirmNewPassword" th:text="#{account.confirmNewPassword}">Confirm New Password</label>
|
||||
<input type="password" class="form-control" name="confirmNewPassword" id="confirmNewPassword" th:placeholder="#{account.confirmNewPassword}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<span id="confirmPasswordError" style="display: none;" th:text="#{confirmPasswordErrorMessage}">New Password and Confirm New Password must match.</span>
|
||||
<button type="submit" class="btn btn-primary" th:text="#{changeCreds.submit}">Change credentials!</button>
|
||||
</div>
|
||||
</form>
|
||||
<script th:inline="javascript">
|
||||
$(document).ready(function() {
|
||||
$.validator.addMethod("passwordMatch", function(value, element) {
|
||||
return $('#newPassword').val() === $('#confirmNewPassword').val();
|
||||
}, /*[[#{confirmPasswordErrorMessage}]]*/ "New Password and Confirm New Password must match.");
|
||||
$('#formsavechangecreds').validate({
|
||||
rules: {
|
||||
currentPassword: {
|
||||
required: true
|
||||
},
|
||||
newPassword: {
|
||||
required: true
|
||||
},
|
||||
confirmNewPassword: {
|
||||
required: true,
|
||||
passwordMatch: true
|
||||
},
|
||||
errorPlacement: function(error, element) {
|
||||
if ($(element).attr("name") === "newPassword" || $(element).attr("name") === "confirmNewPassword") {
|
||||
$("#confirmPasswordError").text(error.text()).show();
|
||||
} else {
|
||||
error.insertAfter(element);
|
||||
}
|
||||
},
|
||||
success: function(label, element) {
|
||||
if ($(element).attr("name") === "newPassword" || $(element).attr("name") === "confirmNewPassword") {
|
||||
$("#confirmPasswordError").hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{EMLToPDF.title}, header=#{EMLToPDF.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<div class="mt-4"></div>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convertto">email</span>
|
||||
<span class="tool-header-text" th:text="#{EMLToPDF.header}"></span>
|
||||
</div>
|
||||
<p th:text="#{processTimeWarning}"></p>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/eml/pdf'}" class="mt-4">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.eml,message/rfc822')}">
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" class="form-check-input" name="downloadHtml" id="downloadHtml">
|
||||
<label class="form-check-label" for="downloadHtml" th:text="#{EMLToPDF.downloadHtml}"></label>
|
||||
<div class="form-text" th:text="#{EMLToPDF.downloadHtmlHelp}"></div>
|
||||
</div>
|
||||
|
||||
<div id="pdfOnlyOptions">
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" class="form-check-input" name="includeAttachments" id="includeAttachments" checked>
|
||||
<label class="form-check-label" for="includeAttachments" th:text="#{EMLToPDF.includeAttachments}"></label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="maxAttachmentSizeMB" class="form-label" th:text="#{EMLToPDF.maxAttachmentSize}"></label>
|
||||
<input type="number" class="form-control" id="maxAttachmentSizeMB" name="maxAttachmentSizeMB" value="10" min="1" max="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#info" aria-expanded="false" aria-controls="info" th:text="#{info}">
|
||||
</button>
|
||||
<div class="collapse mt-3" id="info">
|
||||
<div class="card card-body">
|
||||
<p class="mb-2" th:text="#{EMLToPDF.help}"></p>
|
||||
<ul class="mb-0">
|
||||
<li th:text="#{EMLToPDF.troubleshootingTip1}"></li>
|
||||
<li th:text="#{EMLToPDF.troubleshootingTip2}"></li>
|
||||
<li th:text="#{EMLToPDF.troubleshootingTip3}"></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<br />
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{EMLToPDF.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const downloadHtml = document.getElementById('downloadHtml');
|
||||
const pdfOnlyOptions = document.getElementById('pdfOnlyOptions');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const submitText = /*[[#{EMLToPDF.submit}]]*/ 'Convert to PDF';
|
||||
const downloadHtmlText = 'Download HTML intermediate file instead of PDF';
|
||||
|
||||
function updateFormState() {
|
||||
if (pdfOnlyOptions && submitBtn) {
|
||||
pdfOnlyOptions.style.display = downloadHtml.checked ? 'none' : 'block';
|
||||
submitBtn.textContent = downloadHtml.checked ? downloadHtmlText : submitText;
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadHtml) {
|
||||
downloadHtml.addEventListener('change', updateFormState);
|
||||
updateFormState();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{fileToPDF.title}, header=#{fileToPDF.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convertto">draft</span>
|
||||
<span class="tool-header-text" th:text="#{fileToPDF.header}"></span>
|
||||
</div>
|
||||
<p th:text="#{processTimeWarning}"></p>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/file/pdf'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false)}">
|
||||
</div>
|
||||
<a class="btn btn-outline-primary" data-bs-toggle="collapse" href="#info" role="button"
|
||||
aria-expanded="false" aria-controls="info" th:text="#{fileToPDF.supportedFileTypesInfo}"></a>
|
||||
<div class="collapse" id="info">
|
||||
<p class="mt-3" th:text="#{fileToPDF.credit}"></p>
|
||||
<p class="mt-3" th:text="#{fileToPDF.supportedFileTypes}"></p>
|
||||
<p>Microsoft Word: (DOC, DOCX, DOT, DOTX)</p>
|
||||
<p>Microsoft Excel: (CSV, XLS, XLSX, XLT, XLTX, SLK, DIF)</p>
|
||||
<p>Microsoft PowerPoint: (PPT, PPTX)</p>
|
||||
<p>OpenDocument Formats: (ODT, OTT, ODS, OTS, ODP, OTP, ODG, OTG)</p>
|
||||
<p>Plain Text: (TXT, TEXT, XML)</p>
|
||||
<p>Rich Text Format: (RTF)</p>
|
||||
<p>Images: (BMP, GIF, JPEG, PNG, TIF, PBM, PGM, PPM, RAS, XBM, XPM, SVG, SVM, WMF)</p>
|
||||
<p>HTML: (HTML)</p>
|
||||
<p>Lotus Word Pro: (LWP)</p>
|
||||
<p>StarOffice: (SDA, SDC, SDD, SDW, STC, STD, STI, STW, SXD, SXG, SXI, SXW)</p>
|
||||
<p>Other: (DBF, FODS, VSD, VOR, VOR3, VOR4, UOP, PCT, PS, PDF)</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>
|
||||
<br />
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{fileToPDF.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{HTMLToPDF.title}, header=#{HTMLToPDF.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convertto">html</span>
|
||||
<span class="tool-header-text" th:text="#{HTMLToPDF.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/html/pdf'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='text/html,application/zip' )}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="zoom" th:text="#{HTMLToPDF.zoom}" class="form-label"></label>
|
||||
<input type="number" step="0.1" class="form-control" id="zoom" name="zoom" value="1">
|
||||
</div>
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{HTMLToPDF.submit}"></button>
|
||||
</form>
|
||||
<p class="mt-3" th:text="#{HTMLToPDF.help}"></p>
|
||||
<p class="mt-3" th:text="#{HTMLToPDF.credit}"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,127 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{imageToPDF.title}, header=#{imageToPDF.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convertto">picture_as_pdf</span>
|
||||
<span class="tool-header-text" th:text="#{imageToPDF.header}"></span>
|
||||
</div>
|
||||
<form id="imageToPDFForm" method="post" enctype="multipart/form-data"
|
||||
th:action="@{'/api/v1/convert/img/pdf'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='image/*', inputText=#{imgPrompt})}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="fitOption" th:text="#{imageToPDF.selectLabel}">Fit Options</label>
|
||||
<select class="form-control" id="fitOption" name="fitOption">
|
||||
<option value="fillPage" th:text="#{imageToPDF.fillPage}">Fill Page</option>
|
||||
<option value="fitDocumentToImage" th:text="#{imageToPDF.fitDocumentToImage}">Fit Document to Image
|
||||
</option>
|
||||
<option value="maintainAspectRatio" th:text="#{imageToPDF.maintainAspectRatio}">Maintain Aspect Ratio
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" name="autoRotate" id="autoRotate">
|
||||
<label for="autoRotate" th:text=#{imageToPDF.selectText.2}></label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label th:text="#{pdfToImage.colorType}"></label>
|
||||
<select class="form-control" id="colorType" name="colorType">
|
||||
<option value="color" th:text="#{pdfToImage.color}"></option>
|
||||
<option value="greyscale" th:text="#{pdfToImage.grey}"></option>
|
||||
<option value="blackwhite" th:text="#{pdfToImage.blackwhite}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="hidden" id="override" name="override" value="multi">
|
||||
<div class="mb-3">
|
||||
<label th:text=#{imageToPDF.selectText.3}></label>
|
||||
<select class="form-control" id="conversionType" name="conversionType" disabled>
|
||||
<option value="merge" th:text=#{imageToPDF.selectText.4}></option>
|
||||
<option value="convert" th:text=#{imageToPDF.selectText.5} selected></option>
|
||||
</select>
|
||||
</div>
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{imageToPDF.submit}"></button>
|
||||
<script>
|
||||
$('#fileInput-input').on('change', function (e) {
|
||||
const fileInput = e.target;
|
||||
fileInput.addEventListener('file-input-change', async (e) => {
|
||||
const { allFiles } = e.detail;
|
||||
var conversionType = document.getElementById("conversionType");
|
||||
console.log("files.length=" + allFiles.length)
|
||||
if (allFiles.length > 1) {
|
||||
conversionType.disabled = false;
|
||||
} else {
|
||||
conversionType.disabled = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#conversionType').change(function () {
|
||||
var selectedValue = $(this).val();
|
||||
var override = document.getElementById("override");
|
||||
console.log("selectedValue=" + selectedValue)
|
||||
if (selectedValue === 'merge') {
|
||||
override.value = "single";
|
||||
} else if (selectedValue === 'convert') {
|
||||
override.value = "multi";
|
||||
}
|
||||
});
|
||||
document.getElementById("imageToPDFForm").addEventListener("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const formData = new FormData(form);
|
||||
|
||||
const fitOptionSelect = document.getElementById("fitOption");
|
||||
const fitOptionValue = fitOptionSelect ? fitOptionSelect.value : "fit-to-page"; // Default value
|
||||
formData.append("fitOption", fitOptionValue);
|
||||
|
||||
const overrideInput = document.getElementById("override");
|
||||
console.log("Override value before submission:", overrideInput.value);
|
||||
|
||||
for (let [key, value] of formData.entries()) {
|
||||
console.log(`${key}:`, value);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(form.action, {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log("Form successfully submitted");
|
||||
} else {
|
||||
console.error("Failed to submit the form");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error submitting the form:", error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{MarkdownToPDF.title}, header=#{MarkdownToPDF.header})}">
|
||||
</th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convertto">markdown</span>
|
||||
<span class="tool-header-text" th:text="#{MarkdownToPDF.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/markdown/pdf'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.md')}">
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{MarkdownToPDF.submit}"></button>
|
||||
</form>
|
||||
<p class="mt-3" th:text="#{MarkdownToPDF.help}"></p>
|
||||
<p class="mt-3" th:text="#{MarkdownToPDF.credit}"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{PDFToCSV.title}, header=#{PDFToCSV.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convert">csv</span>
|
||||
<span class="tool-header-text" th:text="#{PDFToCSV.header}"></span>
|
||||
</div>
|
||||
<form id="PDFToCSVForm" th:action="@{'/api/v1/convert/pdf/csv'}" method="post" enctype="multipart/form-data">
|
||||
<input id="pageNumbers" type="hidden" name="pageNumbers">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToCSV.submit}"></button>
|
||||
</form>
|
||||
<p id="instruction-text" style="margin: 0; display: none" th:text="#{PDFToCSV.prompt}"></p>
|
||||
|
||||
<div style="position: relative; width: auto;" id="canvasesContainer">
|
||||
<div>
|
||||
<div style="display:none ;margin: 3px;position: absolute;top: 0;width: 120px;justify-content:space-between;z-index: 10" id="pagination-button-container">
|
||||
<button id='previous-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff; ;border: none;outline: none; border-radius: 4px;'> < </button>
|
||||
<button id='next-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff; ;border: none;outline: none; border-radius: 4px;'> > </button>
|
||||
</div>
|
||||
<canvas id="cropPdfCanvas" style="width: 100%"></canvas>
|
||||
</div>
|
||||
<canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2; width: 100%"></canvas>
|
||||
</div>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<script type="module" th:src="@{'/js/pages/pdf-to-csv.js'}">
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{PDFToHTML.title}, header=#{PDFToHTML.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convert">html</span>
|
||||
<span class="tool-header-text" th:text="#{PDFToHTML.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/html'}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToHTML.submit}"></button>
|
||||
</form>
|
||||
<p class="mt-3" th:text="#{PDFToHTML.credit}"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pdfToImage.title}, header=#{pdfToImage.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convert">photo_library</span>
|
||||
<span class="tool-header-text" th:text="#{pdfToImage.header}"></span>
|
||||
</div>
|
||||
<p th:text="#{processTimeWarning}"></p>
|
||||
<p th:if="${!isPython}" th:text="#{pdfToImage.info}">Python is not installed. Required for WebP conversion.
|
||||
</p>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/img'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label th:text="#{pdfToImage.selectText}"></label>
|
||||
<select class="form-control" name="imageFormat">
|
||||
<option value="png">PNG</option>
|
||||
<option value="jpg">JPG</option>
|
||||
<option value="gif">GIF</option>
|
||||
<option value="tiff">TIFF</option>
|
||||
<option value="bmp">BMP</option>
|
||||
<option th:if="${isPython}" value="webp">WEBP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label th:text="#{pdfToImage.singleOrMultiple}"></label>
|
||||
<select class="form-control" name="singleOrMultiple">
|
||||
<option value="multiple" th:text="#{pdfToImage.multi}"></option>
|
||||
<option value="single" th:text="#{pdfToImage.single}"></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="singleOptionSection">
|
||||
<label for="pageNumbers" th:text="#{pageSelectionPrompt}"></label>
|
||||
<input type="text" name="pageNumbers" class="form-control" id="pageNumbers" value="all"
|
||||
th:placeholder="#{pdfToImage.placeholder}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label th:text="#{pdfToImage.colorType}"></label>
|
||||
<select class="form-control" name="colorType">
|
||||
<option value="color" th:text="#{pdfToImage.color}"></option>
|
||||
<option value="greyscale" th:text="#{pdfToImage.grey}"></option>
|
||||
<option value="blackwhite" th:text="#{pdfToImage.blackwhite}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dpi">DPI:</label>
|
||||
<input type="number" name="dpi" class="form-control" id="dpi" min="1" step="1" value="300" required>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfToImage.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{PDFToMarkdown.title}, header=#{PDFToMarkdown.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convert">markdown_copy</span>
|
||||
<span class="tool-header-text" th:text="#{PDFToMarkdown.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/markdown'}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.pdf')}"></div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToMarkdown.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,78 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pdfToPDFA.title}, header=#{pdfToPDFA.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convert">picture_as_pdf</span>
|
||||
<span class="tool-header-text" th:text="#{pdfToPDFA.header}"></span>
|
||||
</div>
|
||||
<p th:text="#{pdfToPDFA.tip}"></p>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/pdfa'}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<div class="mb-3">
|
||||
<label for="outputFormat" th:text="#{pdfToPDFA.outputFormat}"></label>
|
||||
<select class="form-control" name="outputFormat" id="outputFormat">
|
||||
<option value="pdfa-1">PDF/A-1b</option>
|
||||
<option value="pdfa">PDF/A-2b</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="result" class="alert-warning"></div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfToPDFA.submit}"></button>
|
||||
</form>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<script th:inline="javascript">
|
||||
document.getElementById('fileInput-input').addEventListener('change', async () => {
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||
const fileInput = document.getElementById('fileInput-input');
|
||||
const resultDiv = document.getElementById('result');
|
||||
|
||||
const file = fileInput.files[0];
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
try {
|
||||
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||
|
||||
let hasSignature = false;
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const annotations = await page.getAnnotations({ intent: 'display' });
|
||||
|
||||
annotations.forEach(annotation => {
|
||||
console.log(annotation)
|
||||
if (annotation.subtype === 'Widget' && annotation.fieldType === 'Sig') {
|
||||
hasSignature = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (hasSignature) {
|
||||
/*<![CDATA[*/
|
||||
resultDiv.textContent = /*[[#{pdfToPDFA.pdfWithDigitalSignature}]]*/ "The PDF contains a digital signature. This will be removed in the next step.";
|
||||
/*]]>*/
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.textContent = 'Error reading the PDF: ' + error.message;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<p class="mt-3" th:text="#{pdfToPDFA.credit}"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block
|
||||
th:insert="~{fragments/common :: head(title=#{PDFToPresentation.title}, header=#{PDFToPresentation.header})}">
|
||||
</th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convert">slideshow</span>
|
||||
<span class="tool-header-text" th:text="#{PDFToPresentation.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/presentation'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label th:text="#{PDFToPresentation.selectText.1}"></label>
|
||||
<select class="form-control" name="outputFormat">
|
||||
<option value="pptx">PPTX</option>
|
||||
<option value="ppt">PPT</option>
|
||||
<option value="odp">ODP</option>
|
||||
</select>
|
||||
</div>
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary"
|
||||
th:text="#{PDFToPresentation.submit}"></button>
|
||||
</form>
|
||||
<p class="mt-3" th:text="#{PDFToPresentation.credit}"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{PDFToText.title}, header=#{PDFToText.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convert">text_fields</span>
|
||||
<span class="tool-header-text" th:text="#{PDFToText.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/text'}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<div class="mb-3">
|
||||
<label th:text="#{PDFToText.selectText.1}"></label>
|
||||
<select class="form-control" name="outputFormat">
|
||||
<option th:if="${@endpointConfiguration.isEndpointEnabled('pdf-to-rtf')}" value="rtf">RTF</option>
|
||||
<option value="txt">TXT</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToText.submit}"></button>
|
||||
</form>
|
||||
<p th:if="${@endpointConfiguration.isEndpointEnabled('pdf-to-rtf')}" class="mt-3" th:text="#{PDFToText.credit}"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{PDFToWord.title}, header=#{PDFToWord.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convert">description</span>
|
||||
<span class="tool-header-text" th:text="#{PDFToWord.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/word'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label th:text="#{PDFToWord.selectText.1}"></label>
|
||||
<select class="form-control" name="outputFormat">
|
||||
<option value="docx">DocX</option>
|
||||
<option value="doc">Doc</option>
|
||||
<option value="odt">Odt</option>
|
||||
</select>
|
||||
</div>
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToWord.submit}"></button>
|
||||
</form>
|
||||
<p class="mt-3" th:text="#{PDFToWord.credit}"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{PDFToXML.title}, header=#{PDFToXML.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convert">code</span>
|
||||
<span class="tool-header-text" th:text="#{PDFToXML.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/xml'}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToXML.submit}"></button>
|
||||
</form>
|
||||
<p class="mt-3" th:text="#{PDFToXML.credit}"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{URLToPDF.title}, header=#{URLToPDF.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convertto">link</span>
|
||||
<span class="tool-header-text" th:text="#{URLToPDF.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/url/pdf'}">
|
||||
<input type="text" class="form-control" id="urlInput" name="urlInput" placeholder="http://">
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{URLToPDF.submit}"></button>
|
||||
</form>
|
||||
<p class="mt-3" th:text="#{URLToPDF.credit}"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{crop.title}, header=#{crop.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">crop</span>
|
||||
<span class="tool-header-text" th:text="#{crop.header}"></span>
|
||||
</div>
|
||||
<form id="cropForm" th:action="@{'/api/v1/general/crop'}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<input id="x" type="hidden" name="x">
|
||||
<input id="y" type="hidden" name="y">
|
||||
<input id="width" type="hidden" name="width">
|
||||
<input id="height" type="hidden" name="height">
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{crop.submit}"></button>
|
||||
</form>
|
||||
<div id="canvasesContainer" style="position: relative; margin: 20px 0; width: auto;">
|
||||
<canvas id="cropPdfCanvas" style="width: 100%"></canvas>
|
||||
<canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2; width: 100%"></canvas>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<script type="module" th:src="@{'/js/pages/crop.js'}"></script>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{database.title}, header=#{database.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-9 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">database</span>
|
||||
<span class="tool-header-text" th:text="#{database.title}">Database Im-/Export</span>
|
||||
</div>
|
||||
<p th:if="${error}" th:text="#{'database.' + ${error}}" class="alert alert-danger text-center"></p>
|
||||
<p th:if="${infoMessage}" th:text="#{'database.' + ${infoMessage}}" class="alert alert-success text-center"></p>
|
||||
<div th:if="${databaseVersion!='Unknown'}" class="bg-card mt-3 mb-3">
|
||||
<table class="table table-striped table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" th:text="#{database.fileName}">File Name</th>
|
||||
<th scope="col" th:text="#{database.creationDate}">Creation Date</th>
|
||||
<th scope="col" th:text="#{database.fileSize}">File Size</th>
|
||||
<th scope="col"></th>
|
||||
<th scope="col"></th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="backup : ${backupFiles}">
|
||||
<td th:text="${backup.fileName}"></td>
|
||||
<td th:text="${backup.formattedCreationDate}"></td>
|
||||
<td th:text="${backup.formattedFileSize}"></td>
|
||||
<td th:title="#{database.deleteBackupFile}"><a th:href="@{'/api/v1/database/delete/' + ${backup.fileName}}" style="color: red;"><span class="material-symbols-rounded">delete</span></a></td>
|
||||
<td th:title="#{database.importBackupFile}"><a th:href="@{'/api/v1/database/import-database-file/' + ${backup.fileName}}" style="color: var(--md-nav-section-color-organize);"><span class="material-symbols-rounded">backup</span></a></td>
|
||||
<td th:title="#{database.downloadBackupFile}"><a th:href="@{'/api/v1/database/download/' + ${backup.fileName}}"><span class="material-symbols-rounded">download</span></a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h6 class="text-end"><span class="badge bg-dark" th:text="${databaseVersion}">DB-Version</span></h6>
|
||||
<a th:title="#{database.createBackupFile}" th:text="#{database.createBackupFile}" th:href="@{'/api/v1/database/createDatabaseBackup'}" class="btn btn-outline-primary">Create Backup File</a>
|
||||
</div>
|
||||
<hr>
|
||||
<form th:if="${databaseVersion!='Unknown'}" th:action="@{'/api/v1/database/import-database'}" method="post" enctype="multipart/form-data" class="bg-card mt-3 mb-3">
|
||||
<div style="background: var(--md-sys-color-error-container);border-radius: 2rem;" class="mb-3 p-3">
|
||||
<p th:text="#{database.info_1}"></p>
|
||||
<p th:text="#{database.info_2}"></p>
|
||||
</div>
|
||||
<div class="custom-file-chooser">
|
||||
<div class="mb-3">
|
||||
<input type="file" class="form-control" th:name="fileInput" id="fileInput-input" accept=".sql" required>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{database.submit}">Import Backup</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script th:inline="javascript">
|
||||
const pdfPasswordPrompt = /*[[#{error.pdfPassword}]]*/ '';
|
||||
const multiple = /*[[${multiple}]]*/ false;
|
||||
const remoteCall = /*[[${remoteCall}]]*/ true;
|
||||
</script>
|
||||
<script th:src="@{'/js/fileInput.js'}"></script>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{editTableOfContents.title}, header=#{editTableOfContents.header})}">
|
||||
</th:block>
|
||||
<link rel="stylesheet" th:href="@{'/css/edit-table-of-contents.css'}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon edit">bookmark_add</span>
|
||||
<span class="tool-header-text" th:text="#{editTableOfContents.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'/api/v1/general/edit-table-of-contents'}" method="post" enctype="multipart/form-data" id="editTocForm">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="replaceExisting" name="replaceExisting" checked>
|
||||
<label class="form-check-label" for="replaceExisting"
|
||||
th:text="#{editTableOfContents.replaceExisting}"></label>
|
||||
<input type="hidden" name="replaceExisting" value="false" />
|
||||
</div>
|
||||
|
||||
<div class="bookmark-editor">
|
||||
<h5 th:text="#{editTableOfContents.editorTitle}"></h5>
|
||||
<p th:text="#{editTableOfContents.editorDesc}"></p>
|
||||
|
||||
<div id="bookmarks-container">
|
||||
<!-- Bookmarks will be added here dynamically -->
|
||||
</div>
|
||||
|
||||
<div class="bookmark-actions">
|
||||
<button type="button" id="addBookmarkBtn" class="btn btn-outline-primary" th:text="#{editTableOfContents.addBookmark}"></button>
|
||||
</div>
|
||||
|
||||
<!-- Hidden field to store JSON data -->
|
||||
<input type="hidden" id="bookmarkData" name="bookmarkData" value="[]">
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<a class="btn btn-outline-primary" data-bs-toggle="collapse" href="#info" role="button"
|
||||
aria-expanded="false" aria-controls="info" th:text="#{info}"></a>
|
||||
</p>
|
||||
<div class="collapse" id="info">
|
||||
<p th:text="#{editTableOfContents.desc.1}"></p>
|
||||
<p th:text="#{editTableOfContents.desc.2}"></p>
|
||||
<p th:text="#{editTableOfContents.desc.3}"></p>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{editTableOfContents.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
|
||||
<script th:src="@{'/js/pages/edit-table-of-contents.js'}"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize Bootstrap tooltips
|
||||
if (typeof $ !== 'undefined') {
|
||||
$('[data-bs-toggle="tooltip"]').tooltip();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{error.404.head})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<div class="container">
|
||||
<div th:insert="~{fragments/errorBanner.html :: errorBanner}"></div>
|
||||
<div class="bg-card text-center">
|
||||
<h1 class="display-2" th:text="#{oops}"></h1>
|
||||
<p class="lead" th:if="${param.status == '404'}" th:text="#{error.404.1}"></p>
|
||||
<p class="lead" th:unless="${param.status == '404'}" th:text="#{error.404.2}"></p>
|
||||
<br>
|
||||
<h2 th:text="#{error.needHelp}"></h2>
|
||||
<p th:text="#{error.contactTip}"></p>
|
||||
<div id="button-group">
|
||||
<a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" class="btn btn-primary" target="_blank" th:text="#{error.github}"></a>
|
||||
<a href="https://discord.gg/HYmhKj45pU" id="discord-button" class="btn btn-primary" target="_blank" th:text="#{joinDiscord}"></a>
|
||||
</div>
|
||||
<a th:href="@{'/'}" id="home-button" class="home-button btn btn-primary" th:text="#{goHomepage}"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}">
|
||||
</th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<th:block th:insert="~{fragments/multi-toolAdvert.html :: multi-toolAdvert}"></th:block>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">upload</span>
|
||||
<span class="tool-header-text" th:text="#{pageExtracter.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'/api/v1/general/rearrange-pages'}" method="post" enctype="multipart/form-data">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
<input type="hidden" id="customMode" name="customMode" value="">
|
||||
<div class="mb-3">
|
||||
<label for="pageOrder" th:text="#{pageOrderPrompt}"></label>
|
||||
<input type="text" class="form-control" id="pageOrder" name="pageNumbers"
|
||||
th:placeholder="#{pageExtracter.placeholder}" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pageExtracter.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
<div th:fragment="card" class="feature-card hidden" th:id="${id}" th:if="${@endpointConfiguration.isEndpointEnabled(cardLink)} "
|
||||
th:data-bs-tags="${tags}"
|
||||
th:data-bs-link="@{${endpoint}}">
|
||||
<a th:href="${cardLink}">
|
||||
<div class="d-flex align-items-center">
|
||||
|
||||
<div id="tool-icon" class="icon" alt="icon" th:class="@{${toolGroup}}">
|
||||
<span class="material-symbols-rounded nav-icon" th:text="@{${toolIcon}}"></span>
|
||||
</div>
|
||||
|
||||
<div id="tool-text">
|
||||
<h5 class="card-title" th:text="${cardTitle}"></h5>
|
||||
<p class="card-text" th:text="${cardText}"></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</a>
|
||||
<span class="material-symbols-rounded favorite-icon" th:data-endpoint="@{${endpoint}}" th:onclick="'addToFavorites(\'' + @{${cardLink}} + '\')'">
|
||||
star
|
||||
</span>
|
||||
</div>
|
||||
@@ -0,0 +1,426 @@
|
||||
<th:block th:fragment="head">
|
||||
<!-- Title -->
|
||||
<title th:text="${@appName} + (${title} != null and ${title} != '' ? ' - ' + ${title} : '')"></title>
|
||||
|
||||
<!-- Metadata -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="description" th:content="${@appName} + (${header} != null and ${header} != '' ? ' - ' + ${header} : '')">
|
||||
<meta name="msapplication-TileColor" content="#2d89ef">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- Icons -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" th:href="@{'/apple-touch-icon.png'}">
|
||||
<link rel="icon" type="image/png" sizes="32x32" th:href="@{'/favicon-32x32.png'}">
|
||||
<link rel="icon" type="image/png" sizes="16x16" th:href="@{'/favicon-16x16.png'}">
|
||||
<link rel="manifest" th:href="@{'/site.webmanifest'}" crossorigin="use-credentials">
|
||||
<link rel="mask-icon" th:href="@{'/safari-pinned-tab.svg'}" color="#ca2b2a">
|
||||
<link rel="shortcut icon" th:href="@{'/favicon.ico'}">
|
||||
<meta name="apple-mobile-web-app-title" content="Stirling PDF">
|
||||
<meta name="application-name" content="Stirling PDF">
|
||||
<meta name="msapplication-TileColor" content="#00aba9">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<script>
|
||||
window.stirlingPDF = window.stirlingPDF || {};
|
||||
|
||||
// Capture true system DPI at page load (before any zoom interactions)
|
||||
const systemDPR = window.devicePixelRatio || 1;
|
||||
|
||||
// Determine if this is actually a high DPI screen at page load
|
||||
const isHighDPI = systemDPR > 1.4;
|
||||
|
||||
function scaleNav() {
|
||||
const currentDPR = window.devicePixelRatio || 1;
|
||||
const browserZoom = currentDPR / systemDPR;
|
||||
|
||||
// Counter-scale to maintain same visual size
|
||||
const isMobile = window.innerWidth <= 768 && /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
let baseScale = isMobile ? 3 : (isHighDPI ? 2.2 : 1.1); // Prioritize mobile scaling over high DPI
|
||||
const navScale = baseScale / currentDPR;
|
||||
// Dropdowns at 80% (20% smaller)
|
||||
const dropdownScale = 0.8;
|
||||
|
||||
|
||||
const navbar = document.querySelector('.navbar');
|
||||
|
||||
if (navbar) {
|
||||
// RTL support - check document direction
|
||||
const isRTL = document.documentElement.dir === 'rtl' || document.documentElement.getAttribute('dir') === 'rtl';
|
||||
|
||||
const translateX = isRTL ? '50%' : '-50%';
|
||||
navbar.style.transform = `translateX(${translateX}) scale(${navScale})`;
|
||||
navbar.style.transformOrigin = 'top center';
|
||||
navbar.style.width = `${100 / navScale}%`;
|
||||
|
||||
if (isRTL) {
|
||||
navbar.style.right = '50%';
|
||||
navbar.style.left = 'auto';
|
||||
} else {
|
||||
navbar.style.left = '50%';
|
||||
navbar.style.right = 'auto';
|
||||
}
|
||||
|
||||
// Adjust bottom margin based on scale to prevent overlap/gaps
|
||||
const baseHeight = 60; // Assume base navbar height
|
||||
const scaledHeight = baseHeight * navScale;
|
||||
const marginAdjustment = scaledHeight - baseHeight;
|
||||
navbar.style.marginBottom = `${marginAdjustment}px`;
|
||||
|
||||
// Adjust responsive breakpoint based on effective width after scaling
|
||||
const effectiveWidth = window.innerWidth / navScale;
|
||||
if (effectiveWidth >= 1200) {
|
||||
navbar.classList.add('navbar-expand-lg');
|
||||
navbar.classList.remove('navbar-expand-xl');
|
||||
} else {
|
||||
navbar.classList.add('navbar-expand-xl');
|
||||
navbar.classList.remove('navbar-expand-lg');
|
||||
}
|
||||
|
||||
|
||||
console.log('DPR:', currentDPR, 'isHighDPI:', isHighDPI, 'baseScale:', baseScale, 'navScale:', navScale, 'effective width:', effectiveWidth);
|
||||
}
|
||||
|
||||
// Set CSS custom property for mobile navbar scaling (for sidebar positioning)
|
||||
// Use the ACTUAL scaled height, not a fixed assumption
|
||||
const baseHeight = 60;
|
||||
const actualScaledHeight = baseHeight * navScale;
|
||||
document.documentElement.style.setProperty('--navbar-height', `${actualScaledHeight}px`);
|
||||
|
||||
setTimeout(() => {
|
||||
const dropdowns = document.querySelectorAll('.dropdown-menu');
|
||||
dropdowns.forEach(dropdown => {
|
||||
dropdown.style.transform = `scale(${dropdownScale})`;
|
||||
|
||||
// Use different transform origins based on dropdown position
|
||||
const parentItem = dropdown.closest('.nav-item');
|
||||
const navbar = dropdown.closest('.navbar-nav');
|
||||
|
||||
// Check if this is a right-aligned dropdown (language, favorites, search, etc.)
|
||||
const isRightAligned = navbar && navbar.classList.contains('flex-nowrap') &&
|
||||
!parentItem?.closest('.dropdown-mega');
|
||||
|
||||
dropdown.style.transformOrigin = isRightAligned ? 'top right' : 'top left';
|
||||
});
|
||||
|
||||
console.log('Applied dropdown scale:', dropdownScale);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
let lastDPR = window.devicePixelRatio;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', scaleNav);
|
||||
window.addEventListener('resize', scaleNav);
|
||||
|
||||
setInterval(() => {
|
||||
const currentDPR = window.devicePixelRatio;
|
||||
if (Math.abs(currentDPR - lastDPR) > 0.01) {
|
||||
lastDPR = currentDPR;
|
||||
scaleNav();
|
||||
}
|
||||
}, 100);
|
||||
</script>
|
||||
<script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
|
||||
<script th:src="@{'/js/fetch-utils.js'}"></script>
|
||||
<!-- jQuery -->
|
||||
<script th:src="@{'/js/thirdParty/jquery.min.js'}"></script>
|
||||
<script th:src="@{'/js/thirdParty/jquery.validate.min.js'}"></script>
|
||||
<script th:src="@{'/js/thirdParty/jszip.min.js'}" th:if="${currentPage != 'home'}"></script>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<script th:src="@{'/js/thirdParty/popper.min.js'}"></script>
|
||||
<script th:src="@{'/js/thirdParty/bootstrap.min.js'}"></script>
|
||||
|
||||
<link rel="stylesheet" th:href="@{'/css/bootstrap.min.css'}">
|
||||
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" th:href="@{'/css/bootstrap-icons.min.css'}">
|
||||
|
||||
<!-- Pixel, doesn't collect any PII-->
|
||||
<img th:if="${!@disablePixel}" referrerpolicy="no-referrer-when-downgrade"
|
||||
th:src="'https://pixel.stirlingpdf.com/a.png?x-pxid=4f5fa02f-a065-4efb-bb2c-24509a4b6b92'
|
||||
+ '&machineType=' + ${@machineType}
|
||||
+ '&appVersion=' + ${@appVersion}
|
||||
+ '&licenseType=' + ${@license}
|
||||
+ '&loginEnabled=' + ${@loginEnabled}"
|
||||
style="position: absolute; visibility: hidden;" />
|
||||
|
||||
<!-- Custom -->
|
||||
<link rel="stylesheet" th:href="@{'/css/general.css'}">
|
||||
<link rel="stylesheet" th:href="@{'/css/theme/theme.css'}">
|
||||
<link rel="stylesheet" th:href="@{'/css/theme/componentes.css'}">
|
||||
<link rel="stylesheet" th:href="@{'/css/theme/theme.light.css'}" id="light-mode-styles">
|
||||
<link rel="stylesheet" th:href="@{'/css/theme/theme.dark.css'}" id="dark-mode-styles">
|
||||
<link rel="stylesheet" th:href="@{'/css/rainbow-mode.css'}" id="rainbow-mode-styles" disabled>
|
||||
<link rel="stylesheet" th:href="@{'/css/tab-container.css'}">
|
||||
<link rel="stylesheet" th:href="@{'/css/navbar.css'}">
|
||||
|
||||
<link rel="stylesheet" th:href="@{'/css/error.css'}" th:if="${error}">
|
||||
|
||||
<link rel="stylesheet" th:href="@{'/css/home.css'}" th:if="${currentPage == 'home'}">
|
||||
<link rel="stylesheet" th:href="@{'/css/home-legacy.css'}" th:if="${currentPage == 'home-legacy'}">
|
||||
<link rel="stylesheet" th:href="@{'/css/account.css'}" th:if="${currentPage == 'account'}">
|
||||
<link rel="stylesheet" th:href="@{'/css/licenses.css'}" th:if="${currentPage == 'licenses'}">
|
||||
<link rel="stylesheet" th:href="@{'/css/multi-tool.css'}" th:if="${currentPage == 'multi-tool'}">
|
||||
<link rel="stylesheet" th:href="@{'/css/rotate-pdf.css'}" th:if="${currentPage == 'rotate-pdf'}">
|
||||
<link rel="stylesheet" th:href="@{'/css/stamp.css'}" th:if="${currentPage == 'stamp'}">
|
||||
<link rel="stylesheet" th:href="@{'/css/fileSelect.css'}" th:if="${currentPage != 'home'}">
|
||||
<link rel="stylesheet" th:href="@{'/css/footer.css'}">
|
||||
|
||||
<link rel="preload" th:href="@{'/fonts/google-symbol.woff2'}" as="font" type="font/woff2" crossorigin="anonymous">
|
||||
<link rel="stylesheet" th:href="@{'/css/cookieconsent.css'}">
|
||||
<link rel="stylesheet" th:href="@{'/css/cookieconsentCustomisation.css'}">
|
||||
<script th:src="@{'/js/thirdParty/fontfaceobserver.standalone.js'}"></script>
|
||||
|
||||
<!-- Google MD Icons -->
|
||||
<link rel="stylesheet" th:href="@{'/css/theme/font.css'}">
|
||||
|
||||
<!-- Help Modal -->
|
||||
<link rel="stylesheet" th:href="@{'/css/errorBanner.css'}" th:if="${currentPage != 'home'}">
|
||||
|
||||
<script th:src="@{'/js/cacheFormInputs.js'}" th:if="${currentPage != 'home'}"></script>
|
||||
<script th:src="@{'/js/tab-container.js'}"></script>
|
||||
<script th:src="@{'/js/darkmode.js'}"></script>
|
||||
<script th:src="@{'/js/csrf.js'}"></script>
|
||||
<script th:inline="javascript">
|
||||
|
||||
function UpdatePosthogConsent(){
|
||||
if(typeof(posthog) == "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
window.CookieConsent.acceptedCategory('analytics')?
|
||||
posthog.opt_in_capturing() : posthog.opt_out_capturing();
|
||||
}
|
||||
const stirlingPDFLabel = /*[[${@StirlingPDFLabel}]]*/ '';
|
||||
const analyticsEnabled = /*[[${@analyticsEnabled}]]*/ false;
|
||||
|
||||
const cookieBannerPopUpTitle = /*[[#{cookieBanner.popUp.title}]]*/ "How we use Cookies";
|
||||
const cookieBannerPopUpDescription1 = /*[[#{cookieBanner.popUp.description.1}]]*/ "";
|
||||
const cookieBannerPopUpDescription2 = /*[[#{cookieBanner.popUp.description.2}]]*/ "";
|
||||
const cookieBannerPopUpAcceptAllBtn = /*[[#{cookieBanner.popUp.acceptAllBtn}]]*/ "";
|
||||
const cookieBannerPopUpAcceptNecessaryBtn = /*[[#{cookieBanner.popUp.acceptNecessaryBtn}]]*/ "";
|
||||
const cookieBannerPopUpShowPreferencesBtn = /*[[#{cookieBanner.popUp.showPreferencesBtn}]]*/ "";
|
||||
const cookieBannerPreferencesModalTitle = /*[[#{cookieBanner.preferencesModal.title}]]*/ "";
|
||||
const cookieBannerPreferencesModalAcceptAllBtn = /*[[#{cookieBanner.preferencesModal.acceptAllBtn}]]*/ "";
|
||||
const cookieBannerPreferencesModalAcceptNecessaryBtn = /*[[#{cookieBanner.preferencesModal.acceptNecessaryBtn}]]*/ "";
|
||||
const cookieBannerPreferencesModalSavePreferencesBtn = /*[[#{cookieBanner.preferencesModal.savePreferencesBtn}]]*/ "";
|
||||
const cookieBannerPreferencesModalCloseIconLabel = /*[[#{cookieBanner.preferencesModal.closeIconLabel}]]*/ "";
|
||||
const cookieBannerPreferencesModalServiceCounterLabel = /*[[#{cookieBanner.preferencesModal.serviceCounterLabel}]]*/ "";
|
||||
const cookieBannerPreferencesModalSubtitle = /*[[#{cookieBanner.preferencesModal.subtitle}]]*/ "";
|
||||
const cookieBannerPreferencesModalDescription1 = /*[[#{cookieBanner.preferencesModal.description.1}]]*/ "";
|
||||
const cookieBannerPreferencesModalDescription2= /*[[#{cookieBanner.preferencesModal.description.2}]]*/ "";
|
||||
const cookieBannerPreferencesModalDescription3 = /*[[#{cookieBanner.preferencesModal.description.3}]]*/ "";
|
||||
const cookieBannerPreferencesModalNecessaryTitle1 = /*[[#{cookieBanner.preferencesModal.necessary.title.1}]]*/ "";
|
||||
const cookieBannerPreferencesModalNecessaryTitle2 = /*[[#{cookieBanner.preferencesModal.necessary.title.2}]]*/ "";
|
||||
const cookieBannerPreferencesModalNecessaryDescription = /*[[#{cookieBanner.preferencesModal.necessary.description}]]*/ "";
|
||||
const cookieBannerPreferencesModalAnalyticsTitle = /*[[#{cookieBanner.preferencesModal.analytics.title}]]*/ "";
|
||||
const cookieBannerPreferencesModalAnalyticsDescription = /*[[#{cookieBanner.preferencesModal.analytics.description}]]*/ "";
|
||||
|
||||
if (analyticsEnabled) {
|
||||
!function (t, e) {
|
||||
var o, n, p, r;
|
||||
e.__SV || (window.posthog = e, e._i = [], e.init = function (i, s, a) {
|
||||
function g(t, e) {
|
||||
var o = e.split(".");
|
||||
2 == o.length && (t = t[o[0]], e = o[1]), t[e] = function () {
|
||||
t.push([e].concat(Array.prototype.slice.call(arguments, 0)))
|
||||
}
|
||||
}
|
||||
|
||||
(p = t.createElement("script")).type = "text/javascript", p.async = !0, p.src = s.api_host + "/static/array.js", (r = t.getElementsByTagName("script")[0]).parentNode.insertBefore(p, r);
|
||||
var u = e;
|
||||
for (void 0 !== a ? u = e[a] = [] : a = "posthog", u.people = u.people || [], u.toString = function (t) {
|
||||
var e = "posthog";
|
||||
return "posthog" !== a && (e += "." + a), t || (e += " (stub)"), e
|
||||
}, u.people.toString = function () {
|
||||
return u.toString(1) + ".people (stub)"
|
||||
}, o = "capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "), n = 0; n < o.length; n++) g(u, o[n]);
|
||||
e._i.push([i, s, a])
|
||||
}, e.__SV = 1)
|
||||
}(document, window.posthog || []);
|
||||
posthog.init('phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq', {
|
||||
api_host: 'https://eu.i.posthog.com',
|
||||
persistence: 'localStorage',
|
||||
person_profiles: 'always',
|
||||
mask_all_text: true,
|
||||
mask_all_element_attributes: true,
|
||||
opt_out_capturing_by_default: true
|
||||
})
|
||||
const baseUrl = window.location.hostname;
|
||||
posthog.register_once({
|
||||
'hostname': baseUrl,
|
||||
'UUID': /*[[${@UUID}]]*/ '',
|
||||
'app_version': /*[[${@appVersion}]]*/ '',
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("cc:onConsent", UpdatePosthogConsent);
|
||||
window.addEventListener("cc:onChange", UpdatePosthogConsent);
|
||||
</script>
|
||||
</th:block>
|
||||
|
||||
<th:block th:fragment="game">
|
||||
<dialog id="game-container-wrapper" class="game-container-wrapper" data-bs-modal>
|
||||
<script th:inline="javascript">
|
||||
console.log("loaded game");
|
||||
$(document).ready(function () {
|
||||
|
||||
// Find the file input within the form
|
||||
var fileInput = $('input[type="file"]');
|
||||
|
||||
// Find the closest enclosing form of the file input
|
||||
var form = fileInput.closest('form');
|
||||
|
||||
// Find the submit button within the form
|
||||
var submitButton = form.find('button[type="submit"], input[type="submit"]');
|
||||
|
||||
const boredWaitingText = /*[[#{bored}]]*/ 'Bored Waiting?';
|
||||
const downloadCompleteText = /*[[#{downloadComplete}]]*/ 'Download Complete';
|
||||
window.downloadCompleteText = downloadCompleteText;
|
||||
// Create the 'show-game-btn' button
|
||||
var gameButton = $('<button type="button" class="btn btn-primary" id="show-game-btn" style="display:none;">' + boredWaitingText + '</button>');
|
||||
|
||||
// Insert the 'show-game-btn' just above the submit button
|
||||
submitButton.before(gameButton);
|
||||
|
||||
function loadGameScript(callback) {
|
||||
console.log('loadGameScript called');
|
||||
const script = document.createElement('script');
|
||||
script.src = 'js/game.js';
|
||||
script.onload = callback;
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
let gameScriptLoaded = false;
|
||||
const gameDialog = document.getElementById('game-container-wrapper');
|
||||
$('#show-game-btn').on('click', function () {
|
||||
console.log('Show game button clicked');
|
||||
if (!gameScriptLoaded) {
|
||||
console.log('Show game button load');
|
||||
loadGameScript(function () {
|
||||
console.log('Game script loaded');
|
||||
window.initializeGame();
|
||||
gameScriptLoaded = true;
|
||||
});
|
||||
} else {
|
||||
window.resetGame();
|
||||
}
|
||||
gameDialog.showModal();
|
||||
});
|
||||
gameDialog.addEventListener("click", e => {
|
||||
const dialogDimensions = gameDialog.getBoundingClientRect()
|
||||
if (
|
||||
e.clientX < dialogDimensions.left ||
|
||||
e.clientX > dialogDimensions.right ||
|
||||
e.clientY < dialogDimensions.top ||
|
||||
e.clientY > dialogDimensions.bottom
|
||||
) {
|
||||
gameDialog.close();
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<div id="game-container">
|
||||
<div id="lives">Lives: 3</div>
|
||||
<div id="score">Score: 0</div>
|
||||
<div id="high-score">High Score: 0</div>
|
||||
<div id="level">Level: 1</div>
|
||||
<img th:src="@{'/favicon.svg'}" class="player" id="player" alt="favicon">
|
||||
</div>
|
||||
<link rel="stylesheet" th:href="@{'/css/game.css'}">
|
||||
</dialog>
|
||||
</th:block>
|
||||
|
||||
<th:block th:fragment="fileSelector(name, multipleInputsForSingleRequest)"
|
||||
th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}, remoteCall=${remoteCall} ?: true, disableMultipleFiles=${disableMultipleFiles} ?: false, showUploads=${showUploads} ?: true, notRequired=${notRequired} ?: false">
|
||||
<script th:inline="javascript">
|
||||
(function () {
|
||||
window.stirlingPDF.pdfPasswordPrompt = /*[[#{error.pdfPassword}]]*/ '';
|
||||
window.stirlingPDF.multipleInputsForSingleRequest = /*[[${multipleInputsForSingleRequest}]]*/ false;
|
||||
window.stirlingPDF.disableMultipleFiles = /*[[${disableMultipleFiles}]]*/ false;
|
||||
window.stirlingPDF.remoteCall = /*[[${remoteCall}]]*/ true;
|
||||
window.stirlingPDF.sessionExpired = /*[[#{session.expired}]]*/ '';
|
||||
window.stirlingPDF.refreshPage = /*[[#{session.refreshPage}]]*/ 'Refresh Page';
|
||||
window.stirlingPDF.error = /*[[#{error}]]*/ "Error";
|
||||
window.stirlingPDF.uploadLimitReadable = /*[[${@uploadLimitService.getReadableUploadLimit()}]]*/ 'Unlimited';
|
||||
window.stirlingPDF.uploadLimit = /*[[${@uploadLimitService.getUploadLimit()}]]*/ 0;
|
||||
window.stirlingPDF.uploadLimitExceededSingular = /*[[#{uploadLimitExceededSingular}]]*/ 'is too large. Maximum allowed size is';
|
||||
window.stirlingPDF.uploadLimitExceededPlural = /*[[#{uploadLimitExceededPlural}]]*/ 'are too large. Maximum allowed size is';
|
||||
window.stirlingPDF.pdfCorruptedMessage = /*[[#{error.pdfInvalid}]]*/ 'The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the \'Repair PDF\' feature to fix the file before proceeding.';
|
||||
window.stirlingPDF.tryRepairMessage = /*[[#{error.tryRepair}]]*/ 'Try using the Repair PDF feature to fix corrupted files.';
|
||||
})();
|
||||
</script>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<script th:src="@{'/js/downloader.js'}"></script>
|
||||
<script>
|
||||
window.decrypt = {
|
||||
passwordPrompt: '[[#{decrypt.passwordPrompt}]]',
|
||||
cancelled: '[[#{decrypt.cancelled}]]',
|
||||
noPassword: '[[#{decrypt.noPassword}]]',
|
||||
invalidPassword: '[[#{decrypt.invalidPassword}]]',
|
||||
invalidPasswordHeader: '[[#{decrypt.invalidPasswordHeader}]]',
|
||||
unexpectedError: '[[#{decrypt.unexpectedError}]]',
|
||||
serverError: '[[#{decrypt.serverError}]]',
|
||||
success: '[[#{decrypt.success}]]',
|
||||
};
|
||||
window.fileInput = {
|
||||
dragAndDropPDF: '[[#{fileChooser.dragAndDropPDF}]]',
|
||||
dragAndDropImage: '[[#{fileChooser.dragAndDropImage}]]',
|
||||
addAttachments: '[[#{fileChooser.addAttachments}]]',
|
||||
extractPDF: '[[#{fileChooser.extractPDF}]]',
|
||||
loading: '[[#{loading}]]'
|
||||
};</script>
|
||||
<div class="custom-file-chooser mb-3"
|
||||
|
||||
th:attr="data-bs-unique-id=${name}, data-bs-element-id=${name+'-input'}, data-bs-element-container-id=${name+'-input-container'}, data-bs-show-uploads=${showUploads}, data-bs-files-selected=#{filesSelected}, data-bs-pdf-prompt=#{pdfPrompt}, data-bs-no-file-selected=#{noFileSelected}">
|
||||
<div class="mb-3 d-flex flex-column justify-content-center align-items-center flex-wrap input-container"
|
||||
th:name="${name}+'-input'" th:id="${name}+'-input-container'" th:data-text="#{fileChooser.hoveredDragAndDrop}">
|
||||
<label class="file-input-btn d-none">
|
||||
<input type="file" class="form-control"
|
||||
th:name="${name}"
|
||||
th:id="${name}+'-input'"
|
||||
th:accept="${accept == null ? '*/*': ((accept == '*/*') ? accept : (accept + ',.zip'))}"
|
||||
th:attr="multiple=${!disableMultipleFiles}"
|
||||
th:required="${notRequired} ? null : 'required'">
|
||||
Browse
|
||||
</label>
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<div class="text-center" id="fileInputText" style="word-wrap: break-word; hyphens: auto; line-height: 1.2;">
|
||||
<span th:text="#{fileChooser.click}"></span>
|
||||
<span th:text="#{fileChooser.or}" style="margin: 0 5px;"></span>
|
||||
<span th:text="#{fileChooser.dragAndDrop}" id="dragAndDrop"></span>
|
||||
</div>
|
||||
<hr th:if="${@GoogleDriveEnabled == true}" class="horizontal-divider" >
|
||||
</div>
|
||||
<div th:if="${@GoogleDriveEnabled == true}" th:id="${name}+'-google-drive-button'" class="google-drive-button" th:attr="data-name=${name}, data-multiple=${!disableMultipleFiles}, data-accept=${accept}" >
|
||||
<img th:src="@{'/images/google-drive.svg'}" alt="google drive">
|
||||
</div>
|
||||
</div>
|
||||
<div class="selected-files flex-wrap"></div>
|
||||
<div class="text-muted small mt-0 text-end w-100" th:if="${@uploadLimitService.getUploadLimit() != 0}">
|
||||
<span th:text="#{uploadLimit}">Maximum file size: </span>
|
||||
<span th:text="${@uploadLimitService.getReadableUploadLimit()}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progressBarContainer" style="display: none; position: relative;">
|
||||
<div class="progress" style="height: 1rem;">
|
||||
<div class="progressBar 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="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script th:src="@{'/js/fileInput.js'}" type="module"></script>
|
||||
|
||||
<div th:if="${@GoogleDriveEnabled == true}" >
|
||||
<script type="text/javascript" th:src="@{'/js/googleFilePicker.js'}"></script>
|
||||
<script async defer src="https://apis.google.com/js/api.js" onload="gapiLoaded()"></script>
|
||||
<script async defer src="https://accounts.google.com/gsi/client" onload="gisLoaded()"></script>
|
||||
|
||||
<script th:inline="javascript">
|
||||
window.stirlingPDF.GoogleDriveClientId = /*[[${@GoogleDriveConfig.getClientId()}]]*/ null;
|
||||
window.stirlingPDF.GoogleDriveApiKey = /*[[${@GoogleDriveConfig.getApiKey()}]]*/ null;
|
||||
window.stirlingPDF.GoogleDriveAppId = /*[[${@GoogleDriveConfig.getAppId()}]]*/ null;
|
||||
</script>
|
||||
</div>
|
||||
</th:block>
|
||||
@@ -0,0 +1,75 @@
|
||||
<th:block th:fragment="errorBanner">
|
||||
|
||||
<br th:if="${message}">
|
||||
<div id="errorContainer" th:if="${message}" class="alert alert-danger alert-dismissible alert-container fade show" role="alert">
|
||||
<div class="alert-header d-flex">
|
||||
<span class="alert-heading" th:text="'Error: ' + ${status} + ' ' + ${error}"></span>
|
||||
<button type="button" class="btn btn-danger" th:if="${trace}" onclick="toggletrace()" th:text="#{error.showStack}"></button>
|
||||
<button type="button" class="btn btn-secondary" th:if="${trace}" onclick="copytrace()" th:text="#{error.copyStack}"></button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" onclick="dismissError()">
|
||||
<span class="material-symbols-rounded">
|
||||
close
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<p th:text="${message} + ' for path: ' + ${path}"></p>
|
||||
<!-- Stack trace section -->
|
||||
<div id="trace" th:if="${trace}" style="max-height: 0; overflow: hidden;">
|
||||
<div>
|
||||
<pre id="traceContent" th:text="${trace}"></pre>
|
||||
</div>
|
||||
<!-- Buttons to submit a ticket on GitHub and join Discord server -->
|
||||
<a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" target="_blank" th:text="#{error.github}"></a>
|
||||
<a href="https://discord.gg/HYmhKj45pU" id="discord-button" target="_blank" th:text="#{joinDiscord}"></a>
|
||||
</div>
|
||||
</div>
|
||||
<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();
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</th:block>
|
||||
@@ -0,0 +1,53 @@
|
||||
<th:block th:fragment="errorBannerPerPage">
|
||||
<div id="errorContainer" class="alert alert-danger alert-dismissible fade show" role="alert" style="display: none;">
|
||||
<h4 class="alert-heading" th:text="#{error}"></h4>
|
||||
<p></p>
|
||||
<button type="button" class="btn btn-danger" onclick="toggletrace()" th:text="#{error.showStack}"></button>
|
||||
<button type="button" class="btn btn-secondary" onclick="copytrace()" th:text="#{error.copyStack}"></button>
|
||||
<button type="button" class="btn btn-info" onclick="showHelp()" th:text="#{help}"></button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" onclick="dismissError()">
|
||||
<span class="material-symbols-rounded">
|
||||
close
|
||||
</span>
|
||||
</button>
|
||||
<!-- Stack trace section -->
|
||||
<div id="trace" style="max-height: 0; overflow: hidden;">
|
||||
<div>
|
||||
<pre id="traceContent"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="helpModal" tabindex="-1" role="dialog" aria-labelledby="helpModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document" id="helpModalDialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="helpModalLabel" th:text="#{help}"></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" onclick="dismissError()">
|
||||
<span class="material-symbols-rounded">
|
||||
close
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container">
|
||||
<div id="support-section">
|
||||
<h1 class="display-2" th:text="#{oops}"></h1>
|
||||
<p class="lead" th:text="#{error.sorry}"></p>
|
||||
<br>
|
||||
<h2 th:text="#{error.needHelp}"></h2>
|
||||
<p th:text="#{error.contactTip}"></p>
|
||||
<div id="button-group">
|
||||
<a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" target="_blank" th:text="#{error.githubSubmit}"></a>
|
||||
<a href="https://discord.gg/HYmhKj45pU" id="discord-button" target="_blank" th:text="#{error.discordSubmit}"></a>
|
||||
</div>
|
||||
<a th:href="@{'/'}" class="home-button" th:text="#{goHomepage}"></a>
|
||||
<a data-bs-dismiss="modal" class="home-button" th:text="#{close}"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script th:src="@{'/js/errorBanner.js'}"></script>
|
||||
</th:block>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div th:fragment="featureGroupHeader" class="feature-group-header">
|
||||
<h6 class="menu-title" th:text="${groupTitle}"></h6>
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
<div th:fragment="featureGroupHeader" class="feature-group-header">
|
||||
<h3 class="menu-title" th:text="${groupTitle}"></h3>
|
||||
<span class="material-symbols-rounded header-expand-button">
|
||||
chevron_right
|
||||
</span>
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
<footer th:fragment="footer" id="footer" class="text-center">
|
||||
|
||||
<script type="module" th:src="@{'/js/thirdParty/cookieconsent-config.js'}"></script>
|
||||
<div class="footer-center">
|
||||
<!-- Links section -->
|
||||
<div class="d-flex justify-content-center">
|
||||
<ul class="list-unstyled footer-link-list">
|
||||
<li><a class="footer-link px-2" id="licenses" target="_blank" th:href="@{'/licenses'}" th:text="#{licenses.nav}">Licenses</a></li>
|
||||
<li><a class="footer-link px-2" id="releases" target="_blank" th:href="@{'/releases'}" th:text="#{releases.footer}">Releases</a></li>
|
||||
<li><a class="footer-link px-2" id="survey" target="_blank" href="https://stirlingpdf.info/s/cm28y3niq000o56dv7liv8wsu" th:text="#{survey.nav}">Survey</a></li>
|
||||
<li th:if="${@privacyPolicy != ''}"><a class="footer-link px-2" target="_blank" th:href="${@privacyPolicy}" th:text="#{legal.privacy}">privacyPolicy</a></li>
|
||||
<li th:if="${@termsAndConditions != ''}"><a class="footer-link px-2" target="_blank" th:href="${@termsAndConditions}" th:text="#{legal.terms}">termsAndConditions</a></li>
|
||||
<li th:if="${@accessibilityStatement != ''}"><a class="footer-link px-2" target="_blank" th:href="${@accessibilityStatement}" th:text="#{legal.accessibility}">accessibilityStatement</a></li>
|
||||
<li th:if="${@cookiePolicy != ''}"><a class="footer-link px-2" target="_blank" th:href="${@cookiePolicy}" th:text="#{legal.cookie}">cookiePolicy</a></li>
|
||||
<li th:if="${@impressum != ''}"><a class="footer-link px-2" target="_blank" th:href="${@impressum}" th:text="#{legal.impressum}">impressum</a></li>
|
||||
<li><a class="footer-link px-2" id="cookieBanner" target="_blank" th:text="#{legal.showCookieBanner}" onClick="CookieConsent.show(true)">Cookie Preferences</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- powered by section -->
|
||||
<div class="footer-powered-by">
|
||||
<span th:text="#{poweredBy}"></span>
|
||||
<a href="https://stirlingpdf.com" class="stirling-link">Stirling PDF</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,7 @@
|
||||
<th:block th:fragment="languageEntry(code, name)">
|
||||
<div th:if="${code} == 'en_GB' or ${#lists.isEmpty(@languages) or #lists.contains(@languages, code)}" class="lang-dropdown-item-wrapper">
|
||||
<a
|
||||
class="dropdown-item lang_dropdown-item" href="" th:data-bs-language-code="@{${code}}" th:text="${name}">
|
||||
</a>
|
||||
</div>
|
||||
</th:block>
|
||||
@@ -0,0 +1,44 @@
|
||||
<th:block th:fragment="langs">
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('bg_BG', 'Български')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('ar_AR', 'العربية')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('ca_CA', 'Català')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('zh_CN', '简体中文')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('zh_TW', '繁體中文')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('bo_CN', 'བོད་ཡིག')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('az_AZ', 'Azərbaycan Dili')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('da_DK', 'Dansk')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('de_DE', 'Deutsch')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('en_GB', 'English (GB)')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('en_US', 'English (US)')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('eu_ES', 'Euskara')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('es_ES', 'Español')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('fr_FR', 'Français')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('id_ID', 'Bahasa Indonesia')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('ga_IE', 'Gaeilge')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('it_IT', 'Italiano')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('nl_NL', 'Nederlands')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('fa_IR', 'پارسی')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('pl_PL', 'Polski')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('pt_BR', 'Português (BR)')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('pt_PT', 'Português (PT)')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('ro_RO', 'Română')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('sk_SK', 'Slovenčina')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('sl_SI', 'Slovenščina')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('sv_SE', 'Svenska')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('tr_TR', 'Türkçe')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('ru_RU', 'Русский')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('ko_KR', '한국어')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('ja_JP', '日本語')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('el_GR', 'Ελληνικά')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('hu_HU', 'Magyar')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('hi_IN', 'हिन्दी')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('sr_LATN_RS', 'Srpski')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('uk_UA', 'Українська')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('cs_CZ', 'Česky')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('hr_HR', 'Hrvatski')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('no_NB', 'Norsk')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('th_TH', 'ไทย')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('vi_VN', 'Tiếng Việt')}"></div>
|
||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('ml_IN', 'മലയാളം')}"></div>
|
||||
|
||||
</th:block>
|
||||
@@ -0,0 +1,65 @@
|
||||
<div th:fragment="multi-toolAdvert" class="mx-auto">
|
||||
<div id="multi-toolAdvert" class="multi-toolAdvert">
|
||||
<div>
|
||||
<span th:utext="#{multiTool-advert.message(|multi-tool|)}"></span>
|
||||
<button id="closeMultiToolAdvert" style="position: absolute;
|
||||
inset-inline-end: 12px;
|
||||
inset-block-start: 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;" aria-label="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.multi-toolAdvert {
|
||||
margin-bottom: 10px;
|
||||
margin-inline-start: 50%;
|
||||
max-width: 52rem;
|
||||
z-index: 0;
|
||||
background-color: var(--md-sys-color-surface-5);
|
||||
border-radius: 2rem;
|
||||
padding-block: 10px;
|
||||
padding-inline: 20px 27px;
|
||||
font-size: 0.9rem;
|
||||
display: none;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.multi-toolAdvert a {
|
||||
color: #007bff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.multi-toolAdvert a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const advert = document.getElementById('multi-toolAdvert');
|
||||
const closeBtn = document.getElementById('closeMultiToolAdvert');
|
||||
|
||||
const cacheKey = `closeMultiToolAdvert_${window.location.pathname}`;
|
||||
const isRTL = document.documentElement.dir === 'rtl';
|
||||
|
||||
if (localStorage.getItem(cacheKey) !== 'true') {
|
||||
advert.style.display = 'flex';
|
||||
}
|
||||
|
||||
closeBtn.addEventListener('click', () => {
|
||||
advert.style.display = 'none';
|
||||
localStorage.setItem(cacheKey, 'true');
|
||||
});
|
||||
|
||||
|
||||
if (isRTL) {
|
||||
advert.style.transform = 'translateX(50%)'; // Flip direction for RTL
|
||||
} else {
|
||||
advert.style.transform = 'translateX(-50%)';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
@@ -0,0 +1,291 @@
|
||||
<th:block th:fragment="navElements">
|
||||
<div id="groupOrganize" class="feature-group">
|
||||
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.organize})}">
|
||||
</div>
|
||||
<div class="nav-group-container">
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('multi-tool', 'construction', 'home.multiTool.title', 'home.multiTool.desc', 'multiTool.tags', 'organize')}">
|
||||
</div>
|
||||
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('merge-pdfs', 'add_to_photos', 'home.merge.title', 'home.merge.desc', 'merge.tags', 'organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('split-pdfs', 'cut', 'home.split.title', 'home.split.desc', 'split.tags', 'organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('rotate-pdf', 'rotate_right', 'home.rotate.title', 'home.rotate.desc', 'rotate.tags', 'organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('crop', 'crop', 'home.crop.title', 'home.crop.desc', 'crop.tags', 'organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-organizer', 'format_list_bulleted', 'home.pdfOrganiser.title', 'home.pdfOrganiser.desc', 'pdfOrganiser.tags', 'organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('remove-pages', 'delete', 'home.removePages.title', 'home.removePages.desc', 'removePages.tags', 'organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('multi-page-layout', 'dashboard', 'home.pageLayout.title', 'home.pageLayout.desc', 'pageLayout.tags', 'organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('scale-pages', 'fullscreen', 'home.scalePages.title', 'home.scalePages.desc', 'scalePages.tags', 'organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('extract-page', 'upload', 'home.extractPage.title', 'home.extractPage.desc', 'extractPage.tags', 'organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-single-page', 'looks_one', 'home.PdfToSinglePage.title', 'home.PdfToSinglePage.desc', 'PdfToSinglePage.tags', 'organize')}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="groupConvertTo" class="feature-group">
|
||||
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.convertTo})}">
|
||||
</div>
|
||||
<div class="nav-group-container">
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('img-to-pdf', 'picture_as_pdf', 'home.imageToPdf.title', 'home.imageToPdf.desc', 'imageToPdf.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('url-to-pdf', 'link', 'home.URLToPDF.title', 'home.URLToPDF.desc', 'URLToPDF.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('html-to-pdf', 'html', 'home.HTMLToPDF.title', 'home.HTMLToPDF.desc', 'HTMLToPDF.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('markdown-to-pdf', 'markdown', 'home.MarkdownToPDF.title', 'home.MarkdownToPDF.desc', 'MarkdownToPDF.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('eml-to-pdf', 'email', 'home.EMLToPDF.title', 'home.EMLToPDF.desc', 'EMLToPDF.tags', 'convertto')}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="groupConvertFrom" class="feature-group">
|
||||
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.convertFrom})}">
|
||||
</div>
|
||||
<div class="nav-group-container">
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-img', 'photo_library', 'home.pdfToImage.title', 'home.pdfToImage.desc', 'pdfToImage.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-pdfa', 'picture_as_pdf', 'home.pdfToPDFA.title', 'home.pdfToPDFA.desc', 'pdfToPDFA.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-word', 'description', 'home.PDFToWord.title', 'home.PDFToWord.desc', 'PDFToWord.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-presentation', 'slideshow', 'home.PDFToPresentation.title', 'home.PDFToPresentation.desc', 'PDFToPresentation.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-text', 'text_fields', 'home.PDFToText.title', 'home.PDFToText.desc', 'PDFToText.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-html', 'html', 'home.PDFToHTML.title', 'home.PDFToHTML.desc', 'PDFToHTML.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-xml', 'code', 'home.PDFToXML.title', 'home.PDFToXML.desc', 'PDFToXML.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-csv', 'csv', 'home.tableExtraxt.title', 'home.tableExtraxt.desc', 'tableExtraxt.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-markdown', 'markdown_copy', 'home.PDFToMarkdown.title', 'home.PDFToMarkdown.desc', 'PDFToMarkdown.tags', 'convert')}">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="convertGroup" class="feature-group">
|
||||
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.convertTo})}">
|
||||
</div>
|
||||
<div class="nav-group-container">
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('img-to-pdf', 'picture_as_pdf', 'home.imageToPdf.title', 'home.imageToPdf.desc', 'imageToPdf.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('url-to-pdf', 'link', 'home.URLToPDF.title', 'home.URLToPDF.desc', 'URLToPDF.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('html-to-pdf', 'html', 'home.HTMLToPDF.title', 'home.HTMLToPDF.desc', 'HTMLToPDF.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('markdown-to-pdf', 'markdown', 'home.MarkdownToPDF.title', 'home.MarkdownToPDF.desc', 'MarkdownToPDF.tags', 'convertto')}">
|
||||
</div>
|
||||
</div>
|
||||
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.convertFrom})}">
|
||||
</div>
|
||||
<div class="nav-group-container">
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-img', 'photo_library', 'home.pdfToImage.title', 'home.pdfToImage.desc', 'pdfToImage.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-pdfa', 'picture_as_pdf', 'home.pdfToPDFA.title', 'home.pdfToPDFA.desc', 'pdfToPDFA.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-word', 'description', 'home.PDFToWord.title', 'home.PDFToWord.desc', 'PDFToWord.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-presentation', 'slideshow', 'home.PDFToPresentation.title', 'home.PDFToPresentation.desc', 'PDFToPresentation.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-text', 'text_fields', 'home.PDFToText.title', 'home.PDFToText.desc', 'PDFToText.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-html', 'html', 'home.PDFToHTML.title', 'home.PDFToHTML.desc', 'PDFToHTML.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-xml', 'code', 'home.PDFToXML.title', 'home.PDFToXML.desc', 'PDFToXML.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-csv', 'csv', 'home.tableExtraxt.title', 'home.tableExtraxt.desc', 'tableExtraxt.tags', 'convert')}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="groupSecurity" class="feature-group">
|
||||
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.security})}">
|
||||
</div>
|
||||
<div class="nav-group-container">
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('add-password', 'lock', 'home.addPassword.title', 'home.addPassword.desc', 'addPassword.tags', 'security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('remove-password', 'lock_open_right', 'home.removePassword.title', 'home.removePassword.desc', 'removePassword.tags', 'security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('change-permissions', 'encrypted', 'home.permissions.title', 'home.permissions.desc', 'permissions.tags', 'security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('sign', 'signature', 'home.sign.title', 'home.sign.desc', 'sign.tags', 'security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('cert-sign', 'workspace_premium', 'home.certSign.title', 'home.certSign.desc', 'certSign.tags', 'security')}">
|
||||
</div>
|
||||
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('validate-signature', 'verified', 'home.validateSignature.title', 'home.validateSignature.desc', 'validateSignature.tags', 'security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('remove-cert-sign', 'remove_moderator', 'home.removeCertSign.title', 'home.removeCertSign.desc', 'removeCertSign.tags', 'security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('sanitize-pdf', 'sanitizer', 'home.sanitizePdf.title', 'home.sanitizePdf.desc', 'sanitizePdf.tags', 'security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntryCustom :: navbarEntry('auto-redact', '/images/redact-auto.svg#icon-redact-auto', 'home.autoRedact.title', 'home.autoRedact.desc', 'autoRedact.tags', 'security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntryCustom :: navbarEntry('redact', '/images/redact-manual.svg#icon-redact-manual', 'home.redact.title', 'home.redact.desc', 'redact.tags', 'security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('stamp', 'approval', 'home.AddStampRequest.title', 'home.AddStampRequest.desc', 'AddStampRequest.tags', 'security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('add-watermark', 'water_drop', 'home.watermark.title', 'home.watermark.desc', 'watermark.tags', 'security')}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="groupView" class="feature-group">
|
||||
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.edit})}">
|
||||
</div>
|
||||
<div class="nav-group-container">
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('view-pdf', 'menu_book', 'home.viewPdf.title', 'home.viewPdf.desc', 'viewPdf.tags', 'other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('add-page-numbers', '123', 'home.add-page-numbers.title', 'home.add-page-numbers.desc', 'add-page-numbers.tags', 'other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('add-image', 'text_fields', 'home.addImage.title', 'home.addImage.desc', 'addImage.tags', 'other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('change-metadata', 'assignment', 'home.changeMetadata.title', 'home.changeMetadata.desc', 'changeMetadata.tags', 'other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('ocr-pdf', 'quick_reference_all', 'home.ocr.title', 'home.ocr.desc', 'ocr.tags', 'other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('extract-images', 'photo_library', 'home.extractImages.title', 'home.extractImages.desc', 'extractImages.tags', 'other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('flatten', 'layers_clear', 'home.flatten.title', 'home.flatten.desc', 'flatten.tags', 'other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('remove-blanks', 'scan_delete', 'home.removeBlanks.title', 'home.removeBlanks.desc', 'removeBlanks.tags', 'other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('remove-annotations', 'thread_unread', 'home.removeAnnotations.title', 'home.removeAnnotations.desc', 'removeAnnotations.tags', 'other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('compare', 'compare', 'home.compare.title', 'home.compare.desc', 'compare.tags', 'other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('get-info-on-pdf', 'info', 'home.getPdfInfo.title', 'home.getPdfInfo.desc', 'getPdfInfo.tags', 'other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('remove-image-pdf', 'remove_selection', 'home.removeImagePdf.title', 'home.removeImagePdf.desc', 'removeImagePdf.tags', 'other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('replace-and-invert-color-pdf', 'format_color_fill', 'home.replaceColorPdf.title', 'home.replaceColorPdf.desc', 'replaceColorPdf.tags', 'other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('unlock-pdf-forms', 'preview_off', 'home.unlockPDFForms.title', 'home.unlockPDFForms.desc', 'unlockPDFForms.tags', 'other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('add-attachments', 'attachment', 'home.attachments.title', 'home.attachments.desc', 'attachments.tags', 'other')}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="groupAdvanced" class="feature-group">
|
||||
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.advance})}">
|
||||
</div>
|
||||
<div class="nav-group-container">
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pipeline', 'family_history', 'home.pipeline.title', 'home.pipeline.desc', 'pipeline.tags', 'advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('adjust-contrast', 'palette', 'home.adjust-contrast.title', 'home.adjust-contrast.desc', 'adjust-contrast.tags', 'advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('compress-pdf', 'zoom_in_map', 'home.compressPdfs.title', 'home.compressPdfs.desc', 'compressPdfs.tags', 'advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('extract-image-scans', 'scanner', 'home.ScannerImageSplit.title', 'home.ScannerImageSplit.desc', 'ScannerImageSplit.tags', 'advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('repair', 'build', 'home.repair.title', 'home.repair.desc', 'repair.tags', 'advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntryCustom :: navbarEntry('auto-rename', '/images/rename.svg#icon-rename', 'home.auto-rename.title', 'home.auto-rename.desc', 'auto-rename.tags', 'advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntryCustom :: navbarEntry('auto-split-pdf', '/images/split-auto.svg#icon-split-auto', 'home.autoSplitPDF.title', 'home.autoSplitPDF.desc', 'autoSplitPDF.tags', 'advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('show-javascript', 'javascript', 'home.showJS.title', 'home.showJS.desc', 'showJS.tags', 'advance')}">
|
||||
</div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry('fake-scan', 'scanner', 'fakeScan.title', 'fakeScan.description', 'fakeScan.tags', 'advance')}"></div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntryCustom :: navbarEntry('split-by-size-or-count', '/images/split-size.svg#icon-split-size', 'home.autoSizeSplitPDF.title', 'home.autoSizeSplitPDF.desc', 'autoSizeSplitPDF.tags', 'advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('overlay-pdf', 'layers', 'home.overlay-pdfs.title', 'home.overlay-pdfs.desc', 'overlay-pdfs.tags', 'advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('split-pdf-by-sections', 'grid_on', 'home.split-by-sections.title', 'home.split-by-sections.desc', 'split-by-sections.tags', 'advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('edit-table-of-contents', 'bookmark_add', 'home.editTableOfContents.title', 'home.editTableOfContents.desc', 'editTableOfContents.tags', 'advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntryCustom :: navbarEntry('split-pdf-by-chapters', '/images/split-chapters.svg#icon-split-chapters', 'home.splitPdfByChapters.title', 'home.splitPdfByChapters.desc', 'splitPdfByChapters.tags', 'advance')}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
@@ -0,0 +1,283 @@
|
||||
<div th:fragment="navbar" class="mx-auto" style="position: sticky; top:0; z-index:10000; width:100%">
|
||||
<script th:src="@{'/js/languageSelection.js'}"></script>
|
||||
<script th:src="@{'/js/navbar.js'}"></script>
|
||||
<script th:src="@{'/js/additionalLanguageCode.js'}"></script>
|
||||
<script th:inline="javascript">
|
||||
// Initializing the scripts
|
||||
initLanguageSettings();
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
toolsManager();
|
||||
});
|
||||
</script>
|
||||
<script th:inline="javascript">
|
||||
const currentVersion = /*[[${@appVersion}]]*/ '';
|
||||
const noFavourites = /*[[#{noFavourites}]]*/ '';
|
||||
console.log(noFavourites);
|
||||
const updateAvailable = /*[[#{settings.updateAvailable}]]*/ '';
|
||||
</script>
|
||||
<script th:src="@{'/js/homecard.js'}"></script>
|
||||
<script th:src="@{'/js/githubVersion.js'}"></script>
|
||||
<form th:action="@{'/dummyFormToPopulateCSRF'}" method="post" enctype="multipart/form-data"></form>
|
||||
<nav class="navbar navbar-expand-xl" style="
|
||||
background: var(--md-nav-background);
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-color: var(--md-nav-color-on-separator)">
|
||||
<div class="container " style="max-width: 100%;">
|
||||
<a class="navbar-brand" th:href="${@contextPath}" style="display: flex;">
|
||||
<img class="main-icon" th:src="@{'/favicon.svg'}" alt="icon">
|
||||
<span class="icon-text" th:text="${@navBarText}"></span>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="material-symbols-rounded">
|
||||
menu
|
||||
</span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto flex-nowrap">
|
||||
|
||||
<!-- All Tools -->
|
||||
<li id="navItemToHide" class="nav-item dropdown dropdown-mega position-static">
|
||||
<a class="nav-link" id="navbarDropdown-1" href="#" role="button" data-bs-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<span class="material-symbols-rounded">
|
||||
apps
|
||||
</span>
|
||||
<span class="icon-text" th:data-text="#{navbar.allTools}" th:text="#{navbar.allTools}"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-tp" aria-labelledby="navbarDropdown-1">
|
||||
<div class="dropdown-menu-wrapper" style="justify-content: center; display:flex">
|
||||
<div class="feature-rows">
|
||||
|
||||
<th:block th:insert="~{fragments/navElements.html :: navElements}"></th:block>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="nav-item" th:if="${@endpointConfiguration.isEndpointEnabled('multi-tool')}">
|
||||
<a class="nav-link" href="#" th:href="@{'/multi-tool'}"
|
||||
th:classappend="${currentPage}=='multi-tool' ? 'active' : ''" th:title="#{home.multiTool.desc}">
|
||||
<span class="material-symbols-rounded">
|
||||
construction
|
||||
</span>
|
||||
<span class="icon-text" th:data-text="#{navbar.multiTool}" th:text="#{navbar.multiTool}"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" th:if="${@endpointConfiguration.isEndpointEnabled('pipeline')}">
|
||||
<a class="nav-link" href="#" th:href="@{'/pipeline'}"
|
||||
th:classappend="${currentPage}=='pipeline' ? 'active' : ''" th:title="#{home.pipeline.desc}">
|
||||
<span class="material-symbols-rounded">
|
||||
family_history
|
||||
</span>
|
||||
<span class="icon-text" th:data-text="#{home.pipeline.title}" th:text="#{home.pipeline.title}"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" th:if="${@endpointConfiguration.isEndpointEnabled('compress-pdf')}">
|
||||
<a class="nav-link" href="#" title="#{home.compressPdfs.title}" th:href="@{'/compress-pdf'}"
|
||||
th:classappend="${currentPage}=='compress-pdf' ? 'active' : ''" th:title="#{home.compressPdfs.desc}">
|
||||
<span class="material-symbols-rounded">
|
||||
zoom_in_map
|
||||
</span>
|
||||
<span class="icon-text" th:data-text="#{home.compressPdfs.title}"
|
||||
th:text="#{home.compressPdfs.title}"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" th:href="@{'/view-pdf'}"
|
||||
th:classappend="${currentPage}=='view-pdf' ? 'active' : ''" th:title="#{home.viewPdf.desc}">
|
||||
<span class="material-symbols-rounded">
|
||||
menu_book
|
||||
</span>
|
||||
<span class="icon-text" th:data-text="#{home.viewPdf.title}" th:text="#{home.viewPdf.title}"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- <li class="nav-item">
|
||||
<a class="nav-link" href="#" th:href="@{'/merge-pdfs'}"
|
||||
th:classappend="${currentPage}=='merge-pdfs' ? 'active' : ''" th:title="#{home.merge.desc}">
|
||||
<span class="material-symbols-rounded">
|
||||
add_to_photos
|
||||
</span>
|
||||
<span class="icon-text" th:data-text="#{home.merge.title}" th:text="#{home.merge.title}"></span>
|
||||
</a>
|
||||
</li> -->
|
||||
|
||||
</ul>
|
||||
<ul class="navbar-nav flex-nowrap">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" id="navbarDropdown-5" href="#" role="button" data-bs-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false" th:title="#{navbar.favorite}">
|
||||
<span class="material-symbols-rounded">
|
||||
star
|
||||
</span>
|
||||
<span class="icon-text icon-hide" th:data-text="#{navbar.favorite}" th:text="#{navbar.favorite}"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-tp dropdown-mw-28" aria-labelledby="navbarDropdown-5">
|
||||
<div class="dropdown-menu-wrapper px-xl-2 px-2" id="favoritesDropdown">
|
||||
<!-- Dropdown items will be added here by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="dark-mode-toggle" href="#" th:title="#{navbar.darkmode}">
|
||||
<span class="material-symbols-rounded" id="dark-mode-icon">
|
||||
dark_mode
|
||||
</span>
|
||||
<span class="icon-text icon-hide" id="dark-mode-text" th:data-text="#{navbar.darkmode}"
|
||||
th:text="#{navbar.darkmode}"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" href="#" id="languageDropdown" role="button" data-bs-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false" th:title="#{navbar.language}">
|
||||
<span class="material-symbols-rounded">
|
||||
language
|
||||
</span>
|
||||
<span class="icon-text icon-hide" th:data-text="#{navbar.language}" th:text="#{navbar.language}"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-tp" aria-labelledby="languageDropdown">
|
||||
<div class="dropdown-menu-wrapper px-xl-2 px-2">
|
||||
<div id="languageSelection" class="scrollable-y lang_dropdown-mw scalable-languages-container">
|
||||
<th:block th:insert="~{fragments/languages :: langs}"></th:block>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" href="#" id="searchDropdown" role="button" data-bs-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false" th:title="#{navbar.search}">
|
||||
<span class="material-symbols-rounded">
|
||||
search
|
||||
</span>
|
||||
<span class="icon-text icon-hide">Search</span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-tp" aria-labelledby="searchDropdown">
|
||||
<div class="dropdown-menu-wrapper px-xl-2 px-2">
|
||||
<form th:action="@{''}" class="d-flex p-2 search-form" id="searchForm">
|
||||
<input class="form-control search-input" type="search" th:placeholder="#{navbar.search}"
|
||||
aria-label="Search" id="navbarSearchInput">
|
||||
</form>
|
||||
<!-- Search Results -->
|
||||
<div id="searchResults" class="search-results scrollable-y dropdown-mw-20"></div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" th:if="${!@runningProOrHigher}">
|
||||
<a href="https://stirlingpdf.com/pricing" class="nav-link go-pro-link" target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<span class="go-pro-badge" th:text="#{enterpriseEdition.button}"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<!-- Settings Button -->
|
||||
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#settingsModal"
|
||||
th:title="#{navbar.settings}">
|
||||
<span class="material-symbols-rounded">
|
||||
settings
|
||||
</span>
|
||||
<span class="icon-text icon-hide" th:data-text="#{navbar.settings}" th:text="#{navbar.settings}"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<script th:src="@{'/js/favourites.js'}"></script>
|
||||
<script th:src="@{'/js/search.js'}"></script>
|
||||
</nav>
|
||||
|
||||
<th:block th:insert="~{fragments/errorBannerPerPage.html :: errorBannerPerPage}"></th:block>
|
||||
<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">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="settingsModalLabel" th:text="#{settings.title}"></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="material-symbols-rounded">
|
||||
close
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="mb-0" th:utext="#{settings.appVersion} + ' ' + ${@appVersion}"></p>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 mt-3">
|
||||
<div class="footer-center" style="flex-direction: row;">
|
||||
<a href="https://github.com/Stirling-Tools/Stirling-PDF" class="mx-1" role="button" target="_blank"
|
||||
th:title="#{visitGithub}">
|
||||
<img th:src="@{'/images/github.svg'}" alt="github">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/stirlingtools/stirling-pdf" class="mx-1" role="button" target="_blank"
|
||||
th:title="#{seeDockerHub}">
|
||||
<img th:src="@{'/images/docker.svg'}" alt="docker">
|
||||
</a>
|
||||
<a href="https://discord.gg/Cn8pWhQRxZ" class="mx-1" role="button" target="_blank"
|
||||
th:title="#{joinDiscord}">
|
||||
<img th:src="@{'/images/discord.svg'}" alt="discord">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a th:href="@{'/swagger-ui/index.html'}" class="btn btn-sm btn-outline-primary mx-1" role="button"
|
||||
target="_blank">API</a>
|
||||
<a href="https://github.com/Stirling-Tools/Stirling-PDF/releases"
|
||||
class="btn btn-sm btn-outline-primary mx-1" id="update-btn" th:utext="#{settings.update}" role="button"
|
||||
target="_blank"></a>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="downloadOption" th:utext="#{settings.downloadOption.title}"></label>
|
||||
<select class="form-control" id="downloadOption">
|
||||
<option value="sameWindow" th:utext="#{settings.downloadOption.1}"></option>
|
||||
<option value="newWindow" th:utext="#{settings.downloadOption.2}"></option>
|
||||
<option value="downloadFile" th:utext="#{settings.downloadOption.3}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="zipThreshold" th:utext="#{settings.zipThreshold}"></label><br />
|
||||
<input type="range" class="form-range" min="1" max="9" step="1" id="zipThreshold" value="4">
|
||||
<span id="zipThresholdValue" class="ms-2"></span>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" id="boredWaiting" th:title="#{settings.bored.help}">
|
||||
<label for="boredWaiting" th:text="#{bored}"></label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" id="cacheInputs" th:title="#{settings.cacheInputs.help}">
|
||||
<label for="cacheInputs" th:text="#{settings.cacheInputs.name}"></label>
|
||||
</div>
|
||||
|
||||
<a th:if="${@loginEnabled and @activeSecurity}" th:href="@{'/account'}" class="btn btn-sm btn-outline-primary"
|
||||
role="button" th:text="#{settings.accountSettings}" target="_blank">Account Settings</a>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a th:if="${@loginEnabled and @activeSecurity}" class="btn btn-danger" role="button"
|
||||
th:text="#{settings.signOut}" th:href="@{'/logout'}">Sign Out</a>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script th:src="@{'/js/settings.js'}"></script>
|
||||
<script th:inline="javascript">
|
||||
window.onload = function () {
|
||||
updateFavoritesDropdown();
|
||||
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const navbarLink = document.querySelector(".navbar-brand");
|
||||
const contentPath = /*[[${@contextPath}]]*/ '';
|
||||
if (localStorage.getItem("defaultView") === "home-legacy") {
|
||||
navbarLink.setAttribute("href", contentPath + "home-legacy");
|
||||
} else {
|
||||
navbarLink.setAttribute("href", contentPath);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
<th:block th:fragment="navbarEntry(endpoint, toolIcon, titleKey, descKey, tagKey, toolGroup)"
|
||||
th:if="${@endpointConfiguration.isEndpointEnabled(endpoint)}">
|
||||
<a th:id="@{${endpoint}}" class="dropdown-item" style="position:relative" th:href="@{${endpoint}}"
|
||||
th:data-bs-link="@{${endpoint}}"
|
||||
th:classappend="${endpoint.equals(currentPage)} ? ${toolGroup} + ' active' : '' + ${toolGroup}"
|
||||
th:data-bs-tags="#{${tagKey}}" th:data-bs-title='#{${titleKey}}'>
|
||||
<div style="min-height:2.7rem; align-items: center;display: flex;" th:title="#{${descKey}}" class="icon" alt="icon"
|
||||
th:class="@{${toolGroup}}">
|
||||
<span class="material-symbols-rounded nav-icon" th:text="@{${toolIcon}}" style=" align-items:center; display: flex; justify-content: center; height:2.7rem; width:2.7rem"></span>
|
||||
<span class="icon-text" th:text="#{${titleKey}}"></span>
|
||||
</div>
|
||||
<span class="material-symbols-rounded favorite-icon" style="display: none;" th:data-endpoint="@{${endpoint}}"
|
||||
th:onclick="'addToFavorites(\'' + @{${endpoint}} + '\')'">
|
||||
add
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</th:block>
|
||||
@@ -0,0 +1,21 @@
|
||||
<th:block th:fragment="navbarEntry(endpoint, toolIcon, titleKey, descKey, tagKey, toolGroup)"
|
||||
th:if="${@endpointConfiguration.isEndpointEnabled(endpoint)}">
|
||||
<a th:id="@{${endpoint}}" class="dropdown-item" style="position:relative" th:href="@{${endpoint}}"
|
||||
th:data-bs-link="@{${endpoint}}"
|
||||
th:classappend="${endpoint.equals(currentPage)} ? ${toolGroup} + ' active' : '' + ${toolGroup}"
|
||||
th:data-bs-tags="#{${tagKey}}" th:data-bs-title='#{${titleKey}}'>
|
||||
<div style="min-height:2.5rem;align-items: center;display: flex" th:title="#{${descKey}}" class="icon" alt="icon"
|
||||
th:class="@{${toolGroup}}">
|
||||
<svg class="nav-icon"
|
||||
style="height: 2.7rem; width:2.7rem; align-items:center; display: flex; justify-content: center;">
|
||||
<use th:xlink:href="@{${toolIcon}}"></use>
|
||||
</svg>
|
||||
<span class="icon-text" th:text="#{${titleKey}}"></span>
|
||||
</div>
|
||||
<span class="material-symbols-rounded favorite-icon" style="display: none;" th:data-endpoint="@{${endpoint}}"
|
||||
th:onclick="'addToFavorites(\'' + @{${endpoint}} + '\')'">
|
||||
star
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</th:block>
|
||||
@@ -0,0 +1,531 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title='')}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<!-- Jumbotron -->
|
||||
<div class="p-5 rounded d-none d-md-block" id="jumbotron">
|
||||
<div class="container">
|
||||
<h1 class="display-4 fw-normal" th:text="${@appName}"></h1>
|
||||
<p class="lead fs-4"
|
||||
th:text="${@homeText != 'null' and @homeText != null and @homeText != ''} ? ${@homeText} : #{home.desc}">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<br class="d-md-none">
|
||||
<!-- Features -->
|
||||
<script th:src="@{'/js/homecard-legacy.js'}"></script>
|
||||
<div class=" container">
|
||||
<br>
|
||||
<span class="material-symbols-rounded search-icon">
|
||||
search
|
||||
</span>
|
||||
<input type="text" id="searchBar" onkeyup="filterCardsLegacy()" th:placeholder="#{home.searchBar}" autofocus>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<a href="home" onclick="setAsDefault('home')"
|
||||
style="text-decoration: none; color: inherit; cursor: pointer; display: flex; align-items: center;">
|
||||
<span th:text="#{home.newHomePage}">
|
||||
</span>
|
||||
<span class="material-symbols-rounded toggle-favourites" style="font-size: 2rem; margin-left: 0.2rem;">
|
||||
home
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div id="filtersContainer">
|
||||
<span class="material-symbols-rounded filter-button" onclick="toggleFavoritesOnly()">
|
||||
star
|
||||
</span>
|
||||
<span class="material-symbols-rounded filter-button" onclick="expandCollapseAll(true)">
|
||||
expand_all
|
||||
</span>
|
||||
<span class="material-symbols-rounded filter-button" onclick="expandCollapseAll(false)">
|
||||
collapse_all
|
||||
</span>
|
||||
<span class="material-symbols-rounded filter-button hidden" onclick="switchViewMode()">
|
||||
dashboard
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="features-container">
|
||||
|
||||
<div th:if="${@shouldShow}" class="feature-card favorite update-notice visually-hidden" id="update-link-legacy">
|
||||
<a href="https://github.com/Stirling-Tools/Stirling-PDF/releases" target="_blank" rel="noopener">
|
||||
<div class="d-flex align-items-center">
|
||||
<div id="tool-icon" class="advance" alt="icon">
|
||||
<span class="material-symbols-rounded nav-icon">update</span>
|
||||
</div>
|
||||
<div id="tool-text">
|
||||
<h5 class="card-title" th:text="#{settings.update}"></h5>
|
||||
<p class="card-text" id="app-update"></p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="groupFavorites" class="feature-group-legacy">
|
||||
<div
|
||||
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.favorite})}">
|
||||
</div>
|
||||
<div class="feature-group-container">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="popularTools" class="feature-group-legacy">
|
||||
<div
|
||||
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.popular})}">
|
||||
</div>
|
||||
<div class="feature-group-container">
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='view-pdf', cardTitle=#{home.viewPdf.title}, cardText=#{home.viewPdf.desc}, cardLink='view-pdf', toolIcon='menu_book', tags=#{viewPdf.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', toolIcon='construction', tags=#{multiTool.tags}, toolGroup='organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', toolIcon='family_history', tags=#{pipeline.tags}, toolGroup='advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', toolIcon='zoom_in_map', tags=#{compressPdfs.tags}, toolGroup='advance')}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div id="groupOrganize" class="feature-group-legacy">
|
||||
<div
|
||||
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.organize})}">
|
||||
</div>
|
||||
<div class="feature-group-container">
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', toolIcon='construction', tags=#{multiTool.tags}, toolGroup='organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', toolIcon='add_to_photos', tags=#{merge.tags}, toolGroup='organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='split-pdfs', cardTitle=#{home.split.title}, cardText=#{home.split.desc}, cardLink='split-pdfs', toolIcon='cut', tags=#{split.tags}, toolGroup='organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='rotate-pdf', cardTitle=#{home.rotate.title}, cardText=#{home.rotate.desc}, cardLink='rotate-pdf', toolIcon='rotate_right', tags=#{rotate.tags}, toolGroup='organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='crop', cardTitle=#{home.crop.title}, cardText=#{home.crop.desc}, cardLink='crop', toolIcon='crop', tags=#{crop.tags}, toolGroup='organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='pdf-organizer', cardTitle=#{home.pdfOrganiser.title}, cardText=#{home.pdfOrganiser.desc}, cardLink='pdf-organizer', toolIcon='format_list_bulleted', tags=#{pdfOrganiser.tags}, toolGroup='organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='remove-pages', cardTitle=#{home.removePages.title}, cardText=#{home.removePages.desc}, cardLink='remove-pages', toolIcon='delete', tags=#{removePages.tags}, toolGroup='organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='multi-page-layout', cardTitle=#{home.pageLayout.title}, cardText=#{home.pageLayout.desc}, cardLink='multi-page-layout', toolIcon='dashboard', tags=#{pageLayout.tags}, toolGroup='organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='scale-pages', cardTitle=#{home.scalePages.title}, cardText=#{home.scalePages.desc}, cardLink='scale-pages', toolIcon='fullscreen', tags=#{scalePages.tags}, toolGroup='organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='extract-page', cardTitle=#{home.extractPage.title}, cardText=#{home.extractPage.desc}, cardLink='extract-page', toolIcon='upload', tags=#{extractPage.tags}, toolGroup='organize')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='pdf-to-single-page', cardTitle=#{home.PdfToSinglePage.title}, cardText=#{home.PdfToSinglePage.desc}, cardLink='pdf-to-single-page', toolIcon='looks_one', tags=#{PdfToSinglePage.tags}, toolGroup='organize')}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="groupConvertTo" class="feature-group-legacy">
|
||||
<div
|
||||
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.convertTo})}">
|
||||
</div>
|
||||
<div class="feature-group-container">
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='img-to-pdf', cardTitle=#{home.imageToPdf.title}, cardText=#{home.imageToPdf.desc}, cardLink='img-to-pdf', toolIcon='picture_as_pdf', tags=#{imageToPdf.tags}, toolGroup='image')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='file-to-pdf', cardTitle=#{home.fileToPDF.title}, cardText=#{home.fileToPDF.desc}, cardLink='file-to-pdf', toolIcon='draft', tags=#{fileToPDF.tags}, toolGroup='convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='url-to-pdf', cardTitle=#{home.URLToPDF.title}, cardText=#{home.URLToPDF.desc}, cardLink='url-to-pdf', toolIcon='link', tags=#{URLToPDF.tags}, toolGroup='convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='html-to-pdf', cardTitle=#{home.HTMLToPDF.title}, cardText=#{home.HTMLToPDF.desc}, cardLink='html-to-pdf', toolIcon='html', tags=#{HTMLToPDF.tags}, toolGroup='convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='markdown-to-pdf', cardTitle=#{home.MarkdownToPDF.title}, cardText=#{home.MarkdownToPDF.desc}, cardLink='markdown-to-pdf', toolIcon='markdown', tags=#{MarkdownToPDF.tags}, toolGroup='convert')}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="groupConvertFrom" class="feature-group-legacy">
|
||||
<div
|
||||
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.convertFrom})}">
|
||||
</div>
|
||||
<div class="feature-group-container">
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='pdf-to-img', cardTitle=#{home.pdfToImage.title}, cardText=#{home.pdfToImage.desc}, cardLink='pdf-to-img', toolIcon='photo_library', tags=#{pdfToImage.tags}, toolGroup='image')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='pdf-to-pdfa', cardTitle=#{home.pdfToPDFA.title}, cardText=#{home.pdfToPDFA.desc}, cardLink='pdf-to-pdfa', toolIcon='picture_as_pdf', tags=#{pdfToPDFA.tags}, toolGroup='convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='pdf-to-word', cardTitle=#{home.PDFToWord.title}, cardText=#{home.PDFToWord.desc}, cardLink='pdf-to-word', toolIcon='description', tags=#{PDFToWord.tags}, toolGroup='word')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='pdf-to-presentation', cardTitle=#{home.PDFToPresentation.title}, cardText=#{home.PDFToPresentation.desc}, cardLink='pdf-to-presentation', toolIcon='slideshow', tags=#{PDFToPresentation.tags}, toolGroup='ppt')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='pdf-to-text', cardTitle=#{home.PDFToText.title}, cardText=#{home.PDFToText.desc}, cardLink='pdf-to-text', toolIcon='text_fields', tags=#{PDFToText.tags}, toolGroup='convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='pdf-to-html', cardTitle=#{home.PDFToHTML.title}, cardText=#{home.PDFToHTML.desc}, cardLink='pdf-to-html', toolIcon='html', tags=#{PDFToHTML.tags}, toolGroup='convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='pdf-to-xml', cardTitle=#{home.PDFToXML.title}, cardText=#{home.PDFToXML.desc}, cardLink='pdf-to-xml', toolIcon='code', tags=#{PDFToXML.tags}, toolGroup='convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='pdf-to-csv', cardTitle=#{home.tableExtraxt.title}, cardText=#{home.tableExtraxt.desc}, cardLink='pdf-to-csv', toolIcon='csv', tags=#{tableExtraxt.tags}, toolGroup='convert')}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="groupSecurity" class="feature-group-legacy">
|
||||
<div
|
||||
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.security})}">
|
||||
</div>
|
||||
<div class="feature-group-container">
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='add-password', cardTitle=#{home.addPassword.title}, cardText=#{home.addPassword.desc}, cardLink='add-password', toolIcon='lock', tags=#{addPassword.tags}, toolGroup='security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='remove-password', cardTitle=#{home.removePassword.title}, cardText=#{home.removePassword.desc}, cardLink='remove-password', toolIcon='lock_open_right', tags=#{removePassword.tags}, toolGroup='security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='change-permissions', cardTitle=#{home.permissions.title}, cardText=#{home.permissions.desc}, cardLink='change-permissions', toolIcon='encrypted', tags=#{permissions.tags}, toolGroup='security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='sign', cardTitle=#{home.sign.title}, cardText=#{home.sign.desc}, cardLink='sign', toolIcon='signature', tags=#{sign.tags}, toolGroup='sign')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='cert-sign', cardTitle=#{home.certSign.title}, cardText=#{home.certSign.desc}, cardLink='cert-sign', toolIcon='workspace_premium', tags=#{certSign.tags}, toolGroup='security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='validate-signature', cardTitle=#{home.validateSignature.title}, cardText=#{home.validateSignature.desc}, cardLink='validate-signature', toolIcon='verified', tags=#{validateSignature.tags}, toolGroup='security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='remove-cert-sign', cardTitle=#{home.removeCertSign.title}, cardText=#{home.removeCertSign.desc}, cardLink='remove-cert-sign', toolIcon='remove_moderator', tags=#{removeCertSign.tags}, toolGroup='security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='sanitize-pdf', cardTitle=#{home.sanitizePdf.title}, cardText=#{home.sanitizePdf.desc}, cardLink='sanitize-pdf', toolIcon='sanitizer', tags=#{sanitizePdf.tags}, toolGroup='security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='auto-redact', cardTitle=#{home.autoRedact.title}, cardText=#{home.autoRedact.desc}, cardLink='auto-redact', toolIcon='ink_eraser', tags=#{autoRedact.tags}, toolGroup='security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='redact', cardTitle=#{home.redact.title}, cardText=#{home.redact.desc}, cardLink='redact', toolIcon='playlist_remove', tags=#{redact.tags}, toolGroup='security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='stamp', cardTitle=#{home.AddStampRequest.title}, cardText=#{home.AddStampRequest.desc}, cardLink='stamp', toolIcon='approval', tags=#{AddStampRequest.tags}, toolGroup='security')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='add-watermark', cardTitle=#{home.watermark.title}, cardText=#{home.watermark.desc}, cardLink='add-watermark', toolIcon='water_drop', tags=#{watermark.tags}, toolGroup='security')}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="groupView" class="feature-group-legacy">
|
||||
<div
|
||||
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.edit})}">
|
||||
</div>
|
||||
<div class="feature-group-container">
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='view-pdf', cardTitle=#{home.viewPdf.title}, cardText=#{home.viewPdf.desc}, cardLink='view-pdf', toolIcon='menu_book', tags=#{viewPdf.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='add-page-numbers', cardTitle=#{home.add-page-numbers.title}, cardText=#{home.add-page-numbers.desc}, cardLink='add-page-numbers', toolIcon='123', tags=#{add-page-numbers.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='add-image', cardTitle=#{home.addImage.title}, cardText=#{home.addImage.desc}, cardLink='add-image', toolIcon='add_photo_alternate', tags=#{addImage.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='change-metadata', cardTitle=#{home.changeMetadata.title}, cardText=#{home.changeMetadata.desc}, cardLink='change-metadata', toolIcon='assignment', tags=#{changeMetadata.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='ocr-pdf', cardTitle=#{home.ocr.title}, cardText=#{home.ocr.desc}, cardLink='ocr-pdf', toolIcon='quick_reference_all', tags=#{ocr.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='extract-images', cardTitle=#{home.extractImages.title}, cardText=#{home.extractImages.desc}, cardLink='extract-images', toolIcon='wallpaper', tags=#{extractImages.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='flatten', cardTitle=#{home.flatten.title}, cardText=#{home.flatten.desc}, cardLink='flatten', toolIcon='layers_clear', tags=#{flatten.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='remove-blanks', cardTitle=#{home.removeBlanks.title}, cardText=#{home.removeBlanks.desc}, cardLink='remove-blanks', toolIcon='scan_delete', tags=#{removeBlanks.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='remove-annotations', cardTitle=#{home.removeAnnotations.title}, cardText=#{home.removeAnnotations.desc}, cardLink='remove-annotations', toolIcon='thread_unread', tags=#{removeAnnotations.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='compare', cardTitle=#{home.compare.title}, cardText=#{home.compare.desc}, cardLink='compare', toolIcon='compare', tags=#{compare.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='get-info-on-pdf', cardTitle=#{home.getPdfInfo.title}, cardText=#{home.getPdfInfo.desc}, cardLink='get-info-on-pdf', toolIcon='info', tags=#{getPdfInfo.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='remove-image-pdf', cardTitle=#{home.removeImagePdf.title}, cardText=#{home.removeImagePdf.desc}, cardLink='remove-image-pdf', toolIcon='remove_selection', tags=#{removeImagePdf.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='replace-and-invert-color-pdf', cardTitle=#{home.replaceColorPdf.title}, cardText=#{home.replaceColorPdf.desc}, cardLink='replace-and-invert-color-pdf', toolIcon='format_color_fill', tags=#{replaceColorPdf.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='unlock-pdf-forms', cardTitle=#{home.unlockPDFForms.title}, cardText=#{home.unlockPDFForms.desc}, cardLink='unlock-pdf-forms', toolIcon='preview_off', tags=#{unlockPDFForms.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('add-attachments', 'attachment', 'home.attachments.title', 'home.attachments.desc', 'attachments.tags', 'other')}">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="groupAdvanced" class="feature-group-legacy">
|
||||
<div
|
||||
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.advance})}">
|
||||
</div>
|
||||
<div class="feature-group-container">
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', toolIcon='family_history', tags=#{pipeline.tags}, toolGroup='advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='adjust-contrast', cardTitle=#{home.adjust-contrast.title}, cardText=#{home.adjust-contrast.desc}, cardLink='adjust-contrast', toolIcon='palette', tags=#{adjust-contrast.tags}, toolGroup='advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', toolIcon='zoom_in_map', tags=#{compressPdfs.tags}, toolGroup='advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='extract-image-scans', cardTitle=#{home.ScannerImageSplit.title}, cardText=#{home.ScannerImageSplit.desc}, cardLink='extract-image-scans', toolIcon='scanner', tags=#{ScannerImageSplit.tags}, toolGroup='advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='repair', cardTitle=#{home.repair.title}, cardText=#{home.repair.desc}, cardLink='repair', toolIcon='build', tags=#{repair.tags}, toolGroup='advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='auto-rename', cardTitle=#{home.auto-rename.title}, cardText=#{home.auto-rename.desc}, cardLink='auto-rename', toolIcon='text_fields_alt', tags=#{auto-rename.tags}, toolGroup='advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='auto-split-pdf', cardTitle=#{home.autoSplitPDF.title}, cardText=#{home.autoSplitPDF.desc}, cardLink='auto-split-pdf', toolIcon='cut', tags=#{autoSplitPDF.tags}, toolGroup='advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='show-javascript', cardTitle=#{home.showJS.title}, cardText=#{home.showJS.desc}, cardLink='show-javascript', toolIcon='javascript', tags=#{showJS.tags}, toolGroup='advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='split-by-size-or-count', cardTitle=#{home.autoSizeSplitPDF.title}, cardText=#{home.autoSizeSplitPDF.desc}, cardLink='split-by-size-or-count', toolIcon='vertical_split', tags=#{autoSizeSplitPDF.tags}, toolGroup='advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='overlay-pdf', cardTitle=#{home.overlay-pdfs.title}, cardText=#{home.overlay-pdfs.desc}, cardLink='overlay-pdf', toolIcon='layers', tags=#{overlay-pdfs.tags}, toolGroup='advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='split-pdf-by-sections', cardTitle=#{home.split-by-sections.title}, cardText=#{home.split-by-sections.desc}, cardLink='split-pdf-by-sections', toolIcon='grid_on', tags=#{split-by-sections.tags}, toolGroup='advance')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='split-pdf-by-chapters', cardTitle=#{home.splitPdfByChapters.title}, cardText=#{home.splitPdfByChapters.desc}, cardLink='split-pdf-by-chapters', toolIcon='book', tags=#{splitPdfByChapters.tags}, toolGroup='advance')}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Survey Modal -->
|
||||
<div class="modal fade" id="surveyModal" tabindex="-1" role="dialog" aria-labelledby="surveyModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="surveyModalLabel" th:text="#{survey.title}">Stirling-PDF Survey</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p th:text="#{survey.meeting.1}">If you're using Stirling PDF at work, we'd love to speak to you. We're offering free technical support in exchange for a 15 minute user discovery session.</p>
|
||||
<p th:text="#{survey.meeting.2}">This is a chance to:</p>
|
||||
<p><span>🛠️</span><span th:text="#{survey.meeting.3}">Get help with deployment, integrations, or troubleshooting</span></p>
|
||||
<p><span>📢</span><span th:text="#{survey.meeting.4}">Provide direct feedback on performance, edge cases, and feature gaps</span></p>
|
||||
<p><span>🔍</span><span th:text="#{survey.meeting.5}">Help us refine Stirling PDF for real-world enterprise use</span></p>
|
||||
<p th:text="#{survey.meeting.6}">If you're interested, you can book time with our team directly.</p>
|
||||
<p th:text="#{survey.meeting.7}">Looking forward to digging into your use cases and making Stirling PDF even better!</p>
|
||||
<a href="https://calendly.com/d/cm4p-zz5-yy8/stirling-pdf-15-minute-group-discussion" target="_blank" class="btn btn-primary" id="takeSurvey2" th:text="#{survey.meeting.button}">Book meeting</a>
|
||||
</br>
|
||||
</br>
|
||||
<p th:text="#{survey.meeting.notInterested}">Not a business and/or interested in a meeting?</p>
|
||||
|
||||
<p th:text="#{survey.please}">Please consider taking our survey!</p>
|
||||
<a href="https://stirlingpdf.info/s/cm28y3niq000o56dv7liv8wsu" target="_blank" class="btn btn-primary"
|
||||
id="takeSurvey" th:text="#{survey.button}">Take Survey</a>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" id="dontShowAgain">
|
||||
<label for="dontShowAgain" th:text="#{survey.dontShowAgain}">Don't show again</label>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}">Close</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Analytics Modal -->
|
||||
<div class="modal fade" id="analyticsModal" tabindex="-1" role="dialog" aria-labelledby="analyticsModalLabel"
|
||||
aria-hidden="true" th:if="${@analyticsPrompt}">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="analyticsModalLabel" th:text="#{analytics.title}">Do you want make Stirling PDF
|
||||
better?</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p th:text="#{analytics.paragraph1}">Stirling PDF has opt in analytics to help us improve the product. We do
|
||||
not track any personal information or file contents.</p>
|
||||
<p th:text="#{analytics.paragraph2}">Please consider enabling analytics to help Stirling-PDF grow and to allow
|
||||
us to understand our users better.</p>
|
||||
<p th:text="#{analytics.settings}">You can change the settings for analytics in the config/settings.yml file
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-between">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="setAnalytics(false)"
|
||||
th:text="#{analytics.disable}">Disable analytics</button>
|
||||
<button type="button" class="btn btn-primary" th:text="#{analytics.enable}"
|
||||
onclick="setAnalytics(true)">Enable analytics</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script th:src="@{'/js/fetch-utils.js'}"></script>
|
||||
<script th:inline="javascript">
|
||||
|
||||
/*<![CDATA[*/
|
||||
const analyticsPromptBoolean = /*[[${@analyticsPrompt}]]*/ false;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (analyticsPromptBoolean) {
|
||||
const analyticsModal = new bootstrap.Modal(document.getElementById('analyticsModal'));
|
||||
analyticsModal.show();
|
||||
}
|
||||
});
|
||||
/*]]>*/
|
||||
function setAnalytics(enabled) {
|
||||
fetchWithCsrf('api/v1/settings/update-enable-analytics', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(enabled)
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
console.log('Analytics setting updated successfully');
|
||||
bootstrap.Modal.getInstance(document.getElementById('analyticsModal')).hide();
|
||||
} else if (response.status === 208) {
|
||||
console.log('Analytics setting has already been set. Please edit /config/settings.yml to change it.', response);
|
||||
alert('Analytics setting has already been set. Please edit /config/settings.yml to change it.');
|
||||
} else {
|
||||
throw new Error('Unexpected response status: ' + response.status);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error updating analytics setting:', error);
|
||||
alert('An error occurred while updating the analytics setting. Please try again.');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const surveyVersion = "3.0";
|
||||
const modal = new bootstrap.Modal(document.getElementById('surveyModal'));
|
||||
const dontShowAgain = document.getElementById('dontShowAgain');
|
||||
const takeSurveyButton = document.getElementById('takeSurvey');
|
||||
|
||||
const viewThresholds = [5, 10, 15, 22, 30, 50, 75, 100, 150, 200];
|
||||
|
||||
// Check if survey version changed and reset page views if it did
|
||||
const storedVersion = localStorage.getItem('surveyVersion');
|
||||
if (storedVersion && storedVersion !== surveyVersion) {
|
||||
localStorage.setItem('pageViews', '0');
|
||||
localStorage.setItem('surveyVersion', surveyVersion);
|
||||
}
|
||||
|
||||
let pageViews = parseInt(localStorage.getItem('pageViews') || '0');
|
||||
|
||||
pageViews++;
|
||||
localStorage.setItem('pageViews', pageViews.toString());
|
||||
|
||||
function shouldShowSurvey() {
|
||||
if (localStorage.getItem('dontShowSurvey') === 'true' ||
|
||||
localStorage.getItem('surveyTaken') === 'true') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If survey version changed and we hit a threshold, show the survey
|
||||
if (localStorage.getItem('surveyVersion') !== surveyVersion &&
|
||||
viewThresholds.includes(pageViews)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return viewThresholds.includes(pageViews);
|
||||
}
|
||||
|
||||
if (shouldShowSurvey()) {
|
||||
modal.show();
|
||||
}
|
||||
|
||||
dontShowAgain.addEventListener('change', function () {
|
||||
if (this.checked) {
|
||||
localStorage.setItem('dontShowSurvey', 'true');
|
||||
localStorage.setItem('surveyVersion', surveyVersion);
|
||||
} else {
|
||||
localStorage.removeItem('dontShowSurvey');
|
||||
localStorage.removeItem('surveyVersion');
|
||||
}
|
||||
});
|
||||
|
||||
takeSurveyButton.addEventListener('click', function () {
|
||||
localStorage.setItem('surveyTaken', 'true');
|
||||
localStorage.setItem('surveyVersion', surveyVersion);
|
||||
modal.hide();
|
||||
});
|
||||
|
||||
if (localStorage.getItem('dontShowSurvey')) {
|
||||
modal.hide();
|
||||
}
|
||||
});
|
||||
|
||||
function setAsDefault(value) {
|
||||
localStorage.setItem('defaultView', value);
|
||||
console.log(`Default view set to: ${value}`);
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,294 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title='')}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page-container" style="max-height:100vh;">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<div style="transform-origin: center top; flex:0 1 auto; display:flex; flex-direction:column; align-items:center; justify-content:flex-start;" id="scale-wrap">
|
||||
<br class="d-md-none">
|
||||
<!-- Features -->
|
||||
<script th:src="@{'/js/homecard.js'}"></script>
|
||||
<div style="
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;"
|
||||
>
|
||||
<div>
|
||||
<br>
|
||||
<div style="justify-content: center; display: flex;">
|
||||
<div style="margin:0 3rem">
|
||||
<div>
|
||||
<div
|
||||
style="display:flex; flex-direction: column; justify-content: center; width:100%; margin-bottom:1rem">
|
||||
<div style="width:fit-content; margin: 0 auto; padding: 0 3rem">
|
||||
<div class="lead fs-4"
|
||||
th:text="${@homeText != 'null' and @homeText != null and @homeText != ''} ? ${@homeText} : #{home.desc}">
|
||||
</div>
|
||||
</div>
|
||||
<div id="groupRecent" style="width: fit-content; margin: 0 auto">
|
||||
<div
|
||||
th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.recent})}">
|
||||
</div>
|
||||
<div class="recent-features">
|
||||
<div class="newfeature"
|
||||
th:insert="~{fragments/navbarEntry :: navbarEntry('add-attachments', 'attachment', 'home.attachments.title', 'home.attachments.desc', 'attachments.tags', 'other')}">
|
||||
</div>
|
||||
<div class="newfeature"
|
||||
th:insert="~{fragments/navbarEntry :: navbarEntry('fake-scan', 'scanner', 'fakeScan.title', 'fakeScan.description', 'fakeScan.tags', 'advance')}">
|
||||
</div>
|
||||
<div class="newfeature"
|
||||
th:insert="~{fragments/navbarEntry :: navbarEntry('compress-pdf', 'zoom_in_map', 'home.compressPdfs.title', 'home.compressPdfs.desc', 'compressPDFs.tags', 'advance')}">
|
||||
</div>
|
||||
<div class="newfeature"
|
||||
th:insert="~{fragments/navbarEntry :: navbarEntry('edit-table-of-contents', 'bookmark_add', 'home.editTableOfContents.title', 'home.editTableOfContents.desc', 'editTableOfContents.tags', 'advance')}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="text" id="searchBar" onkeyup="filterCards()" th:placeholder="#{home.searchBar}" autofocus>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; column-gap: 3rem; flex-wrap: wrap; margin-left:1rem">
|
||||
<div
|
||||
style="height:2.5rem; display: flex; align-items: center; cursor: pointer; justify-content: center;">
|
||||
<label for="sort-options" th:text="#{home.sortBy}">Sort by:</label>
|
||||
<select id="sort-options" style="border:none;">
|
||||
<option value="alphabetical" th:text="#{home.alphabetical}"> </option>
|
||||
<!-- <option value="personal">Your most used</option> -->
|
||||
<option value="global" th:text="#{home.globalPopularity}"></option>
|
||||
<!-- <option value="server">Popularity in organisation</option> -->
|
||||
</select>
|
||||
</div>
|
||||
<div
|
||||
style="display: flex; align-items: center; flex-wrap: wrap; align-content: flex-start; width: fit-content; max-width: 100%; gap:2rem; justify-content: center;" >
|
||||
<div th:title="#{home.setFavorites}" style="display: flex; align-items: center; cursor: pointer;"
|
||||
onclick="toggleFavoritesMode()">
|
||||
<span class="material-symbols-rounded toggle-favourites"
|
||||
style="font-size: 2rem; margin-left: 0.2rem;">
|
||||
star
|
||||
</span>
|
||||
</div>
|
||||
<div onclick="toggleFavoritesView()" th:title="#{home.hideFavorites}" id="favouritesVisibility"
|
||||
style="display: flex; align-items: center; cursor: pointer;">
|
||||
<span id="toggle-favourites-icon" class="material-symbols-rounded toggle-favourites"
|
||||
style="font-size: 2rem; margin-left: 0.2rem;">
|
||||
visibility
|
||||
</span>
|
||||
</div>
|
||||
<a th:if="${@shouldShow}" href="https://github.com/Stirling-Tools/Stirling-PDF/releases"
|
||||
target="_blank" id="update-link" rel="noopener" th:title="#{settings.update}"
|
||||
style="text-decoration: none; color: inherit; cursor: pointer; display: flex; align-items: center;">
|
||||
<span class="material-symbols-rounded toggle-favourites"
|
||||
style="font-size: 2rem; margin-left: 0.2rem;">
|
||||
update
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
<div style="width:100%; overflow-x:visible;">
|
||||
<div class="features-container" style=" border-top: 1px;
|
||||
border-top-style: solid;
|
||||
border-color: var(--md-nav-color-on-separator);
|
||||
margin-top: 1rem;
|
||||
">
|
||||
<div class="feature-rows">
|
||||
<div id="groupFavorites" class="feature-group">
|
||||
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.favorite})}">
|
||||
</div>
|
||||
<div class="nav-group-container">
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/navElements.html :: navElements}"></th:block>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Survey Modal -->
|
||||
<div class="modal fade" id="surveyModal" tabindex="-1" role="dialog" aria-labelledby="surveyModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="surveyModalLabel" th:text="#{survey.title}">Stirling-PDF Survey</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p th:text="#{survey.meeting.1}">If you're using Stirling PDF at work, we'd love to speak to you. We're offering free technical support in exchange for a 15 minute user discovery session.</p>
|
||||
<p th:text="#{survey.meeting.2}">This is a chance to:</p>
|
||||
<p><span>🛠️</span><span th:text="#{survey.meeting.3}">Get help with deployment, integrations, or troubleshooting</span></p>
|
||||
<p><span>📢</span><span th:text="#{survey.meeting.4}">Provide direct feedback on performance, edge cases, and feature gaps</span></p>
|
||||
<p><span>🔍</span><span th:text="#{survey.meeting.5}">Help us refine Stirling PDF for real-world enterprise use</span></p>
|
||||
<p th:text="#{survey.meeting.6}">If you're interested, you can book time with our team directly.</p>
|
||||
<p th:text="#{survey.meeting.7}">Looking forward to digging into your use cases and making Stirling PDF even better!</p>
|
||||
<a href="https://calendly.com/d/crsr-tz6-487" target="_blank" class="btn btn-primary" id="takeSurvey2" th:text="#{survey.meeting.button}">Book meeting</a>
|
||||
</br>
|
||||
</br>
|
||||
<p th:text="#{survey.meeting.notInterested}">Not a business and/or interested in a meeting?</p>
|
||||
|
||||
<p th:text="#{survey.please}">Please consider taking our survey!</p>
|
||||
<a href="https://stirlingpdf.info/s/cm28y3niq000o56dv7liv8wsu" target="_blank" class="btn btn-primary"
|
||||
id="takeSurvey" th:text="#{survey.button}">Take Survey</a>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" id="dontShowAgain">
|
||||
<label for="dontShowAgain" th:text="#{survey.dontShowAgain}">Don't show again</label>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}">Close</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Analytics Modal -->
|
||||
<div class="modal fade" id="analyticsModal" tabindex="-1" role="dialog" aria-labelledby="analyticsModalLabel"
|
||||
aria-hidden="true" th:if="${@analyticsPrompt}">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="analyticsModalLabel" th:text="#{analytics.title}">Do you want make Stirling PDF
|
||||
better?</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p th:text="#{analytics.paragraph1}">Stirling PDF has opt in analytics to help us improve the product. We do
|
||||
not track any personal information or file contents.</p>
|
||||
<p th:text="#{analytics.paragraph2}">Please consider enabling analytics to help Stirling-PDF grow and to allow
|
||||
us to understand our users better.</p>
|
||||
<p th:text="#{analytics.settings}">You can change the settings for analytics in the config/settings.yml file
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-between">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="setAnalytics(false)"
|
||||
th:text="#{analytics.disable}">Disable analytics</button>
|
||||
<button type="button" class="btn btn-primary" th:text="#{analytics.enable}"
|
||||
onclick="setAnalytics(true)">Enable analytics</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
.favorite-icon {
|
||||
cursor: pointer;
|
||||
width: 0rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.toggle-favourites {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-favourites.active {
|
||||
color: gold;
|
||||
}
|
||||
</style>
|
||||
<script th:src="@{'/js/fetch-utils.js'}"></script>
|
||||
<script th:inline="javascript">
|
||||
/*<![CDATA[*/
|
||||
window.analyticsPromptBoolean = /*[[${@analyticsPrompt}]]*/ false;
|
||||
/*]]>*/
|
||||
|
||||
window.showSurvey = /*[[${showSurveyFromDocker}]]*/ true
|
||||
</script>
|
||||
<script th:src="@{'/js/pages/home.js'}" th:inline="javascript"></script>
|
||||
<script>
|
||||
function scaleStuff() {
|
||||
const w = 1440;
|
||||
const h = 1000;
|
||||
|
||||
const sx = window.innerWidth / w;
|
||||
const sy = window.innerHeight / h;
|
||||
const s = Math.max(0.9, Math.min(sx, sy));
|
||||
const el = document.getElementById('scale-wrap');
|
||||
const container = document.querySelector('.page-container');
|
||||
|
||||
const nav = document.querySelector('.navbar, nav');
|
||||
const navH = nav ? nav.offsetHeight : 0;
|
||||
const space = container.offsetHeight - navH;
|
||||
const origH = el.scrollHeight;
|
||||
|
||||
// Progressive bonus based on how tall the screen is
|
||||
const aspectRatio = window.innerWidth / window.innerHeight;
|
||||
const tallScreenBonus = Math.max(0, (1.8 - aspectRatio) * 0.15); // More bonus for taller screens
|
||||
|
||||
let finalS = Math.min(s * 0.75, (space * (0.98 + tallScreenBonus)) / origH);
|
||||
finalS = Math.max(0.7, Math.min(1.0, finalS)); // Never scale above 100%
|
||||
|
||||
el.style.transform = `scale(${finalS})`;
|
||||
el.style.transformOrigin = 'top center';
|
||||
el.style.height = `${origH * finalS}px`;
|
||||
|
||||
// Dynamically adjust features container width based on scale
|
||||
const featuresContainer = document.querySelector('.features-container');
|
||||
if (featuresContainer) {
|
||||
const isRTL = document.documentElement.dir === 'rtl' || document.documentElement.getAttribute('dir') === 'rtl';
|
||||
const dynamicWidth = Math.min(120, 100 / finalS);
|
||||
const offset = (dynamicWidth - 100) / 2;
|
||||
|
||||
featuresContainer.style.width = `${dynamicWidth}%`;
|
||||
|
||||
if (isRTL) {
|
||||
featuresContainer.style.right = `-${offset}%`;
|
||||
featuresContainer.style.left = 'auto';
|
||||
} else {
|
||||
featuresContainer.style.left = `-${offset}%`;
|
||||
featuresContainer.style.right = 'auto';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
let prevW = window.innerWidth;
|
||||
let prevH = window.innerHeight;
|
||||
let prevDPR = window.devicePixelRatio;
|
||||
|
||||
function onResize() {
|
||||
const w = window.innerWidth;
|
||||
const h = window.innerHeight;
|
||||
const dpr = window.devicePixelRatio;
|
||||
|
||||
if (w !== prevW || h !== prevH) {
|
||||
if (dpr === prevDPR) {
|
||||
scaleStuff();
|
||||
}
|
||||
}
|
||||
|
||||
prevW = w;
|
||||
prevH = h;
|
||||
prevDPR = dpr;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(scaleStuff, 100);
|
||||
});
|
||||
|
||||
window.addEventListener('resize', onResize);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{licenses.title}, header=#{licenses.title})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">license</span>
|
||||
<span class="tool-header-text" th:text="#{licenses.header}">3rd Party licenses</span>
|
||||
</div>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" th:text="#{licenses.module}">Module</th>
|
||||
<th scope="col" th:text="#{licenses.version}">Version</th>
|
||||
<th scope="col" th:text="#{licenses.license}">License</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="dep : ${dependencies}">
|
||||
<td><a th:href="${dep.moduleUrl}" th:text="${dep.moduleName}"></a></td>
|
||||
<td th:text="${dep.moduleVersion}"></td>
|
||||
<td><a th:href="${dep.moduleLicenseUrl}" th:text="${dep.moduleLicense}"></a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,178 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{login.title}, header=#{login.header})}"></th:block>
|
||||
<link rel="stylesheet" th:href="@{'/css/login.css'}">
|
||||
<script th:src="@{'/js/languageSelection.js'}"></script>
|
||||
<script th:src="@{'/js/additionalLanguageCode.js'}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="your-container-class"></div>
|
||||
<div class="container-flex">
|
||||
<main class="form-signin">
|
||||
<script th:inline="javascript">
|
||||
const redirectAttempts = parseInt(localStorage.getItem('ssoRedirectAttempts') || '0');
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const hasRedirectError = urlParams.has('error');
|
||||
const hasLogout = urlParams.has('logout');
|
||||
const hasMessage = urlParams.has('message');
|
||||
const MAX_REDIRECT_ATTEMPTS = 3;
|
||||
|
||||
document.addEventListener('modeChanged', function(e) {
|
||||
var mode = e.detail;
|
||||
|
||||
document.body.classList.remove("light-mode", "dark-mode", "rainbow-mode"); // remove all mode classes first
|
||||
|
||||
switch (mode) {
|
||||
case "on":
|
||||
document.body.classList.add("dark-mode");
|
||||
break;
|
||||
case "off":
|
||||
document.body.classList.add("light-mode");
|
||||
break;
|
||||
case "rainbow":
|
||||
document.body.classList.add("rainbow-mode");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
const runningProOrHigher = /*[[${@runningProOrHigher}]]*/ false;
|
||||
const SSOAutoLogin = /*[[${@SSOAutoLogin}]]*/ false;
|
||||
const loginMethod = /*[[${loginMethod}]]*/ 'normal';
|
||||
const providerList = /*[[${providerList}]]*/ {};
|
||||
const shouldAutoRedirect = !hasRedirectError &&
|
||||
!hasLogout &&
|
||||
!hasMessage &&
|
||||
redirectAttempts < MAX_REDIRECT_ATTEMPTS &&
|
||||
loginMethod !== 'normal' && runningProOrHigher && SSOAutoLogin;
|
||||
|
||||
console.log('Should redirect:', shouldAutoRedirect, {
|
||||
'No error': !hasRedirectError,
|
||||
'No logout': !hasLogout,
|
||||
'No message': !hasMessage,
|
||||
'Under max attempts': redirectAttempts < MAX_REDIRECT_ATTEMPTS,
|
||||
'Is only OAuth2': loginMethod === 'oauth2'
|
||||
});
|
||||
|
||||
if (shouldAutoRedirect && providerList && Object.keys(providerList).length > 0) {
|
||||
localStorage.setItem('ssoRedirectAttempts', redirectAttempts + 1);
|
||||
const firstProvider = Object.keys(providerList)[0];
|
||||
window.location.href = firstProvider;
|
||||
}
|
||||
|
||||
// Reset redirect attempts if successful login or after 1 hour
|
||||
const lastAttemptTime = parseInt(localStorage.getItem('lastRedirectAttempt') || '0');
|
||||
if (Date.now() - lastAttemptTime > 3600000) { // 1 hour
|
||||
localStorage.setItem('ssoRedirectAttempts', '0');
|
||||
}
|
||||
localStorage.setItem('lastRedirectAttempt', Date.now().toString());
|
||||
|
||||
|
||||
const defaultLocale = getStoredOrDefaultLocale();
|
||||
checkUserLanguage(defaultLocale);
|
||||
|
||||
const dropdownItems = document.querySelectorAll('.lang_dropdown-item');
|
||||
let activeItem;
|
||||
|
||||
for (let i = 0; i < dropdownItems.length; i++) {
|
||||
const item = dropdownItems[i];
|
||||
item.classList.remove('active');
|
||||
if (item.dataset.bsLanguageCode === defaultLocale) {
|
||||
item.classList.add('active');
|
||||
activeItem = item;
|
||||
}
|
||||
item.addEventListener('click', handleDropdownItemClick);
|
||||
}
|
||||
|
||||
const dropdown = document.getElementById('languageDropdown');
|
||||
|
||||
if (activeItem) {
|
||||
dropdown.innerHTML = activeItem.innerHTML; // This will set the dropdown button's content to the active language's flag and name
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<div class="text-center">
|
||||
<img class="my-4" th:src="@{'/favicon.svg'}" alt="favicon" width="144" height="144">
|
||||
|
||||
<h1 class="h1 mb-3 fw-normal" th:text="${@appName}">Stirling-PDF</h1>
|
||||
<div th:if="${altLogin} and (${loginMethod} == 'all' or ${loginMethod} == 'oauth2' or ${loginMethod} == 'saml2')">
|
||||
<a href="#" class="w-100 btn btn-lg btn-primary" data-bs-toggle="modal" data-bs-target="#loginsModal" th:text="#{login.ssoSignIn}">Login Via SSO</a>
|
||||
<br>
|
||||
<br>
|
||||
<hr />
|
||||
</div>
|
||||
<div th:if="${errorOAuth}" class="alert alert-danger text-center">
|
||||
<div th:if="${errorOAuth}" th:text="#{${errorOAuth}}">OAuth2: Error Message</div>
|
||||
</div>
|
||||
|
||||
<div th:if="${error}" class="alert alert-danger text-center">
|
||||
<div th:if="${error}" th:text="#{${error}}">OAuth2: Error Message</div>
|
||||
</div>
|
||||
<div class="text-danger text-center">
|
||||
<div th:if="${logoutMessage}" class="alert alert-success" th:text="#{${logoutMessage}}"></div>
|
||||
<div th:if="${messageType}" class="alert alert-success">
|
||||
<span th:text="#{${messageType}}">Default message if not found</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form th:if="${loginMethod} == 'all' or ${loginMethod} == 'normal'" th:action="@{'/login'}" method="post">
|
||||
<h2 class="h5 mb-3 fw-normal" th:text="#{login.signinTitle}">Please sign in</h2>
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="username" name="username" placeholder="admin">
|
||||
<label for="username" th:text="#{username}">Username</label>
|
||||
</div>
|
||||
<div class="form-floating">
|
||||
<input type="password" class="form-control" id="password" name="password" placeholder="Password">
|
||||
<label for="password" th:text="#{password}">Password</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check m-2 mb-3">
|
||||
<input type="checkbox" name="remember-me" id="remember-me">
|
||||
<label for="remember-me" th:text="#{login.rememberme}"></label>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary" th:text="#{login.signin}">Sign in</button>
|
||||
</form>
|
||||
<div class="mt-3"> <!-- Added a margin-top for spacing -->
|
||||
<div class="dropdown text-center">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="languageDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
English (GB)
|
||||
<!-- Default language placeholder -->
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="languageDropdown">
|
||||
<!-- Here's where the fragment will be included -->
|
||||
<div id="languageSelection" class="scrollable-y" >
|
||||
<th:block th:replace="~{fragments/languages :: langs}"></th:block>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
<div th:if="${altLogin}" class="modal fade" id="loginsModal" tabindex="-1" role="dialog" aria-labelledby="loginsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="loginsModalLabel" th:text="#{login.ssoSignIn}"></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="material-symbols-rounded">
|
||||
close
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3" th:each="provider : ${providerList}">
|
||||
<a th:href="@{|${provider.key}|}" th:text="${provider.value}" class="w-100 btn btn-lg btn-primary">Login Provider</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{merge.title}, header=#{merge.header})}"></th:block>
|
||||
<link rel="stylesheet" th:href="@{'/css/merge.css'}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<th:block th:insert="~{fragments/multi-toolAdvert.html :: multi-toolAdvert}"></th:block>
|
||||
<div class="container" id="dropContainer">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">add_to_photos</span>
|
||||
<span class="tool-header-text" th:text="#{merge.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'api/v1/general/merge-pdfs'}" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label th:text="#{multiPdfDropPrompt}" for="fileInput-input"></label>
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=true, accept='application/pdf')}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="checkbox" name="removeCertSign" id="removeCertSign">
|
||||
<label for="removeCertSign" th:text="#{merge.removeCertSign}">Remove digital signature in the merged
|
||||
file?</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="checkbox" name="generateToc" id="generateToc">
|
||||
<label for="generateToc" th:text="#{merge.generateToc}">Generate table of contents in the merged file?</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<ul id="selectedFiles" class="list-group"></ul>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<button type="button" id="sortByNameBtn" class="btn btn-info" th:text="#{merge.sortByName}"></button>
|
||||
<button type="button" id="sortByDateBtn" class="btn btn-info" th:text="#{merge.sortByDate}"></button>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{merge.submit}"></button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<button type="button" id="resetFileInputBtn" class="btn btn-danger" th:text="#{reset}">Reset</button>
|
||||
</div>
|
||||
</form>
|
||||
<span id="pageTranslation" th:text="#{page}" style="display:none;"></span>
|
||||
<span id="pagesTranslation" th:text="#{pages}" style="display:none;"></span>
|
||||
|
||||
<script type="text/javascript">
|
||||
const pageTranslation = document.getElementById('pageTranslation').innerText; // Get translation for a single page
|
||||
const pagesTranslation = document.getElementById('pagesTranslation').innerText; // Get translation for multiple pages
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as pdfjsLib from './pdfjs-legacy/pdf.mjs';
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||
</script>
|
||||
<script th:src="@{'/js/merge.js'}"></script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{attachments.title}, header=#{attachments.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon other">attachment</span>
|
||||
<span class="tool-header-text" th:text="#{attachments.header}"></span>
|
||||
</div>
|
||||
|
||||
<form action="#" th:action="@{/api/v1/misc/add-attachments}" method="post" enctype="multipart/form-data">
|
||||
<!-- PDF file selector -->
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', disableMultipleFiles=true, multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
|
||||
<!-- Attachment files selector -->
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='attachments', disableMultipleFiles=false, multipleInputsForSingleRequest=true, accept='*/*', inputText=#{attachments.selectFiles})}">
|
||||
</div>
|
||||
|
||||
<!-- Submit button -->
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{attachments.addButton}">Add Attachments</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,116 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{addImage.title}, header=#{addImage.header})}"></th:block>
|
||||
<script th:src="@{'/js/thirdParty/interact.min.js'}"></script>
|
||||
<link rel="stylesheet" th:href="@{'/css/add-image.css'}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon other">add_photo_alternate</span>
|
||||
<span class="tool-header-text" th:text="#{addImage.header}"></span>
|
||||
</div>
|
||||
|
||||
<!-- pdf selector -->
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='pdf-upload', disableMultipleFiles=true, multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<script type="module" th:src="@{'/js/pages/add-image.js'}"></script>
|
||||
<div class="show-on-file-selected">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=false, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="draggable-buttons-box ignore-rtl">
|
||||
<button class="btn btn-outline-secondary" th:title="#{sign.delete}"
|
||||
onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())"
|
||||
style="color: #C02223; border-color: #C02223; background-color: rgba(255, 0, 0, 0.1);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash"
|
||||
viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z" />
|
||||
<path
|
||||
d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" th:title="#{sign.addToAll}"
|
||||
onclick="DraggableUtils.addAllPagesDraggableCanvas(DraggableUtils.getLastInteracted())">
|
||||
<span class="material-symbols-rounded">
|
||||
content_copy
|
||||
</span>
|
||||
</button>
|
||||
<div id="rotation-controls" class="align-items-center" style="display: none;">
|
||||
<div class="input-with-icon">
|
||||
<span class="material-symbols-rounded icon" style="margin-right: 3px;">
|
||||
screen_rotation
|
||||
</span>
|
||||
<input type="number" id="rotation-input" class="form-control form-control-sm me-2" value="0" step="10"
|
||||
style="width: 6rem" />
|
||||
</div>
|
||||
</div>
|
||||
<button id="ratioToggleBtn" th:title="#{sign.maintainRatio}" class="btn btn-outline-secondary"
|
||||
onclick="DraggableUtils.toggleMaintainRatio()">
|
||||
<span class="material-symbols-rounded">
|
||||
Aspect_Ratio
|
||||
</span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" th:title="#{sign.first}" onclick="goToFirstOrLastPage(false)"
|
||||
style="margin-left:auto">
|
||||
<span class="material-symbols-rounded">
|
||||
keyboard_double_arrow_left
|
||||
</span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" th:title="#{sign.previous}" id="incrementPage"
|
||||
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-chevron-left" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd"
|
||||
d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" th:title="#{sign.next}" id="decrementPage"
|
||||
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-chevron-right" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd"
|
||||
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" th:title="#{sign.last}" onclick="goToFirstOrLastPage(true)">
|
||||
<span class="material-symbols-rounded">
|
||||
keyboard_double_arrow_right
|
||||
</span>
|
||||
</button>
|
||||
<button id="download-pdf" th:title="#{downloadPdf}" class="btn btn-outline-secondary"
|
||||
style="border-color: green; color:#b2e3a8; background: rgba(24, 122, 5, 1)">
|
||||
<span class="material-symbols-rounded">
|
||||
download
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- draggables box -->
|
||||
<div id="box-drag-container" class="show-on-file-selected">
|
||||
<canvas id="pdf-canvas"></canvas>
|
||||
<script th:src="@{'/js/draggable-utils.js'}"></script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{addPageNumbers.title}, header=#{addPageNumbers.header})}"></th:block>
|
||||
<style>
|
||||
.a4container {
|
||||
position: relative;
|
||||
width: 50%;
|
||||
aspect-ratio: 0.707/1;
|
||||
border: 1px solid #ddd;
|
||||
box-sizing: border-box;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.pageNumber {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 1em;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
background-color: #ccc;
|
||||
width: 15%;
|
||||
height: 15%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.pageNumber:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
#myForm {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.selectedPosition {
|
||||
background-color: #0a0;
|
||||
}
|
||||
|
||||
.selectedPosition.selectedHovered {
|
||||
background-color: #006600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon other">123</span>
|
||||
<span class="tool-header-text" th:text="#{addPageNumbers.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/misc/add-page-numbers'}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<br>
|
||||
<div class="mb-3">
|
||||
<label for="customMargin" th:text="#{addPageNumbers.selectText.2}"></label>
|
||||
<select class="form-control" id="customMargin" name="customMargin">
|
||||
<option value="small" th:text="#{sizes.small}"></option>
|
||||
<option value="medium" selected th:text="#{sizes.medium}"></option>
|
||||
<option value="large" th:text="#{sizes.large}"></option>
|
||||
<option value="x-large" th:text="#{sizes.x-large}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label th:text="#{addPageNumbers.selectText.3}"></label>
|
||||
<div class="a4container">
|
||||
<div class="pageNumber" id="1" style="top: 10%; left: 10%;">1</div>
|
||||
<div class="pageNumber" id="2" style="top: 10%; left: 50%;">2</div>
|
||||
<div class="pageNumber" id="3" style="top: 10%; left: 90%;">3</div>
|
||||
<div class="pageNumber" id="4" style="top: 50%; left: 10%;">4</div>
|
||||
<div class="pageNumber" id="5" style="top: 50%; left: 50%;">5</div>
|
||||
<div class="pageNumber" id="6" style="top: 50%; left: 90%;">6</div>
|
||||
<div class="pageNumber" id="7" style="top: 90%; left: 10%;">7</div>
|
||||
<div class="pageNumber selectedPosition" id="8" style="top: 90%; left: 50%;">8</div>
|
||||
<div class="pageNumber" id="9" style="top: 90%; left: 90%;">9</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="numberInput" name="position" value="8">
|
||||
<div class="mb-3">
|
||||
<label for="fontSize" th:text="#{addPageNumbers.fontSize}"></label>
|
||||
<input type="number" class="form-control" id="fontSize" name="fontSize" min="1" value="12" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="fontType" th:text="#{addPageNumbers.fontName}"></label>
|
||||
<select class="form-control" id="fontType" name="fontType">
|
||||
<option value="Times">Times Roman</option>
|
||||
<option value="Helvetica">Helvetica</option>
|
||||
<option value="Courier">Courier New</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="startingNumber" th:text="#{addPageNumbers.selectText.4}"></label>
|
||||
<input type="number" class="form-control" id="startingNumber" name="startingNumber" min="1" required value="1">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pagesToNumber" th:text="#{addPageNumbers.selectText.5}"></label>
|
||||
<input type="text" class="form-control" id="pagesToNumber" name="pagesToNumber" th:placeholder="#{addPageNumbers.numberPagesDesc}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="customText" th:text="#{addPageNumbers.selectText.6}"></label>
|
||||
<input type="text" class="form-control" id="customText" name="customText" th:placeholder="#{addPageNumbers.customNumberDesc}">
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{addPageNumbers.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let cells = document.querySelectorAll('.pageNumber');
|
||||
let inputField = document.getElementById('numberInput');
|
||||
|
||||
cells.forEach(cell => {
|
||||
cell.addEventListener('click', function(e) {
|
||||
cells.forEach(cell => {
|
||||
cell.classList.remove('selectedPosition'); // Remove selected class from all cells
|
||||
cell.classList.remove('selectedHovered'); // Also remove selectedHovered class
|
||||
});
|
||||
let selectedLocation = e.target.id;
|
||||
inputField.value = selectedLocation;
|
||||
e.target.classList.add('selectedPosition'); // Add selected class to clicked cell
|
||||
e.target.classList.add('selectedHovered'); // Add selectedHovered class
|
||||
});
|
||||
|
||||
cell.addEventListener('mouseenter', function(e) {
|
||||
if(e.target.classList.contains('selectedPosition')) {
|
||||
e.target.classList.add('selectedHovered');
|
||||
}
|
||||
});
|
||||
|
||||
cell.addEventListener('mouseleave', function(e) {
|
||||
if(e.target.classList.contains('selectedPosition')) {
|
||||
e.target.classList.remove('selectedHovered');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{adjustContrast.title}, header=#{adjustContrast.header})}"></th:block>
|
||||
<style>
|
||||
#flex-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
#sliders-container {
|
||||
padding: 0 20px; /* Add some padding to separate sliders from canvas */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-12 bg-card">
|
||||
<form th:action="@{''}">
|
||||
<div class="row justify-content-center">
|
||||
|
||||
<div class="col-md-3">
|
||||
<div id="sliders-container" style="display:none;">
|
||||
<h4><span th:text="#{adjustContrast.contrast}"></span> <span id="contrast-val">100</span>%</h4>
|
||||
<input type="range" min="0" max="200" value="100" id="contrast-slider">
|
||||
|
||||
<h4><span th:text="#{adjustContrast.brightness}"></span> <span id="brightness-val">100</span>%</h4>
|
||||
<input type="range" min="0" max="200" value="100" id="brightness-slider">
|
||||
|
||||
<h4><span th:text="#{adjustContrast.saturation}"></span> <span id="saturation-val">100</span>%</h4>
|
||||
<input type="range" min="0" max="200" value="100" id="saturation-slider">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon advance">palette</span>
|
||||
<span class="tool-header-text" th:text="#{adjustContrast.header}"></span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf', remoteCall='false')}"></div>
|
||||
</div>
|
||||
<br>
|
||||
<canvas id="contrast-pdf-canvas"></canvas>
|
||||
<div class="mb-3 text-left">
|
||||
<button id="download-button" class="btn btn-primary" th:text="#{adjustContrast.download}"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<script type="module" th:src="@{'/js/pages/adjust-contrast.js'}"></script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{autoCrop.title}, header=#{autoCrop.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon advance">crop</span>
|
||||
<span class="tool-header-text" th:text="#{autoCrop.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/misc/auto-crop'}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{autoCrop.submit}"></button>
|
||||
</form>
|
||||
<p class="mt-3" th:text="#{autoCrop.credit}"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{auto-rename.title}, header=#{auto-rename.header})}"></th:block>
|
||||
</head>
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span >
|
||||
<svg class="tool-header-icon advance">
|
||||
<use xlink:href="/images/rename.svg#icon-rename"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="tool-header-text" th:text="#{auto-rename.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/misc/auto-rename'}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{auto-rename.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,98 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{changeMetadata.title}, header=#{changeMetadata.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon other">assignment</span>
|
||||
<span class="tool-header-text" th:text="#{changeMetadata.header}"></span>
|
||||
</div>
|
||||
<form method="post" id="form1" enctype="multipart/form-data" th:action="@{'/api/v1/misc/update-metadata'}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<p class="text-muted" th:text="#{changeMetadata.selectText.1}"></p>
|
||||
<div class="form-check mb-3-inline ms-3">
|
||||
<input type="checkbox" id="deleteAll" name="deleteAll">
|
||||
<label for="deleteAll" th:text="#{changeMetadata.selectText.2}" ></label>
|
||||
</div>
|
||||
<div class="form-check mb-3-inline ms-3">
|
||||
<input type="checkbox" id="customModeCheckbox">
|
||||
<label for="customModeCheckbox" th:text="#{changeMetadata.selectText.3}"></label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-check-label" for="author" th:text="#{changeMetadata.author}"></label>
|
||||
<input type="text" class="form-control" id="author" name="author">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-check-label" for="creationDate" th:text="#{changeMetadata.creationDate}"></label>
|
||||
<input type="text" class="form-control" id="creationDate" name="creationDate" placeholder="2020/12/25 18:30:59">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-check-label" for="creator" th:text="#{changeMetadata.creator}"></label>
|
||||
<input type="text" class="form-control" id="creator" name="creator">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-check-label" for="keywords" th:text="#{changeMetadata.keywords}"></label>
|
||||
<input type="text" class="form-control" id="keywords" name="keywords">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-check-label" for="modificationDate" th:text="#{changeMetadata.modDate}"></label>
|
||||
<input type="text" class="form-control" id="modificationDate" name="modificationDate" placeholder="2020/12/25 18:30:59">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-check-label" for="producer" th:text="#{changeMetadata.producer}"></label>
|
||||
<input type="text" class="form-control" id="producer" name="producer">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-check-label" for="subject" th:text="#{changeMetadata.subject}"></label>
|
||||
<input type="text" class="form-control" id="subject" name="subject">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-check-label" for="title" th:text="#{changeMetadata.title}"></label>
|
||||
<input type="text" class="form-control" id="title" name="title">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-check-label" for="trapped" th:text="#{changeMetadata.trapped}"></label>
|
||||
<select class="form-control" id="trapped" name="trapped">
|
||||
<option value="True" th:text="#{true}"></option>
|
||||
<option value="False" th:text="#{false}" selected></option>
|
||||
<option value="Unknown" th:text="#{unknown}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="customMetadata" style="display: none;">
|
||||
<h3 th:text="#{changeMetadata.selectText.4}"></h3>
|
||||
<div class="mb-3" id="otherMetadataEntries"></div>
|
||||
</div>
|
||||
<div id="customMetadataEntries"></div>
|
||||
<button type="button" class="btn btn-secondary" id="addMetadataBtn" th:text="#{changeMetadata.selectText.5}"></button>
|
||||
<br>
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{changeMetadata.submit}"></button>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<script type="module" th:src="@{'/js/pages/change-metadata.js'}">
|
||||
</script>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,256 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{compare.title}, header=#{compare.header})}"></th:block>
|
||||
<style>
|
||||
.result-column {
|
||||
border: 1px solid #ccc;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 400px);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.color-selector {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 50%;
|
||||
max-height: 100px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
#color-box1,
|
||||
#color-box2 {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.spacer1 {
|
||||
padding-right: calc(var(--bs-gutter-x) * .5);
|
||||
}
|
||||
|
||||
.spacer2 {
|
||||
padding-left: calc(var(--bs-gutter-x) * .5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-9 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon other">compare</span>
|
||||
<span class="tool-header-text" th:text="#{compare.header}"></span>
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', disableMultipleFiles=true, multipleInputsForSingleRequest=false, accept='application/pdf', remoteCall='false')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput2', disableMultipleFiles=true, multipleInputsForSingleRequest=false, accept='application/pdf', remoteCall='false')}">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="flex-container">
|
||||
<div class="color-selector spacer1">
|
||||
<label th:text="#{compare.highlightColor.1}"></label>
|
||||
<label for="color-box1"></label><input type="color" id="color-box1" value="#ff0000">
|
||||
</div>
|
||||
<div class="color-selector spacer2">
|
||||
<label th:text="#{compare.highlightColor.2}"></label>
|
||||
<label for="color-box2"></label><input type="color" id="color-box2" value="#008000">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick="comparePDFs()" th:text="#{compare.submit}"></button>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3 th:text="#{compare.document.1}"></h3>
|
||||
<div id="result1" class="result-column"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h3 th:text="#{compare.document.2}"></h3>
|
||||
<div id="result2" class="result-column"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<script th:inline="javascript">
|
||||
// get the elements
|
||||
var result1 = document.getElementById('result1');
|
||||
var result2 = document.getElementById('result2');
|
||||
|
||||
// add event listeners
|
||||
result1.addEventListener('scroll', function () {
|
||||
result2.scrollTop = result1.scrollTop;
|
||||
});
|
||||
|
||||
result2.addEventListener('scroll', function () {
|
||||
result1.scrollTop = result2.scrollTop;
|
||||
});
|
||||
async function comparePDFs() {
|
||||
const file1 = document.getElementById("fileInput-input").files[0];
|
||||
const file2 = document.getElementById("fileInput2-input").files[0];
|
||||
var color1 = document.getElementById('color-box1').value;
|
||||
var color2 = document.getElementById('color-box2').value;
|
||||
|
||||
const complexMessage = /*[[#{compare.complex.message}]]*/ 'One or both of the provided documents are large files, accuracy of comparison may be reduced';
|
||||
const largeFilesMessage = /*[[#{compare.large.file.message}]]*/ 'One or Both of the provided documents are too large to process';
|
||||
const noTextMessage = /*[[#{compare.no.text.message}]]*/ 'One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison."';
|
||||
|
||||
if (!file1 || !file2) {
|
||||
console.error("Please select two PDF files to compare");
|
||||
return;
|
||||
}
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||
|
||||
const [pdf1, pdf2] = await Promise.all([
|
||||
pdfjsLib.getDocument(URL.createObjectURL(file1)).promise,
|
||||
pdfjsLib.getDocument(URL.createObjectURL(file2)).promise
|
||||
]);
|
||||
|
||||
const extractText = async (pdf) => {
|
||||
const pages = [];
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const content = await page.getTextContent();
|
||||
const strings = content.items.map(item => item.str);
|
||||
pages.push(strings.join(" "));
|
||||
}
|
||||
return pages.join(" ");
|
||||
};
|
||||
|
||||
const [text1, text2] = await Promise.all([
|
||||
extractText(pdf1),
|
||||
extractText(pdf2)
|
||||
]);
|
||||
|
||||
if (text1.trim() === "" || text2.trim() === "") {
|
||||
alert(noTextMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const resultDiv1 = document.getElementById("result1");
|
||||
const resultDiv2 = document.getElementById("result2");
|
||||
const loading = /*[[#{loading}]]*/ 'Loading...';
|
||||
|
||||
resultDiv1.innerHTML = loading;
|
||||
resultDiv2.innerHTML = loading;
|
||||
|
||||
// Create a new Worker
|
||||
const worker = new Worker('./js/compare/pdfWorker.js');
|
||||
|
||||
|
||||
// Post messages to the worker
|
||||
worker.postMessage({
|
||||
type: 'SET_COMPLEX_MESSAGE',
|
||||
message: complexMessage
|
||||
});
|
||||
|
||||
worker.postMessage({
|
||||
type: 'SET_TOO_LARGE_MESSAGE',
|
||||
message: largeFilesMessage
|
||||
});
|
||||
|
||||
// Error handling for the worker
|
||||
worker.onerror = function (error) {
|
||||
console.error('Worker error:', error);
|
||||
};
|
||||
worker.onmessage = function (e) {
|
||||
const { status, differences, message } = e.data;
|
||||
if (status === 'error') {
|
||||
|
||||
resultDiv1.innerHTML = '';
|
||||
resultDiv2.innerHTML = '';
|
||||
alert(message);
|
||||
return;
|
||||
}
|
||||
if (status === 'success' && differences) {
|
||||
console.log('Differences:', differences);
|
||||
displayDifferences(differences);
|
||||
}
|
||||
if (event.data.status === 'warning') {
|
||||
console.warn(event.data.message);
|
||||
alert(event.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
worker.postMessage({ text1, text2, color1, color2 });
|
||||
|
||||
const displayDifferences = (differences) => {
|
||||
const resultDiv1 = document.getElementById("result1");
|
||||
const resultDiv2 = document.getElementById("result2");
|
||||
resultDiv1.innerHTML = "";
|
||||
resultDiv2.innerHTML = "";
|
||||
|
||||
differences.forEach(([color, word]) => {
|
||||
const span1 = document.createElement("span");
|
||||
const span2 = document.createElement("span");
|
||||
|
||||
if (color === color2) {
|
||||
span1.style.color = "transparent";
|
||||
span1.style.userSelect = "none";
|
||||
span2.style.color = color;
|
||||
}
|
||||
// If it's a deletion, show it in in the first document and transparent in the second
|
||||
else if (color === color1) {
|
||||
span1.style.color = color;
|
||||
span2.style.color = "transparent";
|
||||
span2.style.userSelect = "none";
|
||||
}
|
||||
// If it's unchanged, show it in black in both
|
||||
else {
|
||||
span1.style.color = color;
|
||||
span2.style.color = color;
|
||||
}
|
||||
|
||||
span1.textContent = word;
|
||||
span2.textContent = word;
|
||||
resultDiv1.appendChild(span1);
|
||||
resultDiv2.appendChild(span2);
|
||||
|
||||
// Add space after each word, or a new line if the word ends with a full stop
|
||||
const spaceOrNewline1 = document.createElement("span");
|
||||
const spaceOrNewline2 = document.createElement("span");
|
||||
if (word.endsWith(".")) {
|
||||
spaceOrNewline1.innerHTML = "<br>";
|
||||
spaceOrNewline2.innerHTML = "<br>";
|
||||
} else {
|
||||
spaceOrNewline1.textContent = " ";
|
||||
spaceOrNewline2.textContent = " ";
|
||||
}
|
||||
resultDiv1.appendChild(spaceOrNewline1);
|
||||
resultDiv2.appendChild(spaceOrNewline2);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{compress.title}, header=#{compress.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon advance">zoom_in_map</span>
|
||||
<span class="tool-header-text" th:text="#{compress.header}"></span>
|
||||
</div>
|
||||
<form action="#" th:action="@{'/api/v1/misc/compress-pdf'}" method="post" enctype="multipart/form-data">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h4 th:text="#{compress.selectText.1}"></h4>
|
||||
<p th:utext="#{compress.selectText.1.1}"></p>
|
||||
<label for="optimizeLevel" th:text="#{compress.selectText.2}"></label>
|
||||
<select name="optimizeLevel" id="optimizeLevel" class="form-control">
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5" selected>5</option>
|
||||
<option value="6">6</option>
|
||||
<option value="7">7</option>
|
||||
<option value="8">8</option>
|
||||
<option value="9">9</option>
|
||||
</select>
|
||||
|
||||
<div class="form-check mt-3">
|
||||
<input class="form-check-input" type="checkbox" name="grayscale" id="grayscaleCheck" value="true">
|
||||
<label class="form-check-label" for="grayscaleCheck" th:text="#{compress.grayscale.label}">
|
||||
Convert images to grayscale for better compression
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h4 th:text="#{compress.selectText.4}"></h4>
|
||||
<label for="expectedOutputSize" th:text="#{compress.selectText.5}"></label>
|
||||
<input type="text" name="expectedOutputSize" id="expectedOutputSize" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{compress.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{home.ScannerImageSplit.title}, header=#{home.ScannerImageSplit.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon advance">scanner</span>
|
||||
<span class="tool-header-text" th:text="#{home.ScannerImageSplit.title}"></span>
|
||||
</div>
|
||||
|
||||
<p th:if="${!isPython}" th:text="#{ScannerImageSplit.info}" class="alert alert-success text-center">Python is not installed. It is required to run.</p>
|
||||
|
||||
<form th:if="${isPython}" id="multiPdfForm" th:action="@{'/api/v1/misc/extract-image-scans'}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='image/*, application/pdf')}"></div>
|
||||
<div class="mb-3">
|
||||
<label for="angleThreshold" th:text="#{ScannerImageSplit.selectText.1}"></label>
|
||||
<input type="number" class="form-control" id="angleThreshold" name="angle_threshold" value="10">
|
||||
<small id="angleThresholdHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.2}"></small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="tolerance" th:text="#{ScannerImageSplit.selectText.3}"></label>
|
||||
<input type="number" class="form-control" id="tolerance" name="tolerance" value="20">
|
||||
<small id="toleranceHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.4}"></small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="minArea" th:text="#{ScannerImageSplit.selectText.5}"></label>
|
||||
<input type="number" class="form-control" id="minArea" name="min_area" value="8000">
|
||||
<small id="minAreaHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.6}"></small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="minContourArea" th:text="#{ScannerImageSplit.selectText.7}"></label>
|
||||
<input type="number" class="form-control" id="minContourArea" name="min_contour_area" value="500">
|
||||
<small id="minContourAreaHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.8}"></small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="borderSize" th:text="#{ScannerImageSplit.selectText.9}"></label>
|
||||
<input type="number" class="form-control" id="borderSize" name="border_size" value="1">
|
||||
<small id="borderSizeHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.10}"></small>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{genericSubmit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{extractImages.title}, header=#{extractImages.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon other">photo_library</span>
|
||||
<span class="tool-header-text" th:text="#{extractImages.header}"></span>
|
||||
</div>
|
||||
<form id="multiPdfForm" th:action="@{'/api/v1/misc/extract-images'}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
<div class="mb-3">
|
||||
<input type="checkbox" name="allowDuplicates" id="allowDuplicates">
|
||||
<label for="allowDuplicates" th:text="#{extractImages.allowDuplicates}"></label>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{extractImages.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,163 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{fakeScan.title}, header=#{fakeScan.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon advance">scanner</span>
|
||||
<span class="tool-header-text" th:text="#{fakeScan.title}"></span>
|
||||
</div>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" id="uploadForm" th:action="@{'/api/v1/misc/fake-scan'}">
|
||||
<input type="hidden" name="advancedEnabled" id="advancedEnabled" value="false">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<br>
|
||||
<div class="mb-3">
|
||||
<label for="quality" class="form-label" th:text="#{fakeScan.quality}"></label>
|
||||
<select class="form-select" id="quality" name="quality">
|
||||
<option value="low" th:text="#{fakeScan.quality.low}"></option>
|
||||
<option value="medium" th:text="#{fakeScan.quality.medium}"></option>
|
||||
<option value="high" th:text="#{fakeScan.quality.high}" selected></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="rotation" class="form-label" th:text="#{fakeScan.rotation}"></label>
|
||||
<select class="form-select" id="rotation" name="rotation">
|
||||
<option value="none" th:text="#{fakeScan.rotation.none}"></option>
|
||||
<option value="slight" th:text="#{fakeScan.rotation.slight}" selected></option>
|
||||
<option value="moderate" th:text="#{fakeScan.rotation.moderate}"></option>
|
||||
<option value="severe" th:text="#{fakeScan.rotation.severe}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="advancedSettingsToggle">
|
||||
<label class="form-check-label" for="advancedSettingsToggle" th:text="#{fakeScan.advancedSettings}"></label>
|
||||
</div>
|
||||
<div id="advancedSettings" style="display:none; border:1px solid #eee; padding:1em; border-radius:8px; margin-bottom:1em;">
|
||||
<div class="mb-3">
|
||||
<label for="colorspace" class="form-label" th:text="#{fakeScan.colorspace}"></label>
|
||||
<select class="form-select" id="colorspace" name="colorspace">
|
||||
<option value="grayscale" th:text="#{fakeScan.colorspace.grayscale}" selected></option>
|
||||
<option value="color" th:text="#{fakeScan.colorspace.color}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="border" class="form-label" th:text="#{fakeScan.border}"></label>
|
||||
<input type="number" class="form-control" id="border" name="border" min="0" max="100" value="20">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="rotate" class="form-label" th:text="#{fakeScan.rotate}"></label>
|
||||
<input type="number" class="form-control" id="rotate" name="rotate" min="-15" max="15" value="0">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="rotateVariance" class="form-label" th:text="#{fakeScan.rotateVariance}"></label>
|
||||
<input type="number" class="form-control" id="rotateVariance" name="rotateVariance" min="0" max="10" value="2">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="brightness" class="form-label" th:text="#{fakeScan.brightness}"></label>
|
||||
<input type="range" class="form-range" id="brightness" name="brightness" min="0.5" max="1.5" step="0.01" value="1.0">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="contrast" class="form-label" th:text="#{fakeScan.contrast}"></label>
|
||||
<input type="range" class="form-range" id="contrast" name="contrast" min="0.5" max="1.5" step="0.01" value="1.0">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="blur" class="form-label" th:text="#{fakeScan.blur}"></label>
|
||||
<input type="range" class="form-range" id="blur" name="blur" min="0" max="5" step="0.1" value="1.0">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="noise" class="form-label" th:text="#{fakeScan.noise}"></label>
|
||||
<input type="range" class="form-range" id="noise" name="noise" min="0" max="20" step="0.1" value="8.0">
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="yellowish" name="yellowish">
|
||||
<label class="form-check-label" for="yellowish" th:text="#{fakeScan.yellowish}"></label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="resolution" class="form-label" th:text="#{fakeScan.resolution}"></label>
|
||||
<input type="number" class="form-control" id="resolution" name="resolution" min="72" max="600" value="300">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 text-left">
|
||||
<button type="submit" class="btn btn-primary" th:text="#{fakeScan.submit}" id="submitBtn"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
<script th:src="@{'/js/fetch-utils.js'}"></script>
|
||||
<script th:inline="javascript">
|
||||
// Show/hide advanced settings
|
||||
document.getElementById('advancedSettingsToggle').addEventListener('change', function() {
|
||||
document.getElementById('advancedSettings').style.display = this.checked ? 'block' : 'none';
|
||||
});
|
||||
|
||||
// Form submission handling
|
||||
const form = document.getElementById('uploadForm');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
// If advanced settings are not enabled, remove advanced fields
|
||||
if (!document.getElementById('advancedSettingsToggle').checked) {
|
||||
const formData = new FormData(form);
|
||||
formData.delete('colorspace');
|
||||
formData.delete('border');
|
||||
formData.delete('rotate');
|
||||
formData.delete('rotateVariance');
|
||||
formData.delete('brightness');
|
||||
formData.delete('contrast');
|
||||
formData.delete('blur');
|
||||
formData.delete('noise');
|
||||
formData.delete('yellowish');
|
||||
formData.delete('resolution');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize advanced settings state
|
||||
const advancedSettingsToggle = document.getElementById('advancedSettingsToggle');
|
||||
const advancedSettings = document.getElementById('advancedSettings');
|
||||
if (advancedSettingsToggle && advancedSettings) {
|
||||
// Helper: enable/disable all fields in advanced settings
|
||||
function setAdvancedFieldsEnabled(enabled) {
|
||||
const fields = advancedSettings.querySelectorAll('input, select');
|
||||
fields.forEach(field => {
|
||||
field.disabled = !enabled;
|
||||
// If enabling and value is empty, set to default
|
||||
if (enabled && (field.value === '' || field.value == null)) {
|
||||
if (field.type === 'number' || field.type === 'range') {
|
||||
field.value = field.defaultValue;
|
||||
} else if (field.type === 'checkbox') {
|
||||
field.checked = field.defaultChecked;
|
||||
} else if (field.tagName === 'SELECT') {
|
||||
field.value = field.querySelector('option[selected]')?.value || field.options[0].value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Set initial state
|
||||
setAdvancedFieldsEnabled(advancedSettingsToggle.checked);
|
||||
advancedSettings.style.display = advancedSettingsToggle.checked ? 'block' : 'none';
|
||||
document.getElementById('advancedEnabled').value = advancedSettingsToggle.checked ? 'true' : 'false';
|
||||
// Add change listener
|
||||
advancedSettingsToggle.addEventListener('change', function() {
|
||||
advancedSettings.style.display = this.checked ? 'block' : 'none';
|
||||
setAdvancedFieldsEnabled(this.checked);
|
||||
document.getElementById('advancedEnabled').value = this.checked ? 'true' : 'false';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{flatten.title}, header=#{flatten.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon other">layers_clear</span>
|
||||
<span class="tool-header-text" th:text="#{flatten.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/misc/flatten'}" id="pdfForm" class="mb-3">
|
||||
<div class="custom-file">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="flattenOnlyForms" name="flattenOnlyForms">
|
||||
<label for="flattenOnlyForms" th:text="#{flatten.flattenOnlyForms}" ></label>
|
||||
</div>
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{flatten.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,240 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{ocr.title}, header=#{ocr.header})}"></th:block>
|
||||
<style>
|
||||
#languages {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--md-sys-color-surface-3);
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function handleLangSelection() {
|
||||
let checkboxes = document.getElementsByName("languages");
|
||||
let selected = false;
|
||||
for (let i = 0; i < checkboxes.length; i++) {
|
||||
if (checkboxes[i].checked) {
|
||||
selected = true;
|
||||
checkboxes[i].setAttribute('required', 'false');
|
||||
}
|
||||
}
|
||||
if (selected) {
|
||||
for (let i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].removeAttribute('required');
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (let i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].setAttribute('required', 'true');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon other">quick_reference_all</span>
|
||||
<span class="tool-header-text" th:text="#{ocr.header}"></span>
|
||||
</div>
|
||||
<form th:if="${#lists.size(languages) > 0}" action="#" th:action="@{'/api/v1/misc/ocr-pdf'}" method="post" enctype="multipart/form-data" class="mb-3">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<div class="mb-3">
|
||||
<label for="languages" class="form-label" th:text="#{ocr.selectText.1}"></label>
|
||||
<hr>
|
||||
<div id="languages">
|
||||
<div class="form-check" th:each="language, iterStat : ${languages}">
|
||||
<input type="checkbox" th:name="languages" th:value="${language}" required th:id="${'language-' + language}" onchange="handleLangSelection()" />
|
||||
<label th:for="${'language-' + language}" th:text="${language}"></label>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label th:text="#{ocr.selectText.10}"></label>
|
||||
<select class="form-control" name="ocrType">
|
||||
<option value="skip-text" th:text="#{ocr.selectText.6}"></option>
|
||||
<option selected value="force-ocr" th:text="#{ocr.selectText.7}"></option>
|
||||
<option value="Normal" th:text="#{ocr.selectText.8}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<br>
|
||||
<label for="languages" class="form-label" th:text="#{ocr.selectText.9}"></label>
|
||||
<div class="mb-3">
|
||||
<label th:text="#{ocr.selectText.12}"></label>
|
||||
<select class="form-control" name="ocrRenderType">
|
||||
<option value="hocr">HOCR (Latin/Roman alphabet only)</option>
|
||||
<option value="sandwich">Sandwich</option>
|
||||
</select>
|
||||
</div>
|
||||
<br>
|
||||
<div class="mb-3" th:if="${@endpointConfiguration.isGroupEnabled('OCRmyPDF')}">
|
||||
<label class="form-label">OCR Options</label>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="sidecar" name="sidecar" value="true">
|
||||
<label class="form-check-label" for="sidecar">Include OCR text in sidecar text file</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="deskew" name="deskew" value="true">
|
||||
<label class="form-check-label" for="deskew">Deskew input file</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="clean" name="clean" value="true">
|
||||
<label class="form-check-label" for="clean">Clean input file</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="cleanFinal" name="cleanFinal" value="true">
|
||||
<label class="form-check-label" for="cleanFinal">Clean final output</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="removeImagesAfter" name="removeImagesAfter" value="true">
|
||||
<label class="form-check-label" for="removeImagesAfter">Remove images from output PDF</label>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{ocr.submit}"></button>
|
||||
</form>
|
||||
<script th:inline="javascript">
|
||||
// Get language translations from Thymeleaf messages
|
||||
const languageTranslations = {};
|
||||
/*[# th:each="lang : ${languages}"]*/
|
||||
languageTranslations['[(${lang})]'] = /*[[#{${'lang.' + lang}}]]*/[(${lang})];
|
||||
/*[/]*/
|
||||
|
||||
// Map two-letter language codes to Tesseract language codes
|
||||
const localeToTesseract = {
|
||||
'en': 'eng',
|
||||
'fr': 'fra',
|
||||
'de': 'deu',
|
||||
'es': 'spa',
|
||||
'it': 'ita',
|
||||
'pt': 'por',
|
||||
'ru': 'rus',
|
||||
'zh': 'chi_sim',
|
||||
'ja': 'jpn',
|
||||
'ko': 'kor',
|
||||
'ar': 'ara',
|
||||
'hi': 'hin',
|
||||
'nl': 'nld',
|
||||
'cs': 'ces',
|
||||
'pl': 'pol',
|
||||
'tr': 'tur',
|
||||
'uk': 'ukr',
|
||||
'vi': 'vie',
|
||||
'sv': 'swe',
|
||||
'no': 'nor',
|
||||
'fi': 'fin',
|
||||
'da': 'dan',
|
||||
'el': 'ell',
|
||||
'he': 'heb',
|
||||
'hu': 'hun',
|
||||
'bg': 'bul',
|
||||
'ro': 'ron',
|
||||
'hr': 'hrv',
|
||||
'sk': 'slk',
|
||||
'id': 'ind',
|
||||
'th': 'tha',
|
||||
'sl': 'slv'
|
||||
};
|
||||
|
||||
// Function to get the translated language name
|
||||
function getTranslatedLanguageName(shortCode) {
|
||||
return languageTranslations[shortCode] || shortCode;
|
||||
}
|
||||
|
||||
// Function to prioritize languages based on browser locale
|
||||
function prioritizeLanguages() {
|
||||
const languageContainer = document.getElementById('languages');
|
||||
if (!languageContainer) return;
|
||||
|
||||
// Update all labels with translated language names
|
||||
const formChecks = Array.from(languageContainer.getElementsByClassName('form-check'));
|
||||
if (formChecks.length === 0) return;
|
||||
|
||||
formChecks.forEach(element => {
|
||||
const label = element.querySelector('label');
|
||||
if (label) {
|
||||
const langCode = label.getAttribute('for').split('-')[1];
|
||||
label.textContent = getTranslatedLanguageName(langCode);
|
||||
}
|
||||
});
|
||||
|
||||
// Get browser/UI language
|
||||
const browserLanguage = document.documentElement.lang || navigator.language || navigator.userLanguage;
|
||||
const uiLanguage = document.documentElement.getAttribute('data-language') || browserLanguage;
|
||||
|
||||
// Extract primary language code
|
||||
const primaryLanguageCode = (uiLanguage || '').split(/[-_]/)[0].toLowerCase();
|
||||
const tesseractPrimaryCode = localeToTesseract[primaryLanguageCode];
|
||||
|
||||
// Define priority language array
|
||||
const priorityLanguages = [];
|
||||
|
||||
// Primary language first
|
||||
if (tesseractPrimaryCode) {
|
||||
priorityLanguages.push(tesseractPrimaryCode);
|
||||
}
|
||||
|
||||
// English second (if not already added)
|
||||
if (tesseractPrimaryCode !== 'eng') {
|
||||
priorityLanguages.push('eng');
|
||||
}
|
||||
|
||||
// Sort the elements
|
||||
const sortedElements = formChecks.sort((a, b) => {
|
||||
const aInput = a.querySelector('input');
|
||||
const bInput = b.querySelector('input');
|
||||
|
||||
if (!aInput || !bInput) return 0;
|
||||
|
||||
const aLangCode = aInput.id.split('-')[1];
|
||||
const bLangCode = bInput.id.split('-')[1];
|
||||
|
||||
const aIsPriority = priorityLanguages.includes(aLangCode);
|
||||
const bIsPriority = priorityLanguages.includes(bLangCode);
|
||||
|
||||
if (aIsPriority && !bIsPriority) return -1;
|
||||
if (!aIsPriority && bIsPriority) return 1;
|
||||
|
||||
if (aIsPriority && bIsPriority) {
|
||||
// If both are priority, sort by priority order
|
||||
return priorityLanguages.indexOf(aLangCode) - priorityLanguages.indexOf(bLangCode);
|
||||
}
|
||||
|
||||
// For non-priority languages, sort alphabetically by full name
|
||||
return getTranslatedLanguageName(aLangCode).localeCompare(getTranslatedLanguageName(bLangCode));
|
||||
});
|
||||
|
||||
// Clear container and append sorted elements
|
||||
languageContainer.innerHTML = '';
|
||||
sortedElements.forEach(element => {
|
||||
languageContainer.appendChild(element);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
prioritizeLanguages();
|
||||
});
|
||||
</script>
|
||||
<p th:text="#{ocr.help}"></p>
|
||||
<a href="https://docs.stirlingpdf.com/Advanced%20Configuration/OCR">https://docs.stirlingpdf.com/Advanced%20Configuration/OCR</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{printFile.title}, header=#{printFile.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2 th:text="#{printFile.header}"></h2>
|
||||
<form action="#" th:action="@{'/api/v1/misc/print-file'}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf,image/*')}"></div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h4 th:text="#{printFile.selectText.1}">Select Printer</h4> <!-- Assuming the message code printFile.selectText.3 corresponds to "Select Printer" -->
|
||||
<label for="printerName" th:text="#{printFile.selectText.2}">Printer Name:</label> <!-- Assuming the message code printFile.selectText.4 corresponds to "Printer Name:" -->
|
||||
<input type="text" name="printerName" id="printerName" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{printFile.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{removeAnnotations.title}, header=#{removeAnnotations.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon other">thread_unread</span>
|
||||
<span class="tool-header-text" th:text="#{removeAnnotations.header}"></span>
|
||||
</div>
|
||||
<form id="pdfForm" th:action="@{''}" class="mb-3">
|
||||
<div class="custom-file">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf', remoteCall='false')}"></div>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removeAnnotations.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
<script th:src="@{'/js/local-pdf-input-download.js'}"></script>
|
||||
<script>
|
||||
document.getElementById('pdfForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { PDFDocument } = PDFLib;
|
||||
|
||||
const processFile = async (file) => {
|
||||
const origFileUrl = URL.createObjectURL(file);
|
||||
const formPdfBytes = await window.fetchWithCsrf(origFileUrl).then(res => res.arrayBuffer());
|
||||
const pdfDoc = await PDFDocument.load(formPdfBytes, { ignoreEncryption: true });
|
||||
|
||||
const pages = pdfDoc.getPages();
|
||||
|
||||
for (let i = 0; i < pages.length; ++i) {
|
||||
const page = pages[i];
|
||||
const annotations = page.node.Annots();
|
||||
if (!annotations) continue;
|
||||
const ctx = annotations.context;
|
||||
|
||||
for (let j = 0; j < annotations.size(); ++j) {
|
||||
const annotation = annotations.get(j);
|
||||
ctx.delete(annotation);
|
||||
}
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' });
|
||||
const fileName = (file.name ? file.name.replace('.pdf', '') : 'pdf') + '_removed_annotations.pdf';
|
||||
|
||||
return { processedData: pdfBlob, fileName };
|
||||
};
|
||||
|
||||
await downloadFilesWithCallback(processFile);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{removeBlanks.title}, header=#{removeBlanks.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon other">scan_delete</span>
|
||||
<span class="tool-header-text" th:text="#{removeBlanks.header}"></span>
|
||||
</div>
|
||||
<form id="multiPdfForm" th:action="@{'/api/v1/misc/remove-blanks'}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<div class="mb-3">
|
||||
<label for="threshold" th:text="#{removeBlanks.threshold}"></label>
|
||||
<input type="number" class="form-control" id="threshold" name="threshold" value="10" min="0" max="255" step="1">
|
||||
<small id="thresholdHelp" class="form-text text-muted" th:text="#{removeBlanks.thresholdDesc}"></small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="whitePercent" th:text="#{removeBlanks.whitePercent}"></label>
|
||||
<input type="number" class="form-control" id="whitePercent" name="whitePercent" value="99.9" step="0.1" min="0.1" max="100">
|
||||
<small id="whitePercentHelp" class="form-text text-muted" th:text="#{removeBlanks.whitePercentDesc}"></small>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removeBlanks.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{repair.title}, header=#{repair.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon advance">build</span>
|
||||
<span class="tool-header-text" th:text="#{repair.header}"></span>
|
||||
</div>
|
||||
<form id="multiPdfForm" th:action="@{'/api/v1/misc/repair'}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{repair.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{replace-color.title}, header=#{replace-color.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon advance">zoom_in_map</span>
|
||||
<span class="tool-header-text" th:text="#{replace-color.header}"></span>
|
||||
</div>
|
||||
<form action="#" th:action="@{'/api/v1/misc/replace-invert-pdf'}" method="post" enctype="multipart/form-data">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h4 th:text="#{replace-color.selectText.1}"></h4>
|
||||
<select name="replaceAndInvertOption" id="replace-invert" class="form-control">
|
||||
<option value="HIGH_CONTRAST_COLOR" th:text="#{replace-color.selectText.2}" ></option>
|
||||
<option value="CUSTOM_COLOR" th:text="#{replace-color.selectText.3}"></option>
|
||||
<option value="FULL_INVERSION" th:text="#{replace-color.selectText.4}" selected></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3" id="high-contrast-options" style="display: none">
|
||||
<div class="card-body">
|
||||
<h4 th:text="#{replace-color.selectText.5}"></h4>
|
||||
<select name="highContrastColorCombination" id="high-contrast" class="form-control">
|
||||
<option value="WHITE_TEXT_ON_BLACK" th:text="#{replace-color.selectText.6}" selected></option>
|
||||
<option value="BLACK_TEXT_ON_WHITE" th:text="#{replace-color.selectText.7}"></option>
|
||||
<option value="YELLOW_TEXT_ON_BLACK" th:text="#{replace-color.selectText.8}"></option>
|
||||
<option value="GREEN_TEXT_ON_BLACK" th:text="#{replace-color.selectText.9}"></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3" id = "custom-color-1" style="display: none">
|
||||
<div class="card-body">
|
||||
<h4 th:text="#{replace-color.selectText.10}"></h4>
|
||||
<input type="color" name="textColor" id="text-color" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3" id = "custom-color-2" style="display: none">
|
||||
<div class="card-body">
|
||||
<h4 th:text="#{replace-color.selectText.11}"></h4>
|
||||
<input type="color" name="backGroundColor" id="bg-color" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{replace-color.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$('#replace-invert').change(function() {
|
||||
var selectedOption = $(this).val();
|
||||
|
||||
// Hide all conditional fields by default
|
||||
$('#high-contrast-options').hide();
|
||||
$('#custom-color-1').hide();
|
||||
$('#custom-color-2').hide();
|
||||
|
||||
if (selectedOption === "HIGH_CONTRAST_COLOR") {
|
||||
$('#high-contrast-options').show();
|
||||
} else if (selectedOption === "CUSTOM_COLOR") {
|
||||
$('#custom-color-1').show();
|
||||
$('#custom-color-2').show();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{showJS.title}, header=#{showJS.header})}"></th:block>
|
||||
<link th:href="@{'/css/prism.css'}" rel="stylesheet">
|
||||
<script th:src="@{'/js/thirdParty/prism.js'}"></script>
|
||||
<style>
|
||||
/* Add a max-height and make it scrollable */
|
||||
#script-content {
|
||||
max-height: 1000px; /* Adjust this to your preferred maximum height */
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon advance">javascript</span>
|
||||
<span class="tool-header-text" th:text="#{showJS.header}"></span>
|
||||
</div>
|
||||
<form id="pdfInfoForm" method="post" enctype="multipart/form-data" th:action="@{'/show-javascript'}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, remoteCall='false', accept='application/pdf')}"></div>
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{showJS.submit}"></button>
|
||||
</form>
|
||||
<div class="container mt-4">
|
||||
<!-- Iterate over each main section in the JSON -->
|
||||
<div id="script-content">
|
||||
<!-- JavaScript will populate this section -->
|
||||
</div>
|
||||
|
||||
<!-- Button to download the JSON -->
|
||||
<a href="#" id="downloadJS" class="btn btn-primary mt-3" style="display: none;" th:text="#{showJS.downloadJS}">Download JSON</a>
|
||||
</div>
|
||||
<script th:src="@{'/js/fetch-utils.js'}"></script>
|
||||
<script>
|
||||
document.querySelector('#pdfInfoForm').addEventListener('submit', function(event){
|
||||
event.preventDefault();
|
||||
|
||||
// Fetch the formData
|
||||
const formData = new FormData(event.target);
|
||||
|
||||
fetchWithCsrf('api/v1/misc/show-javascript', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(response => response.text())
|
||||
.then(data => {
|
||||
// Escape < and > characters
|
||||
let escapedData = data.replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
// Create the elements manually
|
||||
let preElement = document.createElement('pre');
|
||||
let codeElement = document.createElement('code');
|
||||
codeElement.classList.add('language-javascript');
|
||||
codeElement.textContent = escapedData; // Use textContent instead of innerHTML
|
||||
preElement.appendChild(codeElement);
|
||||
|
||||
let scriptContent = document.querySelector('#script-content');
|
||||
// Clear existing content, if any
|
||||
while (scriptContent.firstChild) {
|
||||
scriptContent.removeChild(scriptContent.firstChild);
|
||||
}
|
||||
scriptContent.appendChild(preElement);
|
||||
|
||||
// Highlight the code using Prism.js
|
||||
Prism.highlightAll();
|
||||
|
||||
// Create a blob object from the data and create a URL for it
|
||||
let blob = new Blob([data], {type: 'application/javascript'});
|
||||
let url = URL.createObjectURL(blob);
|
||||
|
||||
// Set the URL as the href of the download button and provide a download name
|
||||
let downloadButton = document.querySelector('#downloadJS');
|
||||
downloadButton.href = url;
|
||||
downloadButton.download = 'extracted.js';
|
||||
downloadButton.style.display = 'block';
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,203 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{AddStampRequest.title}, header=#{AddStampRequest.header})}"></th:block>
|
||||
<style>
|
||||
.a4container {
|
||||
position: relative;
|
||||
width: 50%;
|
||||
aspect-ratio: 0.707/1;
|
||||
border: 1px solid #ddd;
|
||||
box-sizing: border-box;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon security">approval</span>
|
||||
<span class="tool-header-text" th:text="#{AddStampRequest.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/misc/add-stamp'}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<br>
|
||||
<div class="mb-3">
|
||||
<label for="pageOrder" th:text="#{pageSelectionPrompt}"></label>
|
||||
<input type="text" class="form-control" id="pageOrder" name="pageNumbers" value="1" placeholder="(e.g. 1,3,2 or 4-8,2,10-12 or 2n-1)" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="customMargin" class="form-label" th:text="#{AddStampRequest.customMargin}">Custom Margin</label>
|
||||
<select class="form-select" id="customMargin" name="customMargin">
|
||||
<option value="small" th:text="#{sizes.small}"></option>
|
||||
<option value="medium" selected th:text="#{sizes.medium}"></option>
|
||||
<option value="large" th:text="#{sizes.large}"></option>
|
||||
<option value="x-large" th:text="#{sizes.x-large}"></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label th:text="#{AddStampRequest.position}"></label>
|
||||
<div class="a4container">
|
||||
<div class="pageNumber" id="1" style="top: 10%; left: 10%;">1</div>
|
||||
<div class="pageNumber" id="2" style="top: 10%; left: 50%;">2</div>
|
||||
<div class="pageNumber" id="3" style="top: 10%; left: 90%;">3</div>
|
||||
<div class="pageNumber" id="4" style="top: 50%; left: 10%;">4</div>
|
||||
<div class="pageNumber" id="5" style="top: 50%; left: 50%;">5</div>
|
||||
<div class="pageNumber" id="6" style="top: 50%; left: 90%;">6</div>
|
||||
<div class="pageNumber" id="7" style="top: 90%; left: 10%;">7</div>
|
||||
<div class="pageNumber selectedPosition" id="8" style="top: 90%; left: 50%;">8</div>
|
||||
<div class="pageNumber" id="9" style="top: 90%; left: 90%;">9</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="numberInput" name="position" value="8">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="stampType" class="form-label" th:text="#{AddStampRequest.stampType}">Stamp Type</label>
|
||||
<select class="form-select" id="stampType" name="stampType" onchange="toggleFileOption()">
|
||||
<option value="text" th:text="#{watermark.type.1}">Text</option>
|
||||
<option value="image" th:text="#{watermark.type.2}">Image</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="stampTextGroup" class="mb-3">
|
||||
<label for="stampText" class="form-label" th:text="#{AddStampRequest.stampText}">Stamp Text</label>
|
||||
<input type="text" class="form-control" id="stampText" name="stampText">
|
||||
</div>
|
||||
|
||||
<div id="stampImageGroup" class="mb-3" style="display: none;">
|
||||
<label for="stampImage" class="form-label" th:text="#{AddStampRequest.stampImage}">Stamp Image</label>
|
||||
<input type="file" class="form-control" id="stampImage" name="stampImage" accept="image/*" >
|
||||
</div>
|
||||
|
||||
<div id="alphabetGroup" class="mb-3">
|
||||
<label for="alphabet" class="form-label" th:text="#{AddStampRequest.alphabet}">Alphabet</label>
|
||||
<select class="form-select" id="alphabet" name="alphabet">
|
||||
<option value="roman">Roman</option>
|
||||
<option value="arabic">العربية</option>
|
||||
<option value="japanese">日本語</option>
|
||||
<option value="korean">한국어</option>
|
||||
<option value="chinese">简体中文</option>
|
||||
<option value="thai">ไทย</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="fontSize" class="form-label" th:text="#{AddStampRequest.fontSize}">Font Size</label>
|
||||
<input type="number" class="form-control" id="fontSize" name="fontSize" value="30">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="rotation" class="form-label" th:text="#{AddStampRequest.rotation}">Rotation</label>
|
||||
<input type="number" class="form-control" id="rotation" name="rotation" value="0">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="opacity" class="form-label" th:text="#{AddStampRequest.opacity}">Opacity</label>
|
||||
<input type="number" class="form-control" id="opacity" name="opacity" step="0.1" value="0.5">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="overrideX" class="form-label" th:text="#{AddStampRequest.overrideX}">Override X</label>
|
||||
<input type="number" class="form-control" id="overrideX" name="overrideX" value="-1">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="overrideY" class="form-label" th:text="#{AddStampRequest.overrideY}">Override Y</label>
|
||||
<input type="number" class="form-control" id="overrideY" name="overrideY" value="-1">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="customColor" class="form-label" th:text="#{AddStampRequest.customColor}">Custom Color</label>
|
||||
<div class="form-control form-control-color" style="background-color: #d3d3d3;">
|
||||
<input type="color" id="customColor" name="customColor" value="#d3d3d3">
|
||||
</div>
|
||||
<script>
|
||||
let colorInput = document.getElementById("customColor");
|
||||
if (colorInput) {
|
||||
let colorInputContainer = colorInput.parentElement;
|
||||
if (colorInputContainer) {
|
||||
colorInput.onchange = function() {
|
||||
colorInputContainer.style.backgroundColor = colorInput.value;
|
||||
}
|
||||
colorInputContainer.style.backgroundColor = colorInput.value;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{AddStampRequest.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let cells = document.querySelectorAll('.pageNumber');
|
||||
let inputField = document.getElementById('numberInput');
|
||||
|
||||
cells.forEach(cell => {
|
||||
cell.addEventListener('click', function(e) {
|
||||
cells.forEach(cell => {
|
||||
cell.classList.remove('selectedPosition'); // Remove selected class from all cells
|
||||
cell.classList.remove('selectedHovered'); // Also remove selectedHovered class
|
||||
});
|
||||
let selectedLocation = e.target.id;
|
||||
inputField.value = selectedLocation;
|
||||
e.target.classList.add('selectedPosition'); // Add selected class to clicked cell
|
||||
e.target.classList.add('selectedHovered'); // Add selectedHovered class
|
||||
});
|
||||
|
||||
cell.addEventListener('mouseenter', function(e) {
|
||||
if(e.target.classList.contains('selectedPosition')) {
|
||||
e.target.classList.add('selectedHovered');
|
||||
}
|
||||
});
|
||||
|
||||
cell.addEventListener('mouseleave', function(e) {
|
||||
if(e.target.classList.contains('selectedPosition')) {
|
||||
e.target.classList.remove('selectedHovered');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function toggleFileOption() {
|
||||
const stampImage = document.getElementById('stampImage');
|
||||
const stampType = document.getElementById('stampType').value;
|
||||
const stampTextGroup = document.getElementById('stampTextGroup');
|
||||
const stampImageGroup = document.getElementById('stampImageGroup');
|
||||
const alphabetGroup = document.getElementById('alphabetGroup');
|
||||
|
||||
if (stampType === 'text') {
|
||||
if (stampImage.hasAttribute('required')) {
|
||||
stampImage.removeAttribute('required');
|
||||
}
|
||||
stampTextGroup.style.display = 'block';
|
||||
stampImageGroup.style.display = 'none';
|
||||
alphabetGroup.style.display = 'block';
|
||||
} else if (stampType === 'image') {
|
||||
if (!stampImage.hasAttribute('required')) {
|
||||
stampImage.setAttribute('required', '');
|
||||
}
|
||||
stampTextGroup.style.display = 'none';
|
||||
stampImageGroup.style.display = 'block';
|
||||
alphabetGroup.style.display = 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{unlockPDFForms.title}, header=#{unlockPDFForms.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon other">preview_off</span>
|
||||
<span class="tool-header-text" th:text="#{unlockPDFForms.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/misc/unlock-pdf-forms'}" id="pdfForm" class="mb-3">
|
||||
<div class="custom-file">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
</div>
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{unlockPDFForms.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pageLayout.title}, header=#{pageLayout.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">dashboard</span>
|
||||
<span class="tool-header-text" th:text="#{pageLayout.header}"></span>
|
||||
</div>
|
||||
<form id="multiPdfForm" th:action="@{'/api/v1/general/multi-page-layout'}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<div class="mb-3">
|
||||
<label for="pagesPerSheet" th:text="#{pageLayout.pagesPerSheet}"></label>
|
||||
<select class="form-control" id="pagesPerSheet" name="pagesPerSheet">
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
<option value="9">9</option>
|
||||
<option value="16">16</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" id="addBorder" name="addBorder">
|
||||
<label for="addBorder" th:text="#{pageLayout.addBorder}"></label>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pageLayout.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,226 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{multiTool.title}, header=#{multiTool.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="image-highlighter"></div>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon advance">construction</span>
|
||||
<span class="tool-header-text" th:text="#{multiTool.header}"></span>
|
||||
</div>
|
||||
<div class="mt-action-bar d-flex flex-wrap">
|
||||
<div class="mt-filename">
|
||||
<input type="text" class="form-control" id="filename-input"
|
||||
th:placeholder="#{multiTool.uploadPrompts}">
|
||||
</div>
|
||||
<div class="mt-file-uploader">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, accept='image/*, application/pdf', showUploads=false)}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="selected-pages-display" class="selected-pages-container hidden">
|
||||
<div class="selected-pages-header">
|
||||
<h5 th:text="#{multiTool.selectedPages}">Selected Pages</h5>
|
||||
<input type="text" id="csv-input" class="form-control" placeholder="1,3,5-10" value="">
|
||||
</div>
|
||||
<ul id="selected-pages-list" class="pages-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="multi-tool-container">
|
||||
<div id="pages-container-wrapper">
|
||||
<div id="pages-container">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-action-btn">
|
||||
<button id="undo-btn" th:title="#{multiTool.undo}" class="btn btn-secondary" onclick="undo()"
|
||||
disabled>
|
||||
<span class="material-symbols-rounded">
|
||||
undo
|
||||
</span>
|
||||
</button>
|
||||
<button id="redo-btn" class="btn btn-secondary" th:title="#{multiTool.redo}" onclick="redo()"
|
||||
disabled>
|
||||
<span class="material-symbols-rounded">
|
||||
redo
|
||||
</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.rotateLeft}"
|
||||
onclick="rotateAll(-90)" disabled>
|
||||
<span class="material-symbols-rounded">
|
||||
rotate_left
|
||||
</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.rotateRight}"
|
||||
onclick="rotateAll(90)" disabled>
|
||||
<span class="material-symbols-rounded">
|
||||
rotate_right
|
||||
</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.split}" onclick="splitAll()"
|
||||
disabled>
|
||||
<span class="material-symbols-rounded">
|
||||
cut
|
||||
</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.insertPageBreak}"
|
||||
onclick="addFilesBlankAll()" disabled>
|
||||
<span class="material-symbols-rounded">
|
||||
insert_page_break
|
||||
</span>
|
||||
</button>
|
||||
<button id="select-pages-container" th:title="#{multiTool.selectPages}"
|
||||
class="btn btn-secondary enable-on-file" onclick="toggleSelectPageVisibility()" disabled>
|
||||
<span id="select-pages-button" class="material-symbols-rounded">
|
||||
event_list
|
||||
</span>
|
||||
</button>
|
||||
<button id="deselect-All-Container" th:title="#{multiTool.deselectAll}"
|
||||
class="btn btn-secondary enable-on-file hidden" onclick="deselectAll()" disabled>
|
||||
<span class="material-symbols-rounded" id="deselect-icon">deselect</span>
|
||||
</button>
|
||||
<button id="select-All-Container" th:title="#{multiTool.selectAll}"
|
||||
class="btn btn-secondary enable-on-file hidden" onclick="selectAll()" disabled>
|
||||
<span class="material-symbols-rounded" id="select-icon">select_all</span>
|
||||
</button>
|
||||
<button id="delete-button" th:title="#{multiTool.deleteSelected}"
|
||||
class="btn btn-danger delete hidden" onclick="deleteSelected()">
|
||||
<span class="material-symbols-rounded">delete</span>
|
||||
</button>
|
||||
<button id="export-selected-button" th:title="#{multiTool.downloadSelected}"
|
||||
style="border-color: green; color:#b2e3a8; background: rgba(24, 122, 5, 1)"
|
||||
class="btn btn-primary enable-on-file hidden" onclick="exportPdf(true)" disabled>
|
||||
<span class="material-symbols-rounded">
|
||||
file_save
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button style="border-color: green; color:#b2e3a8; background: rgba(24, 122, 5, 1)"
|
||||
th:title="#{multiTool.downloadAll}" id="export-button" class="btn btn-primary enable-on-file"
|
||||
onclick="exportPdf(false)" disabled>
|
||||
<span class="material-symbols-rounded">
|
||||
download
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="drag-container"></div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<script>
|
||||
window.selectedPages = [];
|
||||
window.selectPage = false;
|
||||
window.selectAll = false;
|
||||
|
||||
window.translations = {
|
||||
rotateLeft: '[[#{multiTool.rotateLeft}]]',
|
||||
rotateRight: '[[#{multiTool.rotateRight}]]',
|
||||
moveLeft: '[[#{multiTool.moveLeft}]]',
|
||||
moveRight: '[[#{multiTool.moveRight}]]',
|
||||
delete: '[[#{multiTool.delete}]]',
|
||||
split: '[[#{multiTool.split}]]',
|
||||
addFile: '[[#{multiTool.addFile}]]',
|
||||
insertPageBreak: '[[#{multiTool.insertPageBreak}]]',
|
||||
dragDropMessage: '[[#{multiTool.dragDropMessage}]]',
|
||||
undo: '[[#{multiTool.undo}]]',
|
||||
redo: '[[#{multiTool.redo}]]',
|
||||
};
|
||||
|
||||
window.decrypt = {
|
||||
passwordPrompt: '[[#{decrypt.passwordPrompt}]]',
|
||||
cancelled: '[[#{decrypt.cancelled}]]',
|
||||
noPassword: '[[#{decrypt.noPassword}]]',
|
||||
invalidPassword: '[[#{decrypt.invalidPassword}]]',
|
||||
invalidPasswordHeader: '[[#{decrypt.invalidPasswordHeader}]]',
|
||||
unexpectedError: '[[#{decrypt.unexpectedError}]]',
|
||||
serverError: '[[#{decrypt.serverError}]]',
|
||||
success: '[[#{decrypt.success}]]',
|
||||
}
|
||||
|
||||
window.corrupt = {
|
||||
pdfInvalid: '[[#{error.pdfInvalid}]]',
|
||||
tryRepair: '[[#{error.tryRepair}]]',
|
||||
}
|
||||
|
||||
const csvInput = document.getElementById("csv-input");
|
||||
csvInput.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Enter") {
|
||||
updatePagesFromCSV();
|
||||
}
|
||||
});
|
||||
csvInput.addEventListener("blur", function () {
|
||||
updatePagesFromCSV();
|
||||
});
|
||||
</script>
|
||||
<script type="module">
|
||||
import { UndoManager } from './js/multitool/UndoManager.js';
|
||||
import PdfContainer from './js/multitool/PdfContainer.js';
|
||||
import DragDropManager from "./js/multitool/DragDropManager.js";
|
||||
import ImageHighlighter from "./js/multitool/ImageHighlighter.js";
|
||||
import PdfActionsManager from './js/multitool/PdfActionsManager.js';
|
||||
// enables drag and drop
|
||||
|
||||
const pdfUpload = document.querySelector("input[name=pdf-upload]");
|
||||
pdfUpload.addEventListener("change", async (e) => {
|
||||
if (!e.target.files) return;
|
||||
await pdfContainer.handleDroppedFiles( e.target.files);
|
||||
e.target.dispatchEvent(new CustomEvent('reset', {}));
|
||||
});
|
||||
|
||||
var undoManager = new UndoManager();
|
||||
const dragDropManager = new DragDropManager('drag-container', 'pages-container');
|
||||
// enables image highlight on click
|
||||
const imageHighlighter = new ImageHighlighter('image-highlighter');
|
||||
// enables the default action buttons on each file
|
||||
const pdfActionsManager = new PdfActionsManager('pages-container', undoManager);
|
||||
// Scroll the wrapper horizontally
|
||||
|
||||
// Automatically exposes rotateAll, addFiles and exportPdf to the window for the global buttons.
|
||||
const pdfContainer = new PdfContainer(
|
||||
'pages-container',
|
||||
'pages-container-wrapper',
|
||||
[
|
||||
dragDropManager,
|
||||
imageHighlighter,
|
||||
pdfActionsManager
|
||||
],
|
||||
undoManager
|
||||
)
|
||||
|
||||
document.addEventListener('keydown', function (event) {
|
||||
let targetElementId = event.target.id;
|
||||
|
||||
// To avoid undoing/redoing the page when the user is simply undoing/redoing text
|
||||
const isFilenameInputField = (targetElementId === 'filename-input') && (event.target === document.activeElement);
|
||||
|
||||
const isUndo = (event.ctrlKey && event.key === 'z');
|
||||
const isRedo = (event.ctrlKey && event.key == 'y');
|
||||
if (isUndo && !isFilenameInputField)
|
||||
undoManager.undo();
|
||||
else if (isRedo && !isFilenameInputField) undoManager.redo();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,102 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{split-by-size-or-count.title}, header=#{split-by-size-or-count.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon advance">layers</span>
|
||||
<span class="tool-header-text" th:text="#{overlay-pdfs.header}"></span>
|
||||
</div>
|
||||
<form id="overlayForm" method="post" enctype="multipart/form-data" th:action="@{'/api/v1/general/overlay-pdfs'}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='overlayFiles', multipleInputsForSingleRequest=true, accept='application/pdf')}"></div>
|
||||
|
||||
<label for="overlayMode" th:text="#{overlay-pdfs.mode.label}">Overlay Mode</label>
|
||||
<select id="overlayMode" name="overlayMode" class="form-control">
|
||||
<option value="SequentialOverlay" th:text="#{overlay-pdfs.mode.sequential}">Sequential Overlay</option>
|
||||
<option value="InterleavedOverlay" th:text="#{overlay-pdfs.mode.interleaved}">Interleaved Overlay</option>
|
||||
<option value="FixedRepeatOverlay" th:text="#{overlay-pdfs.mode.fixedRepeat}">Fixed Repeat Overlay</option>
|
||||
</select>
|
||||
<br>
|
||||
<label for="overlayPosition" th:text="#{overlay-pdfs.position.label}">Overlay Position</label>
|
||||
<select id="overlayPosition" name="overlayPosition" class="form-control">
|
||||
<option value="0" th:text="#{overlay-pdfs.position.foreground}">Foreground</option>
|
||||
<option value="1" th:text="#{overlay-pdfs.position.background}">Background</option>
|
||||
</select>
|
||||
<div id="countsContainer" style="display: none;">
|
||||
<label th:text="#{overlay-pdfs.counts.label}">Overlay Counts</label>
|
||||
<!-- Inputs for counts will be dynamically added here -->
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{overlay-pdfs.submit}">Submit</button>
|
||||
</form>
|
||||
<script>
|
||||
function updateCountsInputs() {
|
||||
const mode = document.getElementById('overlayMode').value;
|
||||
console.log("mode",mode);
|
||||
const countsContainer = document.getElementById('countsContainer');
|
||||
console.log("countsContainer",countsContainer);
|
||||
countsContainer.innerHTML = ''; // Clear previous inputs
|
||||
|
||||
if (mode === 'FixedRepeatOverlay') {
|
||||
const fileInput = document.getElementById('overlayFiles-input');
|
||||
console.log("fileInput",fileInput);
|
||||
if(fileInput){
|
||||
const files = fileInput.files
|
||||
console.log("files",files);
|
||||
if(files) {
|
||||
const fileCount = files.length;
|
||||
countsContainer.appendChild(document.createElement("br"));
|
||||
const label = document.createElement('label');
|
||||
label.setAttribute('th:text', '#{overlay-pdfs.counts.label}');
|
||||
label.textContent = "Overlay Counts";
|
||||
countsContainer.appendChild(label);
|
||||
for (let i = 0; i < fileCount; i++) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.name = 'counts';
|
||||
input.classList.add('form-control');
|
||||
input.placeholder = 'Count for file ' + (i + 1);
|
||||
countsContainer.appendChild(input);
|
||||
}
|
||||
countsContainer.style.display = 'block';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
countsContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
var fileInput = document.getElementById('overlayFiles-input');
|
||||
console.log("fileInput2",fileInput);
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', updateCountsInputs);
|
||||
}
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
var overlay = document.getElementById('overlayMode');
|
||||
console.log("overlay2",overlay);
|
||||
if (overlay) {
|
||||
overlay.addEventListener('change', updateCountsInputs);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pdfOrganiser.title}, header=#{pdfOrganiser.header})}">
|
||||
</th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<th:block th:insert="~{fragments/multi-toolAdvert.html :: multi-toolAdvert}"></th:block>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">format_list_bulleted</span>
|
||||
<span class="tool-header-text" th:text="#{pdfOrganiser.header}"></span>
|
||||
</div>
|
||||
|
||||
<form th:action="@{'/api/v1/general/rearrange-pages'}" method="post" enctype="multipart/form-data">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="customMode" th:text="#{pdfOrganiser.mode}">Mode</label>
|
||||
<select class="form-control" id="customMode" name="customMode">
|
||||
<option value="" th:text="#{pdfOrganiser.mode.1}">Custom Page Order</option>
|
||||
<option value="REVERSE_ORDER" th:text="#{pdfOrganiser.mode.2}">Reverse Order</option>
|
||||
<option value="DUPLEX_SORT" th:text="#{pdfOrganiser.mode.3}">Duplex Sort</option>
|
||||
<option value="BOOKLET_SORT" th:text="#{pdfOrganiser.mode.4}">Booklet Sort</option>
|
||||
<option value="SIDE_STITCH_BOOKLET_SORT" th:text="#{pdfOrganiser.mode.5}">Side Stitch Booklet Sort
|
||||
</option>
|
||||
<option value="ODD_EVEN_SPLIT" th:text="#{pdfOrganiser.mode.6}">Odd-Even Split</option>
|
||||
<option value="ODD_EVEN_MERGE" th:text="#{pdfOrganiser.mode.10}">Odd-Even Merge</option>
|
||||
<option value="DUPLICATE" th:text="#{pdfOrganiser.mode.11}">Duplicate all pages</option>
|
||||
<option value="REMOVE_FIRST" th:text="#{pdfOrganiser.mode.7}">Remove First</option>
|
||||
<option value="REMOVE_LAST" th:text="#{pdfOrganiser.mode.8}">Remove Last</option>
|
||||
<option value="REMOVE_FIRST_AND_LAST" th:text="#{pdfOrganiser.mode.9}">Remove First and Last</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pageOrder" th:text="#{pageOrderPrompt}"></label>
|
||||
<input type="text" class="form-control" id="pageOrder" name="pageNumbers"
|
||||
th:placeholder="#{pdfOrganiser.placeholder}" required>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfOrganiser.submit}"></button>
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById('customMode').addEventListener('change', function () {
|
||||
var pageOrderInput = document.getElementById('pageOrder');
|
||||
if (this.value === "" || this.value === "DUPLICATE") {
|
||||
pageOrderInput.disabled = false;
|
||||
} else {
|
||||
pageOrderInput.disabled = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pdfToSinglePage.title}, header=#{pdfToSinglePage.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">looks_one</span>
|
||||
<span class="tool-header-text" th:text="#{pdfToSinglePage.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/general/pdf-to-single-page'}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfToSinglePage.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,204 @@
|
||||
<!DOCTYPE html>
|
||||
<html
|
||||
th:lang="${#locale.language}"
|
||||
th:dir="#{language.direction}"
|
||||
th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org"
|
||||
>
|
||||
<head>
|
||||
<th:block
|
||||
th:insert="~{fragments/common :: head(title=#{pipeline.title}, header=#{pipeline.header})}"
|
||||
></th:block>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
th:href="@{'/css/pipeline.css'}"
|
||||
th:if="${currentPage == 'pipeline'}"
|
||||
/>
|
||||
<script th:inline="javascript">
|
||||
const saveSettings = /*[[#{pipelineOptions.saveSettings}]]*/ "";
|
||||
const deletePipelineText = /*[[#{pipeline.deletePrompt}]]*/ "Are you sure you want to delete pipeline";
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br /><br />
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon advance">family_history</span>
|
||||
<span class="tool-header-text" th:text="#{pipeline.header}"></span>
|
||||
</div>
|
||||
<div class="text-end text-top">
|
||||
<button
|
||||
id="uploadPipelineBtn"
|
||||
class="btn btn-primary"
|
||||
th:text="#{pipeline.uploadButton}"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#pipelineSettingsModal"
|
||||
th:text="#{pipeline.configureButton}"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<div class="center-element">
|
||||
<div class="element-margin">
|
||||
<select id="pipelineSelect" class="custom-select form-control">
|
||||
<option
|
||||
value='{"name":"Custom","pipeline":[],"_examples":{"outputDir":"{outputFolder}/{folderName}","outputFileName":"{filename}-{pipelineName}-{date}-{time}"},"outputDir":"{outputFolder}","outputFileName":"{filename}"}'
|
||||
th:text="#{pipeline.defaultOption}"
|
||||
></option>
|
||||
<th:block th:each="config : ${pipelineConfigsWithNames}">
|
||||
<option
|
||||
th:value="${config.json}"
|
||||
th:text="${config.name}"
|
||||
></option>
|
||||
</th:block>
|
||||
</select>
|
||||
</div>
|
||||
<div class="element-margin">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=true, accept='*/*')}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="element-margin text-start">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
id="submitConfigBtn"
|
||||
th:text="#{pipeline.submitButton}"
|
||||
></button>
|
||||
</div>
|
||||
<br/>
|
||||
<a
|
||||
href="https://docs.stirlingpdf.com/Advanced%20Configuration/Pipeline"
|
||||
target="_blank"
|
||||
th:text="#{pipeline.help}"
|
||||
>Pipeline Help</a
|
||||
>
|
||||
<br/>
|
||||
<a
|
||||
href="https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning"
|
||||
target="_blank"
|
||||
th:text="#{pipeline.scanHelp}"
|
||||
>Folder Scanning Help</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The Modal -->
|
||||
<div class="modal" id="pipelineSettingsModal">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content dark-card">
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header">
|
||||
<h2
|
||||
class="modal-title"
|
||||
th:text="#{pipelineOptions.header}"
|
||||
></h2>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close">
|
||||
<span class="material-symbols-rounded">
|
||||
close
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal body -->
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label
|
||||
for="pipelineName"
|
||||
class="form-label"
|
||||
th:text="#{pipelineOptions.pipelineNameLabel}"
|
||||
></label>
|
||||
<input
|
||||
type="text"
|
||||
id="pipelineName"
|
||||
class="form-control"
|
||||
th:placeholder="#{pipelineOptions.pipelineNamePrompt}"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label
|
||||
for="operationsDropdown"
|
||||
th:text="#{pipelineOptions.selectOperation}"
|
||||
></label>
|
||||
<select id="operationsDropdown" class="form-select">
|
||||
<!-- Options will be dynamically populated here -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<button
|
||||
id="addOperationBtn"
|
||||
class="btn btn-primary"
|
||||
th:text="#{pipelineOptions.addOperationButton}"
|
||||
></button>
|
||||
</div>
|
||||
<h3
|
||||
id="pipelineHeader"
|
||||
style="display: none"
|
||||
th:text="#{pipelineOptions.pipelineHeader}"
|
||||
></h3>
|
||||
|
||||
<ol id="pipelineList" class="list-group">
|
||||
<!-- Pipeline operations will be dynamically populated here -->
|
||||
</ol>
|
||||
<div id="pipelineSettingsContent">
|
||||
<!-- pipelineSettings will be dynamically populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal footer -->
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
id="deletePipelineBtn"
|
||||
class="btn btn-danger"
|
||||
th:text="#{delete}"
|
||||
></button>
|
||||
|
||||
<button
|
||||
id="saveBrowserPipelineBtn"
|
||||
class="btn btn-success"
|
||||
th:text="#{saveToBrowser}"
|
||||
></button>
|
||||
<button
|
||||
id="savePipelineBtn"
|
||||
class="btn btn-success"
|
||||
th:text="#{pipelineOptions.saveButton}"
|
||||
></button>
|
||||
<button
|
||||
id="validateButton"
|
||||
class="btn btn-success"
|
||||
th:text="#{pipelineOptions.validateButton}"
|
||||
></button>
|
||||
|
||||
<div class="btn-group">
|
||||
<input
|
||||
type="file"
|
||||
id="uploadPipelineInput"
|
||||
accept=".json"
|
||||
style="display: none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script th:src="@{'/js/fetch-utils.js'}"></script>
|
||||
<script th:src="@{'/js/pipeline.js'}"></script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,511 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{releases.title}, header=#{releases.title})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon history">update</span>
|
||||
<span class="tool-header-text" th:text="#{releases.header}">Release Notes</span>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info" role="alert">
|
||||
<strong th:text="#{releases.current.version}">Current Installed Version</strong>:
|
||||
<span id="currentVersion" th:text="${@appVersion}"></span>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<span th:text="#{releases.note}">All release notes are only available in English</span>
|
||||
</div>
|
||||
|
||||
<div id="loading" class="text-center my-4">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="error-message" class="alert alert-danger d-none" role="alert">
|
||||
Failed to load release notes. Please try again later.
|
||||
</div>
|
||||
|
||||
<!-- Release Notes Container -->
|
||||
<div id="release-notes-container" class="release-notes-container">
|
||||
<!-- Release notes will be dynamically inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.release-notes-container {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.release-card {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.release-card.current-version {
|
||||
border-color: #28a745;
|
||||
background-color: rgba(40, 167, 69, 0.05);
|
||||
}
|
||||
|
||||
.release-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.release-header h3 {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.version {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.release-date {
|
||||
color: #6c757d;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.release-body {
|
||||
font-size: 0.9rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.release-link {
|
||||
color: #0d6efd;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.release-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script th:inline="javascript">
|
||||
/*<![CDATA[*/
|
||||
|
||||
// Get the current version from the appVersion bean
|
||||
const appVersion = /*[[${@appVersion}]]*/ '';
|
||||
|
||||
// GitHub API configuration
|
||||
const REPO_OWNER = 'Stirling-Tools';
|
||||
const REPO_NAME = 'Stirling-PDF';
|
||||
const GITHUB_API = 'https://api.github.com/repos/' + REPO_OWNER + '/' + REPO_NAME;
|
||||
const GITHUB_URL = 'https://github.com/' + REPO_OWNER + '/' + REPO_NAME;
|
||||
const MAX_RELEASES = 8;
|
||||
|
||||
// Secure element creation helper
|
||||
function createElement(tag, attributes = {}, children = []) {
|
||||
const element = document.createElement(tag);
|
||||
Object.entries(attributes).forEach(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
element.setAttribute(key, value);
|
||||
}
|
||||
});
|
||||
children.forEach(child => {
|
||||
if (typeof child === 'string') {
|
||||
element.appendChild(document.createTextNode(child));
|
||||
} else if (child instanceof Node) {
|
||||
element.appendChild(child);
|
||||
}
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
||||
const ALLOWED_TAGS = {
|
||||
'a': ['href', 'target', 'rel', 'class'],
|
||||
'img': ['src', 'alt', 'width', 'height', 'style'],
|
||||
'br': [],
|
||||
'p': [],
|
||||
'div': [],
|
||||
'span': []
|
||||
};
|
||||
|
||||
// Function to safely create HTML elements from string
|
||||
function createSafeElement(htmlString) {
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlString, 'text/html');
|
||||
|
||||
function sanitizeNode(node) {
|
||||
// Safety check for null/undefined
|
||||
if (!node) return null;
|
||||
|
||||
// Handle text nodes
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
return node.cloneNode(true);
|
||||
}
|
||||
|
||||
// Handle element nodes
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const tagName = node.tagName.toLowerCase();
|
||||
|
||||
// Check if tag is allowed
|
||||
if (!ALLOWED_TAGS[tagName]) {
|
||||
return document.createTextNode(node.textContent);
|
||||
}
|
||||
|
||||
// Create new element
|
||||
const cleanElement = document.createElement(tagName);
|
||||
|
||||
// Copy allowed attributes
|
||||
const allowedAttributes = ALLOWED_TAGS[tagName];
|
||||
Array.from(node.attributes).forEach(attr => {
|
||||
if (allowedAttributes.includes(attr.name)) {
|
||||
let value = attr.value;
|
||||
if (attr.name === 'href' || attr.name === 'src') {
|
||||
try {
|
||||
value = encodeURI(value);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
cleanElement.setAttribute(attr.name, value);
|
||||
}
|
||||
});
|
||||
|
||||
// Add security attributes for links
|
||||
if (tagName === 'a') {
|
||||
cleanElement.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
|
||||
// Process children
|
||||
Array.from(node.childNodes).forEach(child => {
|
||||
const cleanChild = sanitizeNode(child);
|
||||
if (cleanChild) {
|
||||
cleanElement.appendChild(cleanChild);
|
||||
}
|
||||
});
|
||||
|
||||
return cleanElement;
|
||||
}
|
||||
|
||||
// If not text or element node, return null
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the actual content from the body
|
||||
const content = doc.body.children;
|
||||
if (!content || content.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If it's a single element, process it directly
|
||||
if (content.length === 1) {
|
||||
return sanitizeNode(content[0]);
|
||||
}
|
||||
|
||||
// If multiple elements, wrap them in a div
|
||||
const wrapper = document.createElement('div');
|
||||
Array.from(content).forEach(node => {
|
||||
const cleanNode = sanitizeNode(node);
|
||||
if (cleanNode) {
|
||||
wrapper.appendChild(cleanNode);
|
||||
}
|
||||
});
|
||||
|
||||
return wrapper;
|
||||
} catch (error) {
|
||||
console.error('Error parsing HTML:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function processGitHubReferences(text) {
|
||||
if (!text || typeof text !== 'string') {
|
||||
return text;
|
||||
}
|
||||
|
||||
let currentText = text;
|
||||
let match;
|
||||
let lastIndex = 0;
|
||||
const result = document.createElement('span');
|
||||
const urlRegex = new RegExp('https://github\\.com/' + REPO_OWNER + '/' + REPO_NAME + '/(?:issues|pull)/(\\d+)', 'g');
|
||||
|
||||
while ((match = urlRegex.exec(text)) !== null) {
|
||||
// Add text before the match
|
||||
if (match.index > lastIndex) {
|
||||
result.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
|
||||
}
|
||||
|
||||
// Create link element
|
||||
const link = document.createElement('a');
|
||||
link.href = encodeURI(match[0]);
|
||||
link.textContent = `#${match[1]}`; // Use issue/PR number
|
||||
link.className = 'release-link';
|
||||
link.target = '_blank';
|
||||
link.rel = 'noopener noreferrer';
|
||||
result.appendChild(link);
|
||||
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
// Add remaining text after last match
|
||||
if (lastIndex < text.length) {
|
||||
result.appendChild(document.createTextNode(text.substring(lastIndex)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Update formatText function to handle processGitHubReferences properly
|
||||
function formatText(text) {
|
||||
const container = document.createElement('div');
|
||||
|
||||
if (!text || typeof text !== 'string') {
|
||||
console.error('Invalid input to formatText:', text);
|
||||
return container;
|
||||
}
|
||||
|
||||
let textWithoutComments;
|
||||
try {
|
||||
textWithoutComments = text.replace(
|
||||
/<!-- Release notes generated using configuration in .github\/release\.yml at main -->/,
|
||||
''
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in replace operation:', error);
|
||||
return container;
|
||||
}
|
||||
|
||||
// Split the text into lines
|
||||
let lines;
|
||||
try {
|
||||
lines = textWithoutComments.split('\n');
|
||||
} catch (error) {
|
||||
console.error('Error in split operation:', error);
|
||||
return container;
|
||||
}
|
||||
|
||||
let currentList = null;
|
||||
|
||||
lines.forEach(line => {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
// Skip empty lines but add spacing
|
||||
if (!trimmedLine) {
|
||||
if (currentList) {
|
||||
container.appendChild(currentList);
|
||||
currentList = null;
|
||||
}
|
||||
container.appendChild(document.createElement('br'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the line is HTML
|
||||
if (trimmedLine.startsWith('<') && trimmedLine.endsWith('>')) {
|
||||
if (currentList) {
|
||||
container.appendChild(currentList);
|
||||
currentList = null;
|
||||
}
|
||||
|
||||
const safeElement = createSafeElement(trimmedLine);
|
||||
if (safeElement) {
|
||||
container.appendChild(safeElement);
|
||||
} else {
|
||||
// If HTML parsing fails, treat as plain text
|
||||
container.appendChild(document.createTextNode(trimmedLine));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for headers
|
||||
const headerMatch = trimmedLine.match(/^(#{1,3})\s+(.+)$/);
|
||||
if (headerMatch) {
|
||||
if (currentList) {
|
||||
container.appendChild(currentList);
|
||||
currentList = null;
|
||||
}
|
||||
const headerLevel = headerMatch[1].length;
|
||||
const headerContent = headerMatch[2];
|
||||
// Process GitHub references in headers
|
||||
const processedContent = processGitHubReferences(headerContent);
|
||||
const header = createElement(`h${headerLevel}`);
|
||||
header.appendChild(processedContent);
|
||||
container.appendChild(header);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for bullet points
|
||||
const bulletMatch = trimmedLine.match(/^[-*]\s+(.+)$/);
|
||||
if (bulletMatch) {
|
||||
if (!currentList) {
|
||||
currentList = document.createElement('ul');
|
||||
}
|
||||
|
||||
const listContent = bulletMatch[1];
|
||||
const listItem = document.createElement('li');
|
||||
|
||||
// Process GitHub references in list items
|
||||
listItem.appendChild(processGitHubReferences(listContent));
|
||||
currentList.appendChild(listItem);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we reach here and have a list, append it
|
||||
if (currentList) {
|
||||
container.appendChild(currentList);
|
||||
currentList = null;
|
||||
}
|
||||
|
||||
// Handle regular paragraph
|
||||
const paragraph = document.createElement('p');
|
||||
paragraph.appendChild(processGitHubReferences(trimmedLine));
|
||||
container.appendChild(paragraph);
|
||||
});
|
||||
|
||||
// Append any remaining list
|
||||
if (currentList) {
|
||||
container.appendChild(currentList);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
const MAX_PREVIOUS_RELEASES = 5;
|
||||
function compareVersions(v1, v2) {
|
||||
const normalize = v => v.replace(/^v/, '');
|
||||
const v1Parts = normalize(v1).split('.').map(Number);
|
||||
const v2Parts = normalize(v2).split('.').map(Number);
|
||||
|
||||
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
|
||||
const v1Part = v1Parts[i] || 0;
|
||||
const v2Part = v2Parts[i] || 0;
|
||||
if (v1Part > v2Part) return 1;
|
||||
if (v1Part < v2Part) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function loadReleases() {
|
||||
const container = document.getElementById('release-notes-container');
|
||||
const loading = document.getElementById('loading');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
|
||||
try {
|
||||
loading.classList.remove('d-none');
|
||||
errorMessage.classList.add('d-none');
|
||||
|
||||
// Clear container safely
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
const cachedReleases = sessionStorage.getItem('releases');
|
||||
|
||||
let releases;
|
||||
if (cachedReleases) {
|
||||
releases = JSON.parse(cachedReleases);
|
||||
console.log("Read from storage");
|
||||
} else {
|
||||
const response = await fetch(GITHUB_API + '/releases');
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
throw new Error('API rate limit exceeded');
|
||||
}
|
||||
if (response.status === 404) {
|
||||
throw new Error('Repository not found');
|
||||
}
|
||||
throw new Error('Failed to fetch releases');
|
||||
}
|
||||
releases = await response.json();
|
||||
sessionStorage.setItem('releases', JSON.stringify(releases));
|
||||
}
|
||||
|
||||
// Sort releases by version number (descending)
|
||||
releases.sort((a, b) => compareVersions(b.tag_name, a.tag_name));
|
||||
|
||||
// Find index of current version
|
||||
const currentVersionIndex = releases.findIndex(release =>
|
||||
compareVersions(release.tag_name, 'v' + appVersion) === 0 ||
|
||||
compareVersions(release.tag_name, appVersion) === 0
|
||||
);
|
||||
|
||||
if (currentVersionIndex === -1) {
|
||||
container.appendChild(createElement('div', {
|
||||
class: 'alert alert-warning'
|
||||
}, ['Current version not found in releases.']));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current version and 8 previous releases
|
||||
const endIndex = Math.min(currentVersionIndex + MAX_PREVIOUS_RELEASES + 1, releases.length);
|
||||
const relevantReleases = releases.slice(currentVersionIndex, endIndex);
|
||||
|
||||
if (relevantReleases.length === 0) {
|
||||
container.appendChild(createElement('div', {
|
||||
class: 'alert alert-warning'
|
||||
}, ['No releases found.']));
|
||||
return;
|
||||
}
|
||||
|
||||
relevantReleases.forEach((release, index) => {
|
||||
const isCurrentVersion = index === 0; // First release in the array is current version
|
||||
|
||||
const releaseCard = createElement('div', {
|
||||
class: `release-card ${isCurrentVersion ? 'current-version' : ''}`
|
||||
});
|
||||
|
||||
const header = createElement('div', { class: 'release-header' });
|
||||
|
||||
const h3 = createElement('h3', {}, [
|
||||
createElement('span', { class: 'version' }, [release.tag_name]),
|
||||
createElement('span', { class: 'release-date' }, [
|
||||
new Date(release.created_at).toLocaleDateString()
|
||||
])
|
||||
]);
|
||||
|
||||
header.appendChild(h3);
|
||||
|
||||
if (isCurrentVersion) {
|
||||
header.appendChild(createElement('span', {
|
||||
class: 'badge bg-success'
|
||||
}, ['Installed']));
|
||||
}
|
||||
|
||||
releaseCard.appendChild(header);
|
||||
|
||||
const body = createElement('div', { class: 'release-body' });
|
||||
body.appendChild(formatText(release.body || 'No release notes available.'));
|
||||
|
||||
releaseCard.appendChild(body);
|
||||
container.appendChild(releaseCard);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading releases:', error);
|
||||
errorMessage.classList.remove('d-none');
|
||||
} finally {
|
||||
loading.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load releases when the page loads
|
||||
document.addEventListener('DOMContentLoaded', loadReleases);
|
||||
|
||||
/*]]>*/
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{removeImage.title}, header=#{removeImage.header})}"></th:block>
|
||||
<link rel="stylesheet" th:href="@{'/css/removeImage.css'}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon word">remove_selection</span>
|
||||
<span class="tool-header-text" th:text="#{removeImage.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'api/v1/general/remove-image-pdf'}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removeImage.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pageRemover.title}, header=#{pageRemover.header})}">
|
||||
</th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<th:block th:insert="~{fragments/multi-toolAdvert.html :: multi-toolAdvert}"></th:block>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">delete</span>
|
||||
<span class="tool-header-text" th:text="#{pageRemover.header}"></span>
|
||||
</div>
|
||||
|
||||
<form th:action="@{'/api/v1/general/remove-pages'}" method="post" enctype="multipart/form-data">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="fileInput" th:text="#{pageRemover.pagesToDelete}"></label>
|
||||
<input type="text" class="form-control" id="fileInput" name="pageNumbers"
|
||||
th:placeholder="#{pageRemover.placeholder}" required>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pageRemover.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('fileInput').addEventListener('input', function () {
|
||||
this.value = this.value.replace(/\s+/g, '');;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{rotate.title}, header=#{rotate.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<th:block th:insert="~{fragments/multi-toolAdvert.html :: multi-toolAdvert}"></th:block>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">rotate_right</span>
|
||||
<span class="tool-header-text" th:text="#{rotate.header}"></span>
|
||||
</div>
|
||||
|
||||
<form action="#" th:action="@{'/api/v1/general/rotate-pdf'}" th:object="${rotateForm}" method="post"
|
||||
enctype="multipart/form-data">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
<input type="hidden" id="angleInput" name="angle" value="0">
|
||||
|
||||
<div id="editSection" style="display: none">
|
||||
<div id="previewContainer">
|
||||
<!-- pdf-preview -->
|
||||
</div>
|
||||
|
||||
<div class="buttonContainer">
|
||||
<button type="button" class="btn btn-secondary" onclick="rotate(-90)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z" />
|
||||
<path
|
||||
d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{rotate.submit}"></button>
|
||||
<button type="button" class="btn btn-secondary" onclick="rotate(90)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" />
|
||||
<path
|
||||
d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<script>
|
||||
const angleInput = document.getElementById("angleInput");
|
||||
const fileInput = document.getElementById("fileInput-input");
|
||||
const previewContainer = document.getElementById("previewContainer");
|
||||
// const preview = document.getElementById("pdf-preview");
|
||||
fileInput.addEventListener("change", async function () {
|
||||
console.log("loading pdf");
|
||||
|
||||
document.querySelector("#editSection").style.display = "block";
|
||||
|
||||
const existingPreview = document.getElementById("pdf-preview");
|
||||
if (existingPreview) {
|
||||
existingPreview.remove();
|
||||
}
|
||||
var url = URL.createObjectURL(fileInput.files[0])
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'
|
||||
const pdf = await pdfjsLib.getDocument(url).promise;
|
||||
const page = await pdf.getPage(1);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
// set the canvas size to the size of the page
|
||||
if (page.rotate == 90 || page.rotate == 270) {
|
||||
canvas.width = page.view[3];
|
||||
canvas.height = page.view[2];
|
||||
} else {
|
||||
canvas.width = page.view[2];
|
||||
canvas.height = page.view[3];
|
||||
}
|
||||
|
||||
// render the page onto the canvas
|
||||
var renderContext = {
|
||||
canvasContext: canvas.getContext("2d"),
|
||||
viewport: page.getViewport({ scale: 1 })
|
||||
};
|
||||
|
||||
await page.render(renderContext).promise;
|
||||
const preview = document.createElement("img");
|
||||
preview.id = "pdf-preview";
|
||||
preview.alt = "preview";
|
||||
preview.src = canvas.toDataURL();
|
||||
previewContainer.appendChild(preview);
|
||||
});
|
||||
|
||||
function rotate(deg) {
|
||||
const preview = document.getElementById("pdf-preview");
|
||||
var lastTransform = preview.style.rotate;
|
||||
if (!lastTransform) {
|
||||
lastTransform = "0";
|
||||
}
|
||||
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ''));
|
||||
const newAngle = lastAngle + deg;
|
||||
preview.style.rotate = newAngle + "deg";
|
||||
angleInput.value = newAngle;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{scalePages.title}, header=#{scalePages.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">fullscreen</span>
|
||||
<span class="tool-header-text" th:text="#{scalePages.header}"></span>
|
||||
</div>
|
||||
<form id="scalePagesFrom" th:action="@{'/api/v1/general/scale-pages'}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<div class="mb-3">
|
||||
<label for="pageSize" th:text="#{scalePages.pageSize}"></label>
|
||||
<select class="form-control" id="pageSize" name="pageSize">
|
||||
<option value="KEEP" th:text="#{scalePages.keepPageSize}" selected></option>
|
||||
<option value="A0">A0</option>
|
||||
<option value="A1">A1</option>
|
||||
<option value="A2">A2</option>
|
||||
<option value="A3">A3</option>
|
||||
<option value="A4">A4</option>
|
||||
<option value="A5">A5</option>
|
||||
<option value="A6">A6</option>
|
||||
<option value="LETTER">Letter</option>
|
||||
<option value="LEGAL">Legal</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="scaleFactor" th:text="#{scalePages.scaleFactor}"></label>
|
||||
<input class="form-control" type="number" id="scaleFactor" name="scaleFactor" step="any" min="0" value="1">
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{scalePages.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,88 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{addPassword.title}, header=#{addPassword.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon security">lock</span>
|
||||
<span class="tool-header-text" th:text="#{addPassword.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'api/v1/security/add-password'}" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label th:text="#{addPassword.selectText.1}"></label>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label th:text="#{addPassword.selectText.14}"></label> <input type="password" class="form-control" id="ownerPassword" name="ownerPassword">
|
||||
<small class="form-text text-muted" th:text="#{addPassword.selectText.15}"></small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label th:text="#{addPassword.selectText.2}"></label> <input type="password" class="form-control" id="password" name="password">
|
||||
<small class="form-text text-muted" th:text="#{addPassword.selectText.16}"></small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label th:text="#{addPassword.selectText.3}"></label> <select class="form-control" id="keyLength" name="keyLength">
|
||||
<option value="40">40</option>
|
||||
<option value="128" selected>128</option>
|
||||
<option value="256">256</option>
|
||||
</select>
|
||||
<small class="form-text text-muted" th:text="#{addPassword.selectText.4}"></small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="mb-2" th:text="#{addPassword.selectText.5}"></label>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="preventAssembly" name="preventAssembly">
|
||||
<label for="preventAssembly" th:text="#{addPassword.selectText.6}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="preventExtractContent" name="preventExtractContent">
|
||||
<label for="preventExtractContent" th:text="#{addPassword.selectText.7}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="preventExtractForAccessibility" name="preventExtractForAccessibility">
|
||||
<label for="preventExtractForAccessibility" th:text="#{addPassword.selectText.8}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="preventFillInForm" name="preventFillInForm">
|
||||
<label for="preventFillInForm" th:text="#{addPassword.selectText.9}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="preventModify" name="preventModify">
|
||||
<label for="preventModify" th:text="#{addPassword.selectText.10}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="preventModifyAnnotations" name="preventModifyAnnotations">
|
||||
<label for="preventModifyAnnotations" th:text="#{addPassword.selectText.11}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="preventPrinting" name="preventPrinting">
|
||||
<label for="preventPrinting" th:text="#{addPassword.selectText.12}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="preventPrintingFaithful" name="preventPrintingFaithful">
|
||||
<label for="preventPrintingFaithful" th:text="#{addPassword.selectText.13}"></label>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="mb-3 text-left">
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{addPassword.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,176 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{watermark.title}, header=#{watermark.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body onload="toggleFileOption()">
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon security">water_drop</span>
|
||||
<span class="tool-header-text" th:text="#{watermark.header}"></span>
|
||||
</div>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'api/v1/security/add-watermark'}">
|
||||
<div class="mb-3">
|
||||
<label th:text="#{watermark.selectText.1}"></label>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label th:text="#{watermark.selectText.8}"></label>
|
||||
<select class="form-control" id="watermarkType" name="watermarkType" onchange="toggleFileOption()">
|
||||
<option value="text" th:text="#{watermark.type.1}"></option>
|
||||
<option value="image" th:text="#{watermark.type.2}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="alphabetGroup" class="mb-3">
|
||||
<label for="fontSize" th:text="#{alphabet} + ':'"></label>
|
||||
<select class="form-control" name="alphabet" id="alphabet-select">
|
||||
<option value="roman">Roman</option>
|
||||
<option value="arabic">العربية</option>
|
||||
<option value="japanese">日本語</option>
|
||||
<option value="korean">한국어</option>
|
||||
<option value="chinese">简体中文</option>
|
||||
<option value="thai">ไทย</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="watermarkTextGroup" class="mb-3">
|
||||
<label for="watermarkText" th:text="#{watermark.selectText.2}"></label>
|
||||
<input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required>
|
||||
</div>
|
||||
|
||||
<div id="watermarkImageGroup" class="mb-3" style="display: none;">
|
||||
<label for="watermarkImage" th:text="#{watermark.selectText.9}"></label>
|
||||
<input type="file" id="watermarkImage" name="watermarkImage" class="form-control-file" accept="image/*">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="fontSize" th:text="#{watermark.selectText.3}"></label>
|
||||
<input type="text" id="fontSize" name="fontSize" class="form-control" value="30">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="opacity" th:text="#{watermark.selectText.7}"></label>
|
||||
<input type="text" id="opacity" name="opacityText" class="form-control" value="50" onblur="updateOpacityValue()">
|
||||
<input type="hidden" id="opacityReal" name="opacity" value="0.5">
|
||||
</div>
|
||||
<script>
|
||||
const opacityInput = document.getElementById('opacity');
|
||||
const opacityRealInput = document.getElementById('opacityReal');
|
||||
|
||||
const updateOpacityValue = () => {
|
||||
let percentageValue = parseFloat(opacityInput.value.replace('%', ''));
|
||||
if (isNaN(percentageValue)) {
|
||||
percentageValue = 0;
|
||||
}
|
||||
percentageValue = Math.min(Math.max(percentageValue, 0), 100);
|
||||
opacityInput.value = `${percentageValue}`;
|
||||
opacityRealInput.value = (percentageValue / 100).toFixed(2);
|
||||
};
|
||||
|
||||
const appendPercentageSymbol = () => {
|
||||
if (!opacityInput.value.endsWith('%')) {
|
||||
opacityInput.value += '%';
|
||||
}
|
||||
};
|
||||
|
||||
opacityInput.addEventListener('focus', () => {
|
||||
opacityInput.value = opacityInput.value.replace('%', '');
|
||||
});
|
||||
opacityInput.addEventListener('blur', () => {
|
||||
updateOpacityValue();
|
||||
appendPercentageSymbol();
|
||||
});
|
||||
|
||||
// Set initial values
|
||||
updateOpacityValue();
|
||||
appendPercentageSymbol();
|
||||
</script>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="rotation" th:text="#{watermark.selectText.4}"></label>
|
||||
<input type="text" id="rotation" name="rotation" class="form-control" value="45">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="widthSpacer" th:text="#{watermark.selectText.5}"></label>
|
||||
<input type="text" id="widthSpacer" name="widthSpacer" class="form-control" value="50">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="heightSpacer" th:text="#{watermark.selectText.6}"></label>
|
||||
<input type="text" id="heightSpacer" name="heightSpacer" class="form-control" value="50">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="customColor" class="form-label" th:text="#{watermark.customColor}">Custom
|
||||
Color</label>
|
||||
<div class="form-control form-control-color" style="background-color: #d3d3d3;">
|
||||
<input type="color" id="customColor" name="customColor" value="#d3d3d3">
|
||||
</div>
|
||||
<script>
|
||||
let colorInput = document.getElementById("customColor");
|
||||
if (colorInput) {
|
||||
let colorInputContainer = colorInput.parentElement;
|
||||
if (colorInputContainer) {
|
||||
colorInput.onchange = function() {
|
||||
colorInputContainer.style.backgroundColor = colorInput.value;
|
||||
}
|
||||
colorInputContainer.style.backgroundColor = colorInput.value;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" id="convertPDFToImage" name="convertPDFToImage">
|
||||
<label for="convertPDFToImage" th:text="#{watermark.selectText.10}"></label>
|
||||
</div>
|
||||
<div class="mb-3 text-left">
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{watermark.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function toggleFileOption() {
|
||||
const watermarkType = document.getElementById('watermarkType').value;
|
||||
const watermarkTextGroup = document.getElementById('watermarkTextGroup');
|
||||
const watermarkImageGroup = document.getElementById('watermarkImageGroup');
|
||||
const alphabetGroup = document.getElementById('alphabetGroup'); // This is the new addition
|
||||
const watermarkText = document.getElementById('watermarkText');
|
||||
const watermarkImage = document.getElementById('watermarkImage');
|
||||
|
||||
if (watermarkType === 'text') {
|
||||
if (watermarkImage.hasAttribute('required')) {
|
||||
watermarkImage.removeAttribute('required');
|
||||
}
|
||||
watermarkTextGroup.style.display = 'block';
|
||||
watermarkText.required = true;
|
||||
watermarkImageGroup.style.display = 'none';
|
||||
watermarkImage.required = false;
|
||||
alphabetGroup.style.display = 'block';
|
||||
} else if (watermarkType === 'image') {
|
||||
if (watermarkImage.hasAttribute('required')) {
|
||||
watermarkImage.removeAttribute('required');
|
||||
}
|
||||
watermarkTextGroup.style.display = 'none';
|
||||
watermarkText.required = false;
|
||||
watermarkImageGroup.style.display = 'block';
|
||||
watermarkImage.required = true;
|
||||
alphabetGroup.style.display = 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{autoRedact.title}, header=#{autoRedact.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<svg class="material-symbols-rounded tool-header-icon security">
|
||||
<use xlink:href="/images/redact-auto.svg#icon-redact-auto"></use>
|
||||
</svg>
|
||||
<span class="tool-header-text" th:text="#{autoRedact.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'api/v1/security/auto-redact'}" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<input type="file" class="form-control" id="fileInput" name="fileInput" required
|
||||
accept="application/pdf">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="listOfText" class="form-label" th:text="#{autoRedact.textsToRedactLabel}"></label>
|
||||
<textarea class="form-control" id="listOfText" name="listOfText" rows="4" required
|
||||
th:placeholder="#{autoRedact.textsToRedactPlaceholder}"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="defaultColor" class="form-label" th:text="#{autoRedact.colorLabel}">Color</label>
|
||||
<select class="form-control" id="defaultColor" name="defaultColor"
|
||||
onchange="handleColorChange(this.value)">
|
||||
<option value="#000000" th:text="#{black}">Black</option>
|
||||
<option value="#FFFFFF" th:text="#{white}">White</option>
|
||||
<option value="#FF0000" th:text="#{red}">Red</option>
|
||||
<option value="#00FF00" th:text="#{green}">Green</option>
|
||||
<option value="#0000FF" th:text="#{blue}">Blue</option>
|
||||
<option value="custom" th:text="#{custom}">Custom...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Custom Color Input -->
|
||||
<div class="mb-3" id="customColorContainer" style="display: none;">
|
||||
<label for="customColor" class="form-label" th:text="#{autoRedact.colorLabel}">Custom Color</label>
|
||||
<input type="text" class="form-control" id="customColor" name="redactColor" placeholder="#FF00FF">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function handleColorChange(selectedValue) {
|
||||
if (selectedValue === "custom") {
|
||||
document.getElementById('customColorContainer').style.display = 'block';
|
||||
} else {
|
||||
document.getElementById('customColorContainer').style.display = 'none';
|
||||
document.getElementById('customColor').value = selectedValue;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" id="useRegex" name="useRegex">
|
||||
<label for="useRegex" th:text="#{autoRedact.useRegexLabel}"></label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" id="wholeWordSearch" name="wholeWordSearch">
|
||||
<label for="wholeWordSearch" th:text="#{autoRedact.wholeWordSearchLabel}"></label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="customPadding" class="form-label" th:text="#{autoRedact.customPaddingLabel}"></label>
|
||||
<input type="number" step="0.1" class="form-control" id="customPadding" name="customPadding"
|
||||
value="0.1">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" id="convertPDFToImage" name="convertPDFToImage" checked>
|
||||
<label for="convertPDFToImage" th:text="#{autoRedact.convertPDFToImageLabel}"></label>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary"
|
||||
th:text="#{autoRedact.submitButton}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,116 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{certSign.title}, header=#{certSign.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon security">workspace_premium</span>
|
||||
<span class="tool-header-text" th:text="#{certSign.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'api/v1/security/cert-sign'}" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label th:text="#{certSign.selectPDF}"></label>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
</div>
|
||||
<!-- Tell users to use keytool to generate JKS for other formats -->
|
||||
<div class="mb-3">
|
||||
<label th:text="#{certSign.jksNote}"></label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="certType" th:text="#{certSign.certType}"></label>
|
||||
<select class="form-control" id="certType" name="certType">
|
||||
<option value="" th:text="#{selectFilter}"></option>
|
||||
<option value="PEM">PEM</option>
|
||||
<option value="PKCS12">PKCS12</option>
|
||||
<option value="JKS">JKS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="pemGroup" style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label th:text="#{certSign.selectKey}"></label>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='privateKeyFile', multipleInputsForSingleRequest=false, notRequired=true, accept='.pem,.der')}"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label th:text="#{certSign.selectCert}"></label>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='certFile', multipleInputsForSingleRequest=false, notRequired=true, accept='.pem,.der')}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3" id="p12Group" style="display: none;">
|
||||
<label th:text="#{certSign.selectP12}"></label>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='p12File', notRequired=true, multipleInputsForSingleRequest=false, accept='.p12,.pfx')}"></div>
|
||||
</div>
|
||||
<div class="mb-3" id="jksGroup" style="display: none;">
|
||||
<label th:text="#{certSign.selectJKS}"></label>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='jksFile', notRequired=true, multipleInputsForSingleRequest=false, accept='.jks,.keystore')}"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label th:text="#{certSign.password}" for="password"></label>
|
||||
<input type="password" class="form-control" id="password" name="password">
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" id="showSignature" name="showSignature">
|
||||
<label th:text="#{certSign.showSig}" for="showSignature"></label>
|
||||
</div>
|
||||
<div id="signatureDetails" style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label for="reason" th:text="#{certSign.reason}"></label> <input type="text" class="form-control" id="reason" name="reason">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="location" th:text="#{certSign.location}"></label> <input type="text" class="form-control" id="location" name="location">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="name" th:text="#{certSign.name}"></label> <input type="text" class="form-control" id="name" name="name">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pageNumber" th:text="#{pageNum}"></label> <input type="number" class="form-control" id="pageNumber" name="pageNumber" min="1" value="1">
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" id="showLogo" name="showLogo" checked />
|
||||
<label th:text="#{certSign.showLogo}" for="showLogo"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 text-left">
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{certSign.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('certType').addEventListener('change', function() {
|
||||
var pemGroup = document.getElementById('pemGroup');
|
||||
var p12Group = document.getElementById('p12Group');
|
||||
var jksGroup = document.getElementById('jksGroup');
|
||||
var valueToGroupMap = {
|
||||
'PEM': pemGroup,
|
||||
'PKCS12': p12Group,
|
||||
'JKS': jksGroup
|
||||
};
|
||||
for (var key in valueToGroupMap) {
|
||||
valueToGroupMap[key].style.display = (this.value === key) ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('showSignature').addEventListener('change', function() {
|
||||
var signatureDetails = document.getElementById('signatureDetails');
|
||||
if (this.checked) {
|
||||
signatureDetails.style.display = 'block';
|
||||
} else {
|
||||
signatureDetails.style.display = 'none';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{permissions.title}, header=#{permissions.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon security">encrypted</span>
|
||||
<span class="tool-header-text" th:text="#{permissions.header}"></span>
|
||||
</div>
|
||||
<p th:text="#{permissions.warning}"></p>
|
||||
<form th:action="@{'api/v1/security/add-password'}" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label th:text="#{permissions.selectText.1}"></label>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="mb-2" th:text="#{permissions.selectText.2}"></label>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="preventAssembly" name="preventAssembly">
|
||||
<label for="preventAssembly" th:text="#{permissions.selectText.3}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="preventExtractContent" name="preventExtractContent">
|
||||
<label for="preventExtractContent" th:text="#{permissions.selectText.4}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="preventExtractForAccessibility" name="preventExtractForAccessibility">
|
||||
<label for="preventExtractForAccessibility" th:text="#{permissions.selectText.5}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="preventFillInForm" name="preventFillInForm">
|
||||
<label for="preventFillInForm" th:text="#{permissions.selectText.6}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="preventModify" name="preventModify">
|
||||
<label for="preventModify" th:text="#{permissions.selectText.7}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="preventModifyAnnotations" name="preventModifyAnnotations">
|
||||
<label for="preventModifyAnnotations" th:text="#{permissions.selectText.8}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="preventPrinting" name="preventPrinting">
|
||||
<label for="preventPrinting" th:text="#{permissions.selectText.9}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="preventPrintingFaithful" name="preventPrintingFaithful">
|
||||
<label for="preventPrintingFaithful" th:text="#{permissions.selectText.10}"></label>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="mb-3 text-left">
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{permissions.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,594 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{getPdfInfo.title}, header=#{getPdfInfo.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-7 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon other">info</span>
|
||||
<span class="tool-header-text" th:text="#{getPdfInfo.header}"></span>
|
||||
</div>
|
||||
<form id="pdfInfoForm" method="post" enctype="multipart/form-data" th:action="@{'/api/v1/security/get-info-on-pdf'}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, remoteCall='false', accept='application/pdf')}"></div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{getPdfInfo.submit}"></button>
|
||||
</form>
|
||||
<div class="container mt-0">
|
||||
<!-- PDF Summary section -->
|
||||
<div id="pdf-summary" class="card mt-3 mb-3" style="display: none;">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0" id="summary-heading">PDF Summary</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Quick overview of key details -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6 id="summary-basic-info-heading">Basic Information</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><strong>Pages:</strong> <span id="summary-pages">-</span></li>
|
||||
<li><strong>File Size:</strong> <span id="summary-size">-</span></li>
|
||||
<li><strong>PDF Version:</strong> <span id="summary-version">-</span></li>
|
||||
<li><strong>Language:</strong> <span id="summary-language">-</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 id="summary-doc-info-heading">Document Information</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><strong>Title:</strong> <span id="summary-title">-</span></li>
|
||||
<li><strong>Author:</strong> <span id="summary-author">-</span></li>
|
||||
<li><strong>Created:</strong> <span id="summary-created">-</span></li>
|
||||
<li><strong>Modified:</strong> <span id="summary-modified">-</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Security section -->
|
||||
<div class="mt-4 mb-3">
|
||||
<h6 id="summary-security-heading">Security Status</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div id="encryption-status" class="card mb-2 h-100">
|
||||
<div class="card-body p-2 d-flex align-items-center">
|
||||
<span id="encryption-icon" class="me-2"><i class="bi bi-lock"></i></span>
|
||||
<span id="encryption-text" class="small">Encryption: Unknown</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div id="permissions-status" class="card mb-2 h-100">
|
||||
<div class="card-body p-2 d-flex align-items-center">
|
||||
<span id="permissions-icon" class="me-2"><i class="bi bi-shield"></i></span>
|
||||
<span id="permissions-text" class="small">Permissions: Unknown</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div id="compliance-status" class="card mb-2 h-100">
|
||||
<div class="card-body p-2 d-flex align-items-center">
|
||||
<span id="compliance-icon" class="me-2"><i class="bi bi-check-circle"></i></span>
|
||||
<span id="compliance-text" class="small">Compliance: Unknown</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detailed alerts -->
|
||||
<div id="summary-alerts" class="mt-3">
|
||||
<!-- Will be populated with detailed alerts for encryption, permissions, etc. -->
|
||||
</div>
|
||||
|
||||
<!-- Descriptive note about PDF characteristics -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">PDF Overview</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p id="summary-text" class="mb-0"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Iterate over each main section in the JSON -->
|
||||
<div id="json-content">
|
||||
<!-- JavaScript will populate this section -->
|
||||
</div>
|
||||
|
||||
<!-- Button to download the JSON -->
|
||||
<a href="#" id="downloadJson" class="btn btn-primary mt-3" style="display: none;" th:text="#{getPdfInfo.downloadJson}">Download JSON</a>
|
||||
</div>
|
||||
<script th:src="@{'/js/fetch-utils.js'}"></script>
|
||||
<script th:inline="javascript">
|
||||
// Pre-load message translations
|
||||
const getPdfInfoSummary = /*[[#{getPdfInfo.summary}]]*/ "PDF Summary";
|
||||
const getPdfInfoSummaryEncrypted = /*[[#{getPdfInfo.summary.encrypted}]]*/ "This PDF is encrypted so may face issues with some applications";
|
||||
const getPdfInfoSummaryPermissions = /*[[#{getPdfInfo.summary.permissions}]]*/ "This PDF has {0} restricted permissions which may limit what you can do with it";
|
||||
const getPdfInfoSummaryCompliance = /*[[#{getPdfInfo.summary.compliance}]]*/ "This PDF complies with the {0} standard, meaning it is suitable for {1}";
|
||||
const getPdfInfoSummaryBasicInfo = /*[[#{getPdfInfo.summary.basicInfo}]]*/ "Basic Information";
|
||||
const getPdfInfoSummaryDocInfo = /*[[#{getPdfInfo.summary.docInfo}]]*/ "Document Information";
|
||||
const getPdfInfoSummarySecuritySection = /*[[#{getPdfInfo.summary.security.section}]]*/ "Security Status";
|
||||
const getPdfInfoSummaryEncryptedAlert = /*[[#{getPdfInfo.summary.encrypted.alert}]]*/ "Encrypted PDF - This document is password protected";
|
||||
const getPdfInfoSummaryNotEncryptedAlert = /*[[#{getPdfInfo.summary.not.encrypted.alert}]]*/ "Unencrypted PDF - No password protection";
|
||||
const getPdfInfoSummaryPermissionsAlert = /*[[#{getPdfInfo.summary.permissions.alert}]]*/ "Restricted Permissions - {0} actions are not allowed";
|
||||
const getPdfInfoSummaryAllPermissionsAlert = /*[[#{getPdfInfo.summary.all.permissions.alert}]]*/ "All Permissions Allowed";
|
||||
const getPdfInfoSummaryComplianceAlert = /*[[#{getPdfInfo.summary.compliance.alert}]]*/ "{0} Compliant";
|
||||
const getPdfInfoSummaryNoComplianceAlert = /*[[#{getPdfInfo.summary.no.compliance.alert}]]*/ "No Compliance Standards";
|
||||
|
||||
// Update the summary headings
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('summary-heading').textContent = getPdfInfoSummary;
|
||||
document.getElementById('summary-basic-info-heading').textContent = getPdfInfoSummaryBasicInfo;
|
||||
document.getElementById('summary-doc-info-heading').textContent = getPdfInfoSummaryDocInfo;
|
||||
document.getElementById('summary-security-heading').textContent = getPdfInfoSummarySecuritySection;
|
||||
});
|
||||
|
||||
// Pre-load section descriptions
|
||||
const getPdfInfoSectionBasicInfo = /*[[#{getPdfInfo.section.BasicInfo}]]*/ "Basic Information about the PDF document including file size, page count, and language";
|
||||
const getPdfInfoSectionMetadata = /*[[#{getPdfInfo.section.Metadata}]]*/ "Document metadata including title, author, creation date and other document properties";
|
||||
const getPdfInfoSectionDocumentInfo = /*[[#{getPdfInfo.section.DocumentInfo}]]*/ "Technical details about the PDF document structure and version";
|
||||
const getPdfInfoSectionCompliancy = /*[[#{getPdfInfo.section.Compliancy}]]*/ "PDF standards compliance information (PDF/A, PDF/X, etc.)";
|
||||
const getPdfInfoSectionEncryption = /*[[#{getPdfInfo.section.Encryption}]]*/ "Security and encryption details of the document";
|
||||
const getPdfInfoSectionPermissions = /*[[#{getPdfInfo.section.Permissions}]]*/ "Document permission settings that control what actions can be performed";
|
||||
const getPdfInfoSectionOther = /*[[#{getPdfInfo.section.Other}]]*/ "Additional document components like bookmarks, layers, and embedded files";
|
||||
const getPdfInfoSectionFormFields = /*[[#{getPdfInfo.section.FormFields}]]*/ "Interactive form fields present in the document";
|
||||
const getPdfInfoSectionPerPageInfo = /*[[#{getPdfInfo.section.PerPageInfo}]]*/ "Detailed information about each page in the document";
|
||||
|
||||
document.getElementById("pdfInfoForm").addEventListener("submit", function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Clear previous results when submitting a new form
|
||||
document.getElementById('json-content').innerHTML = '';
|
||||
document.getElementById('pdf-summary').style.display = 'none';
|
||||
document.getElementById('downloadJson').style.display = 'none';
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
|
||||
fetchWithCsrf('api/v1/security/get-info-on-pdf', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(response => response.json()).then(data => {
|
||||
// Populate and display the enhanced PDF summary
|
||||
populateSummarySection(data);
|
||||
|
||||
displayJsonData(data);
|
||||
setDownloadLink(data);
|
||||
document.getElementById("downloadJson").style.display = "block";
|
||||
}).catch(error => console.error('Error:', error));
|
||||
|
||||
// Function to reset all summary elements to default state
|
||||
function resetSummaryElements() {
|
||||
// Reset basic information fields
|
||||
document.getElementById('summary-pages').textContent = '-';
|
||||
document.getElementById('summary-size').textContent = '-';
|
||||
document.getElementById('summary-version').textContent = '-';
|
||||
document.getElementById('summary-language').textContent = '-';
|
||||
|
||||
// Reset document information fields
|
||||
document.getElementById('summary-title').textContent = '-';
|
||||
document.getElementById('summary-author').textContent = '-';
|
||||
document.getElementById('summary-created').textContent = '-';
|
||||
document.getElementById('summary-modified').textContent = '-';
|
||||
|
||||
// Reset security status cards
|
||||
const cards = ['encryption-status', 'permissions-status', 'compliance-status'];
|
||||
cards.forEach(id => {
|
||||
const card = document.getElementById(id);
|
||||
// Remove all classes except the base ones
|
||||
card.className = 'card mb-2 h-100';
|
||||
});
|
||||
|
||||
// Reset status text and icons
|
||||
document.getElementById('encryption-icon').innerHTML = '<i class="bi bi-lock"></i>';
|
||||
document.getElementById('encryption-text').textContent = 'Encryption: Unknown';
|
||||
|
||||
document.getElementById('permissions-icon').innerHTML = '<i class="bi bi-shield"></i>';
|
||||
document.getElementById('permissions-text').textContent = 'Permissions: Unknown';
|
||||
|
||||
document.getElementById('compliance-icon').innerHTML = '<i class="bi bi-check-circle"></i>';
|
||||
document.getElementById('compliance-text').textContent = 'Compliance: Unknown';
|
||||
|
||||
// Clear alerts container
|
||||
document.getElementById('summary-alerts').innerHTML = '';
|
||||
|
||||
// Reset summary text
|
||||
document.getElementById('summary-text').innerHTML = '';
|
||||
}
|
||||
|
||||
// Function to populate the enhanced summary section
|
||||
function populateSummarySection(data) {
|
||||
// Reset all elements first
|
||||
resetSummaryElements();
|
||||
|
||||
// Get basic information
|
||||
if (data.BasicInfo) {
|
||||
document.getElementById('summary-pages').textContent = data.BasicInfo["Number of pages"] || "-";
|
||||
|
||||
// Format file size nicely
|
||||
let fileSize = data.BasicInfo["FileSizeInBytes"];
|
||||
if (fileSize) {
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(fileSize) / Math.log(1024));
|
||||
fileSize = (fileSize / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
|
||||
document.getElementById('summary-size').textContent = fileSize;
|
||||
}
|
||||
|
||||
document.getElementById('summary-language').textContent = data.BasicInfo["Language"] || "-";
|
||||
}
|
||||
|
||||
// Get document information
|
||||
if (data.DocumentInfo) {
|
||||
document.getElementById('summary-version').textContent = data.DocumentInfo["PDF version"] || "-";
|
||||
}
|
||||
|
||||
// Get metadata
|
||||
if (data.Metadata) {
|
||||
document.getElementById('summary-title').textContent = data.Metadata["Title"] || "-";
|
||||
document.getElementById('summary-author').textContent = data.Metadata["Author"] || "-";
|
||||
document.getElementById('summary-created').textContent = data.Metadata["CreationDate"] || "-";
|
||||
document.getElementById('summary-modified').textContent = data.Metadata["ModificationDate"] || "-";
|
||||
}
|
||||
|
||||
// Update security status cards
|
||||
|
||||
// Encryption status
|
||||
const encryptionStatusCard = document.getElementById('encryption-status');
|
||||
const encryptionIcon = document.getElementById('encryption-icon');
|
||||
const encryptionText = document.getElementById('encryption-text');
|
||||
|
||||
if (data.Encryption && data.Encryption.IsEncrypted) {
|
||||
encryptionIcon.innerHTML = '<i class="bi bi-lock-fill"></i>';
|
||||
encryptionText.textContent = getPdfInfoSummaryEncryptedAlert;
|
||||
} else {
|
||||
encryptionIcon.innerHTML = '<i class="bi bi-unlock-fill"></i>';
|
||||
encryptionText.textContent = getPdfInfoSummaryNotEncryptedAlert;
|
||||
}
|
||||
|
||||
// Permissions status
|
||||
const permissionsStatusCard = document.getElementById('permissions-status');
|
||||
const permissionsIcon = document.getElementById('permissions-icon');
|
||||
const permissionsText = document.getElementById('permissions-text');
|
||||
|
||||
let restrictedPermissions = [];
|
||||
if (data.Permissions) {
|
||||
for (const [permission, state] of Object.entries(data.Permissions)) {
|
||||
if (state === "Not Allowed") {
|
||||
restrictedPermissions.push(permission);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (restrictedPermissions.length > 0) {
|
||||
permissionsIcon.innerHTML = '<i class="bi bi-shield-lock-fill"></i>';
|
||||
const formattedAlert = getPdfInfoSummaryPermissionsAlert.replace('{0}', restrictedPermissions.length);
|
||||
permissionsText.textContent = formattedAlert;
|
||||
} else {
|
||||
permissionsIcon.innerHTML = '<i class="bi bi-shield-check"></i>';
|
||||
permissionsText.textContent = getPdfInfoSummaryAllPermissionsAlert;
|
||||
}
|
||||
|
||||
// Compliance status
|
||||
const complianceStatusCard = document.getElementById('compliance-status');
|
||||
const complianceIcon = document.getElementById('compliance-icon');
|
||||
const complianceText = document.getElementById('compliance-text');
|
||||
|
||||
let hasCompliance = false;
|
||||
let compliantStandards = [];
|
||||
|
||||
if (data.Compliancy) {
|
||||
for (const [standard, compliant] of Object.entries(data.Compliancy)) {
|
||||
if (compliant === true) {
|
||||
hasCompliance = true;
|
||||
const standardName = standard.replace("Is", "").replace("Compliant", "");
|
||||
compliantStandards.push(standardName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCompliance) {
|
||||
complianceIcon.innerHTML = '<i class="bi bi-check-circle-fill"></i>';
|
||||
const formattedAlert = getPdfInfoSummaryComplianceAlert.replace('{0}', compliantStandards.join(', '));
|
||||
complianceText.textContent = formattedAlert;
|
||||
} else {
|
||||
complianceIcon.innerHTML = '<i class="bi bi-dash-circle"></i>';
|
||||
complianceText.textContent = getPdfInfoSummaryNoComplianceAlert;
|
||||
}
|
||||
|
||||
// Create detailed characteristic alerts
|
||||
const alertsContainer = document.getElementById('summary-alerts');
|
||||
|
||||
// Clear previous alerts
|
||||
alertsContainer.innerHTML = '';
|
||||
|
||||
// Create a single comprehensive security details section
|
||||
let hasSummaryInfo = false;
|
||||
|
||||
// Create a consolidated security details card if there are security details worth highlighting
|
||||
if ((data.Encryption && data.Encryption.IsEncrypted) ||
|
||||
restrictedPermissions.length > 0 ||
|
||||
hasCompliance) {
|
||||
|
||||
const securityDetailsCard = document.createElement('div');
|
||||
securityDetailsCard.className = 'card mt-3 mb-3';
|
||||
|
||||
const cardHeader = document.createElement('div');
|
||||
cardHeader.className = 'card-header';
|
||||
cardHeader.innerHTML = '<h6 class="mb-0">Detailed Security Information</h6>';
|
||||
securityDetailsCard.appendChild(cardHeader);
|
||||
|
||||
const cardBody = document.createElement('div');
|
||||
cardBody.className = 'card-body';
|
||||
|
||||
// Add detailed encryption info
|
||||
if (data.Encryption && data.Encryption.IsEncrypted) {
|
||||
const encryptionDiv = document.createElement('div');
|
||||
encryptionDiv.className = 'mb-3';
|
||||
encryptionDiv.innerHTML = '<h6>Encryption Details:</h6>';
|
||||
|
||||
const encryptionList = document.createElement('ul');
|
||||
encryptionList.className = 'list-unstyled';
|
||||
|
||||
if (data.Encryption.EncryptionAlgorithm) {
|
||||
encryptionList.innerHTML += `<li><strong>Algorithm:</strong> ${data.Encryption.EncryptionAlgorithm}</li>`;
|
||||
}
|
||||
if (data.Encryption.KeyLength) {
|
||||
encryptionList.innerHTML += `<li><strong>Key Length:</strong> ${data.Encryption.KeyLength} bits</li>`;
|
||||
}
|
||||
|
||||
encryptionDiv.appendChild(encryptionList);
|
||||
cardBody.appendChild(encryptionDiv);
|
||||
hasSummaryInfo = true;
|
||||
}
|
||||
|
||||
// Add detailed permissions info
|
||||
if (restrictedPermissions.length > 0) {
|
||||
const permissionsDiv = document.createElement('div');
|
||||
permissionsDiv.className = 'mb-3';
|
||||
permissionsDiv.innerHTML = '<h6>Restricted Permissions:</h6>';
|
||||
|
||||
const permissionsList = document.createElement('ul');
|
||||
restrictedPermissions.forEach(perm => {
|
||||
permissionsList.innerHTML += `<li>${perm}</li>`;
|
||||
});
|
||||
|
||||
permissionsDiv.appendChild(permissionsList);
|
||||
cardBody.appendChild(permissionsDiv);
|
||||
hasSummaryInfo = true;
|
||||
}
|
||||
|
||||
// Add detailed compliance info
|
||||
if (hasCompliance) {
|
||||
const complianceDiv = document.createElement('div');
|
||||
complianceDiv.className = 'mb-3';
|
||||
complianceDiv.innerHTML = '<h6>Standards Compliance:</h6>';
|
||||
|
||||
const complianceList = document.createElement('ul');
|
||||
complianceList.className = 'list-unstyled';
|
||||
|
||||
compliantStandards.forEach(standard => {
|
||||
let standardDescription = '';
|
||||
|
||||
// Add brief descriptions for standards
|
||||
if (standard === "PDF/A") {
|
||||
standardDescription = 'ISO standard for long-term document archiving';
|
||||
} else if (standard === "PDF/X") {
|
||||
standardDescription = 'ISO standard for printing and graphic arts exchange';
|
||||
} else if (standard === "PDF/UA") {
|
||||
standardDescription = 'ISO standard for universal accessibility';
|
||||
} else if (standard === "PDF/E") {
|
||||
standardDescription = 'ISO standard for engineering documents';
|
||||
} else if (standard === "PDF/VT") {
|
||||
standardDescription = 'ISO standard for variable and transactional printing';
|
||||
}
|
||||
|
||||
complianceList.innerHTML += `<li><strong>${standard}:</strong> ${standardDescription}</li>`;
|
||||
});
|
||||
|
||||
complianceDiv.appendChild(complianceList);
|
||||
cardBody.appendChild(complianceDiv);
|
||||
hasSummaryInfo = true;
|
||||
}
|
||||
|
||||
securityDetailsCard.appendChild(cardBody);
|
||||
|
||||
if (hasSummaryInfo) {
|
||||
alertsContainer.appendChild(securityDetailsCard);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a general document summary
|
||||
const summaryTextElement = document.getElementById('summary-text');
|
||||
|
||||
// Create a general summary for the document
|
||||
let generalSummary = `This is a ${data.BasicInfo["Number of pages"] || "multi"}-page PDF`;
|
||||
|
||||
if (data.Metadata && data.Metadata["Title"]) {
|
||||
generalSummary += ` titled "${data.Metadata["Title"]}"`;
|
||||
}
|
||||
|
||||
if (data.Metadata && data.Metadata["Author"]) {
|
||||
generalSummary += ` created by ${data.Metadata["Author"]}`;
|
||||
}
|
||||
|
||||
if (data.DocumentInfo && data.DocumentInfo["PDF version"]) {
|
||||
generalSummary += ` (PDF version ${data.DocumentInfo["PDF version"]})`;
|
||||
}
|
||||
|
||||
// Add security information to the general summary if relevant
|
||||
if (data.Encryption && data.Encryption.IsEncrypted) {
|
||||
generalSummary += '. The document is password protected';
|
||||
|
||||
if (data.Encryption.EncryptionAlgorithm) {
|
||||
generalSummary += ` using ${data.Encryption.EncryptionAlgorithm}`;
|
||||
|
||||
if (data.Encryption.KeyLength) {
|
||||
generalSummary += ` (${data.Encryption.KeyLength} bit)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (restrictedPermissions.length > 0) {
|
||||
generalSummary += `. It has ${restrictedPermissions.length} restricted permissions`;
|
||||
}
|
||||
|
||||
// Add compliance standards if available
|
||||
if (hasCompliance && compliantStandards.length > 0) {
|
||||
generalSummary += `. This document complies with the ${compliantStandards.join(', ')} PDF standard${compliantStandards.length > 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
generalSummary += '.';
|
||||
|
||||
// Remove SummaryData from JSON to avoid duplication
|
||||
if (data.SummaryData) {
|
||||
delete data.SummaryData;
|
||||
}
|
||||
|
||||
summaryTextElement.innerHTML = generalSummary;
|
||||
|
||||
// Display the summary section
|
||||
document.getElementById('pdf-summary').style.display = 'block';
|
||||
}
|
||||
|
||||
function generateSummaryFromData(summaryData) {
|
||||
let summary = [];
|
||||
|
||||
// Handle encryption information
|
||||
if (summaryData.encrypted) {
|
||||
summary.push(getPdfInfoSummaryEncrypted);
|
||||
}
|
||||
|
||||
// Handle permissions information
|
||||
if (summaryData.restrictedPermissions && summaryData.restrictedPermissions.length > 0) {
|
||||
const formattedPermissionsText = getPdfInfoSummaryPermissions.replace('{0}', summaryData.restrictedPermissionsCount);
|
||||
summary.push(formattedPermissionsText);
|
||||
}
|
||||
|
||||
// Handle standard compliance information
|
||||
if (summaryData.standardCompliance) {
|
||||
const formattedComplianceText = getPdfInfoSummaryCompliance
|
||||
.replace('{0}', summaryData.standardCompliance);
|
||||
summary.push(formattedComplianceText);
|
||||
}
|
||||
|
||||
return summary.join(' ');
|
||||
}
|
||||
});
|
||||
|
||||
function displayJsonData(jsonData) {
|
||||
const jsonContent = document.getElementById('json-content');
|
||||
|
||||
while (jsonContent.firstChild) {
|
||||
jsonContent.removeChild(jsonContent.firstChild);
|
||||
}
|
||||
|
||||
for (const key in jsonData) {
|
||||
const sectionElem = createJsonSection(key, jsonData[key]);
|
||||
jsonContent.appendChild(sectionElem);
|
||||
}
|
||||
}
|
||||
|
||||
function setDownloadLink(jsonData) {
|
||||
const downloadLink = document.getElementById('downloadJson');
|
||||
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(jsonData, null, 4));
|
||||
downloadLink.setAttribute("href", dataStr);
|
||||
downloadLink.setAttribute("download", "data.json");
|
||||
}
|
||||
|
||||
function createJsonSection(key, value, depth = 0) {
|
||||
let safeKey = (typeof key === "string") ? key.replace(/[^a-zA-Z0-9]/g, '_') : key;
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card mb-3';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'card-header';
|
||||
header.id = `${safeKey}-heading-${depth}`;
|
||||
const h5Elem = document.createElement('h5');
|
||||
h5Elem.className = 'mb-0 d-flex align-items-center';
|
||||
|
||||
// Create the main content (button or text)
|
||||
if (key === 'XMPMetadata' && typeof value === "string") {
|
||||
const buttonElem = createButtonElement(key, safeKey, depth);
|
||||
h5Elem.appendChild(buttonElem);
|
||||
} else if (value && typeof value === 'object') {
|
||||
if (Array.isArray(value) && value.length === 0) {
|
||||
h5Elem.textContent = `${key}: Empty array`;
|
||||
} else if (!Array.isArray(value) && Object.keys(value).length === 0) {
|
||||
h5Elem.textContent = `${key}: Empty object`;
|
||||
} else {
|
||||
const buttonElem = createButtonElement(key, safeKey, depth);
|
||||
h5Elem.appendChild(buttonElem);
|
||||
}
|
||||
} else {
|
||||
h5Elem.textContent = `${key}: ${String(value)}`;
|
||||
}
|
||||
|
||||
// Info buttons removed as requested
|
||||
|
||||
header.appendChild(h5Elem);
|
||||
card.appendChild(header);
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.id = `${safeKey}-content-${depth}`;
|
||||
content.className = 'collapse';
|
||||
content.setAttribute('aria-labelledby', `${safeKey}-heading-${depth}`);
|
||||
|
||||
if (key === 'XMPMetadata' && typeof value === "string") {
|
||||
const body = document.createElement('div');
|
||||
body.className = 'card-body';
|
||||
const preElem = document.createElement('pre');
|
||||
preElem.textContent = value; // Not escaping since we're using textContent
|
||||
body.appendChild(preElem);
|
||||
content.appendChild(body);
|
||||
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
const body = document.createElement('div');
|
||||
body.className = 'card-body';
|
||||
for (const subKey in value) {
|
||||
const subElem = createJsonSection(subKey, value[subKey], depth + 1);
|
||||
body.appendChild(subElem);
|
||||
}
|
||||
content.appendChild(body);
|
||||
} else if (value && typeof value === 'object' && Array.isArray(value)) {
|
||||
const body = document.createElement('div');
|
||||
body.className = 'card-body';
|
||||
value.forEach((val, index) => {
|
||||
const subElem = createJsonSection(`${key}[${index}]`, val, depth + 1);
|
||||
body.appendChild(subElem);
|
||||
});
|
||||
content.appendChild(body);
|
||||
}
|
||||
|
||||
card.appendChild(content);
|
||||
return card;
|
||||
}
|
||||
function createButtonElement(key, safeKey, depth) {
|
||||
const buttonElem = document.createElement('button');
|
||||
buttonElem.className = 'btn btn-link';
|
||||
buttonElem.type = 'button';
|
||||
buttonElem.dataset.bsToggle = "collapse";
|
||||
buttonElem.dataset.bsTarget = `#${safeKey}-content-${depth}`;
|
||||
buttonElem.setAttribute('aria-expanded', 'true');
|
||||
buttonElem.setAttribute('aria-controls', `${safeKey}-content-${depth}`);
|
||||
buttonElem.textContent = key;
|
||||
return buttonElem;
|
||||
}
|
||||
const collapsibleElems = document.querySelectorAll('[data-bs-toggle="collapse"]');
|
||||
collapsibleElems.forEach(elem => {
|
||||
new bootstrap.Collapse(elem);
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,698 @@
|
||||
<!DOCTYPE html>
|
||||
<html dir="ltr" th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
mozdisallowselectionprint xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{redact.title}, header=#{redact.header})}"></th:block>
|
||||
<script th:src="@{'/js/thirdParty/popper.min.js'}"></script>
|
||||
<script th:src="@{'/js/thirdParty/bootstrap.min.js'}"></script>
|
||||
|
||||
<script th:src="@{'/js/thirdParty/jquery.min.js'}"></script>
|
||||
<script th:src="@{'/js/thirdParty/jquery.validate.min.js'}"></script>
|
||||
|
||||
<link rel="stylesheet" th:href="@{'/css/theme/componentes.css'}">
|
||||
<link rel="stylesheet" th:href="@{'/css/navbar.css'}">
|
||||
|
||||
<!-- This snippet is used in production (included from view-pdf.html) -->
|
||||
<link rel="resource" type="application/l10n" th:href="@{'/pdfjs-legacy/locale/locale.json'}">
|
||||
<script th:src="@{'/pdfjs-legacy/pdf.mjs'}" type="module"></script>
|
||||
|
||||
<link rel="stylesheet" th:href="@{'/pdfjs-legacy/css/viewer-redact.css'}">
|
||||
<script th:src="@{'/pdfjs-legacy/js/viewer.mjs'}" type="module"></script>
|
||||
<script th:src="@{'/js/redact.js'}" type="module"></script>
|
||||
<link rel="stylesheet" th:href="@{'/css/redact.css'}">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div id="redactionFormContainer" class="container mb-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<svg class="material-symbols-rounded tool-header-icon security">
|
||||
<use xlink:href="/images/redact-manual.svg#icon-redact-manual"></use>
|
||||
</svg>
|
||||
<span class="tool-header-text" th:text="#{redact.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'api/v1/security/redact'}" method="post" enctype="multipart/form-data">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, disableMultipleFiles=true, accept='application/pdf')}">
|
||||
</div>
|
||||
<div class="mb-3 form-check d-none">
|
||||
<input type="checkbox" id="convertPDFToImage" name="convertPDFToImage" checked>
|
||||
<label for="convertPDFToImage" th:text="#{redact.convertPDFToImageLabel}"></label>
|
||||
</div>
|
||||
<div class="mb-3 d-none">
|
||||
<label for="pageNumbers" th:text="#{redact.pageRedactionNumbers.title} + ':'" class="mb-2"></label>
|
||||
<input type="text" class="form-control" id="pageNumbers" name="pageNumbers"
|
||||
th:placeholder="#{redact.pageRedactionNumbers.placeholder}">
|
||||
</div>
|
||||
<div class="mb-3 d-none">
|
||||
<label for="pageRedactColor">Page Redaction Color:</label>
|
||||
<input id="pageRedactColor" name="pageRedactionColor" type="color" class="ms-3" value="#000000">
|
||||
</div>
|
||||
<input type="hidden" name="redactions" id="redactions-input">
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary d-none" th:text="#{redact.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pageBasedRedactionOverlay" class="d-none col-md-4 row justify-content-center align-content-center">
|
||||
<div class="bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon security">document_scanner</span>
|
||||
<span class="tool-header-text" th:text="#{redact.pageBasedRedaction}"></span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pageNumbers" th:text="#{redact.pageRedactionNumbers.title} + ':'" class="mb-2"></label>
|
||||
<input type="text" class="form-control" data-for="pageNumbers"
|
||||
th:placeholder="#{redact.pageRedactionNumbers.placeholder}">
|
||||
</div>
|
||||
<div class="mb-3 d-flex">
|
||||
<label for="pageRedactColor" th:inline="text" class="pe-none user-select-none">
|
||||
[[#{redact.redactionColor.title}]]
|
||||
<span class="material-symbols-rounded">palette</span>
|
||||
:
|
||||
</label>
|
||||
<input data-for="pageRedactColor" type="color" class="ms-3" value="#000000">
|
||||
</div>
|
||||
<button id="closePageRedactionBtn" class="btn btn-danger" th:text="#{close}"></button>
|
||||
<button id="applyPageRedactionBtn" class="btn btn-success ms-3" th:text="#{apply}"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="outerContainer" class="d-none">
|
||||
<div id="sidebarContainer">
|
||||
<div id="toolbarSidebar">
|
||||
<div id="toolbarSidebarLeft">
|
||||
<div id="sidebarViewButtons" class="splitToolbarButton toggled" role="radiogroup">
|
||||
<button id="viewThumbnail" th:title="#{redact.showThumbnails}" class="toolbarButton toggled" tabindex="2"
|
||||
data-l10n-id="pdfjs-thumbs-button" role="radio" aria-checked="true" aria-controls="thumbnailView">
|
||||
</button>
|
||||
<button id="viewOutline" th:title="#{redact.showDocumentOutline}" class="toolbarButton" tabindex="3"
|
||||
data-l10n-id="pdfjs-document-outline-button" role="radio" aria-checked="false"
|
||||
aria-controls="outlineView">
|
||||
</button>
|
||||
<button id="viewAttachments" th:title="#{redact.showAttachments}" class="d-none toolbarButton" tabindex="4"
|
||||
data-l10n-id="pdfjs-attachments-button" role="radio" aria-checked="false" aria-controls="attachmentsView">
|
||||
<span data-l10n-id="pdfjs-attachments-button-label">Attachments</span>
|
||||
</button>
|
||||
<button id="viewLayers" th:title="#{redact.showLayers}" class="d-none toolbarButton" tabindex="5"
|
||||
data-l10n-id="pdfjs-layers-button" role="radio" aria-checked="false" aria-controls="layersView">
|
||||
<span data-l10n-id="pdfjs-layers-button-label">Layers</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toolbarSidebarRight">
|
||||
<div id="outlineOptionsContainer">
|
||||
<div class="verticalToolbarSeparator"></div>
|
||||
|
||||
<button id="currentOutlineItem" th:title="#{redact.findCurrentOutlineItem}" class="toolbarButton"
|
||||
disabled="disabled" tabindex="6" data-l10n-id="pdfjs-current-outline-item-button">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sidebarContent">
|
||||
<div id="thumbnailView">
|
||||
</div>
|
||||
<div id="outlineView" class="hidden">
|
||||
</div>
|
||||
<div id="attachmentsView" class="hidden">
|
||||
</div>
|
||||
<div id="layersView" class="hidden">
|
||||
</div>
|
||||
</div>
|
||||
<div id="sidebarResizer"></div>
|
||||
</div> <!-- sidebarContainer -->
|
||||
|
||||
<div id="mainContainer">
|
||||
<div class="d-none findbar hidden doorHanger" id="findbar">
|
||||
<div id="findbarInputContainer">
|
||||
<span class="loadingInput end">
|
||||
<input id="findInput" class="toolbarField" title="Find" placeholder="Find in document…" tabindex="91"
|
||||
data-l10n-id="pdfjs-find-input" aria-invalid="false">
|
||||
</span>
|
||||
<div class="splitToolbarButton">
|
||||
<button id="findPrevious" class="toolbarButton" title="Find the previous occurrence of the phrase"
|
||||
tabindex="92" data-l10n-id="pdfjs-find-previous-button">
|
||||
<span data-l10n-id="pdfjs-find-previous-button-label">Previous</span>
|
||||
</button>
|
||||
<div class="splitToolbarButtonSeparator"></div>
|
||||
<button id="findNext" class="toolbarButton" title="Find the next occurrence of the phrase" tabindex="93"
|
||||
data-l10n-id="pdfjs-find-next-button">
|
||||
<span data-l10n-id="pdfjs-find-next-button-label">Next</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="findbarOptionsOneContainer">
|
||||
<input type="checkbox" id="findHighlightAll" class="toolbarField" tabindex="94">
|
||||
<label for="findHighlightAll" class="toolbarLabel" data-l10n-id="pdfjs-find-highlight-checkbox">Highlight
|
||||
All</label>
|
||||
<input type="checkbox" id="findMatchCase" class="toolbarField" tabindex="95">
|
||||
<label for="findMatchCase" class="toolbarLabel" data-l10n-id="pdfjs-find-match-case-checkbox-label">Match
|
||||
Case</label>
|
||||
</div>
|
||||
<div id="findbarOptionsTwoContainer">
|
||||
<input type="checkbox" id="findMatchDiacritics" class="toolbarField" tabindex="96">
|
||||
<label for="findMatchDiacritics" class="toolbarLabel"
|
||||
data-l10n-id="pdfjs-find-match-diacritics-checkbox-label">Match Diacritics</label>
|
||||
<input type="checkbox" id="findEntireWord" class="toolbarField" tabindex="97">
|
||||
<label for="findEntireWord" class="toolbarLabel" data-l10n-id="pdfjs-find-entire-word-checkbox-label">Whole
|
||||
Words</label>
|
||||
</div>
|
||||
|
||||
<div id="findbarMessageContainer" aria-live="polite">
|
||||
<span id="findResultsCount" class="toolbarLabel"></span>
|
||||
<span id="findMsg" class="toolbarLabel"></span>
|
||||
</div>
|
||||
</div> <!-- findbar -->
|
||||
|
||||
<div class="editorParamsToolbar hidden doorHangerRight" id="editorHighlightParamsToolbar">
|
||||
<div id="highlightParamsToolbarContainer" class="editorParamsToolbarContainer">
|
||||
<div id="editorHighlightColorPicker" class="colorPicker">
|
||||
<span id="highlightColorPickerLabel" class="editorParamsLabel"
|
||||
data-l10n-id="pdfjs-editor-highlight-colorpicker-label">Highlight color</span>
|
||||
</div>
|
||||
<div id="editorHighlightThickness">
|
||||
<label for="editorFreeHighlightThickness" class="editorParamsLabel"
|
||||
data-l10n-id="pdfjs-editor-free-highlight-thickness-input">Thickness</label>
|
||||
<div class="thicknessPicker">
|
||||
<input type="range" id="editorFreeHighlightThickness" class="editorParamsSlider"
|
||||
data-l10n-id="pdfjs-editor-free-highlight-thickness-title" value="12" min="8" max="24" step="1"
|
||||
tabindex="101">
|
||||
</div>
|
||||
</div>
|
||||
<div id="editorHighlightVisibility">
|
||||
<div class="divider"></div>
|
||||
<div class="toggler">
|
||||
<label for="editorHighlightShowAll" class="editorParamsLabel"
|
||||
data-l10n-id="pdfjs-editor-highlight-show-all-button-label">Show all</label>
|
||||
<button id="editorHighlightShowAll" class="toggle-button"
|
||||
data-l10n-id="pdfjs-editor-highlight-show-all-button" aria-pressed="true" tabindex="102"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editorParamsToolbar hidden doorHangerRight" id="editorFreeTextParamsToolbar">
|
||||
<div class="editorParamsToolbarContainer">
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorFreeTextColor" class="editorParamsLabel"
|
||||
data-l10n-id="pdfjs-editor-free-text-color-input">Color</label>
|
||||
<input type="color" id="editorFreeTextColor" class="editorParamsColor" tabindex="103">
|
||||
</div>
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorFreeTextFontSize" class="editorParamsLabel"
|
||||
data-l10n-id="pdfjs-editor-free-text-size-input">Size</label>
|
||||
<input type="range" id="editorFreeTextFontSize" class="editorParamsSlider" value="10" min="5" max="100"
|
||||
step="1" tabindex="104">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editorParamsToolbar hidden doorHangerRight" id="editorInkParamsToolbar">
|
||||
<div class="editorParamsToolbarContainer">
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorInkColor" class="editorParamsLabel"
|
||||
data-l10n-id="pdfjs-editor-ink-color-input">Color</label>
|
||||
<input type="color" id="editorInkColor" class="editorParamsColor" tabindex="105">
|
||||
</div>
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorInkThickness" class="editorParamsLabel"
|
||||
data-l10n-id="pdfjs-editor-ink-thickness-input">Thickness</label>
|
||||
<input type="range" id="editorInkThickness" class="editorParamsSlider" value="1" min="1" max="20" step="1"
|
||||
tabindex="106">
|
||||
</div>
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorInkOpacity" class="editorParamsLabel"
|
||||
data-l10n-id="pdfjs-editor-ink-opacity-input">Opacity</label>
|
||||
<input type="range" id="editorInkOpacity" class="editorParamsSlider" value="100" min="1" max="100" step="1"
|
||||
tabindex="107">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editorParamsToolbar hidden doorHangerRight" id="editorStampParamsToolbar">
|
||||
<div class="editorParamsToolbarContainer">
|
||||
<button id="editorStampAddImage" class="secondaryToolbarButton" title="Add image" tabindex="108"
|
||||
data-l10n-id="pdfjs-editor-stamp-add-image-button">
|
||||
<span class="editorParamsLabel" data-l10n-id="pdfjs-editor-stamp-add-image-button-label">Add
|
||||
image</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="secondaryToolbar" class="secondaryToolbar hidden doorHangerRight">
|
||||
<div id="secondaryToolbarButtonContainer">
|
||||
<button id="secondaryOpenFile" class="secondaryToolbarButton visibleMediumView" title="Open File"
|
||||
tabindex="51" data-l10n-id="pdfjs-open-file-button">
|
||||
<span data-l10n-id="pdfjs-open-file-button-label">Open</span>
|
||||
</button>
|
||||
|
||||
<button id="secondaryPrint" class="secondaryToolbarButton visibleMediumView" title="Print" tabindex="52"
|
||||
data-l10n-id="pdfjs-print-button">
|
||||
<span data-l10n-id="pdfjs-print-button-label">Print</span>
|
||||
</button>
|
||||
|
||||
<button id="secondaryDownload" class="secondaryToolbarButton visibleMediumView" title="Save" tabindex="53"
|
||||
data-l10n-id="pdfjs-save-button">
|
||||
<span data-l10n-id="pdfjs-save-button-label">Save</span>
|
||||
</button>
|
||||
|
||||
<div class="horizontalToolbarSeparator"></div>
|
||||
|
||||
<button id="presentationMode" class="secondaryToolbarButton" title="Switch to Presentation Mode" tabindex="54"
|
||||
data-l10n-id="pdfjs-presentation-mode-button">
|
||||
<span data-l10n-id="pdfjs-presentation-mode-button-label">Presentation Mode</span>
|
||||
</button>
|
||||
|
||||
<a href="#" id="viewBookmark" class="secondaryToolbarButton" title="Current Page (View URL from Current Page)"
|
||||
tabindex="55" data-l10n-id="pdfjs-bookmark-button">
|
||||
<span data-l10n-id="pdfjs-bookmark-button-label">Current Page</span>
|
||||
</a>
|
||||
|
||||
<div id="viewBookmarkSeparator" class="horizontalToolbarSeparator"></div>
|
||||
|
||||
<button id="firstPage" class="secondaryToolbarButton" title="Go to First Page" tabindex="56"
|
||||
data-l10n-id="pdfjs-first-page-button">
|
||||
<span data-l10n-id="pdfjs-first-page-button-label">Go to First Page</span>
|
||||
</button>
|
||||
<button id="lastPage" class="secondaryToolbarButton" title="Go to Last Page" tabindex="57"
|
||||
data-l10n-id="pdfjs-last-page-button">
|
||||
<span data-l10n-id="pdfjs-last-page-button-label">Go to Last Page</span>
|
||||
</button>
|
||||
|
||||
<div class="horizontalToolbarSeparator"></div>
|
||||
|
||||
<button id="pageRotateCw" class="secondaryToolbarButton" title="Rotate Clockwise" tabindex="58"
|
||||
data-l10n-id="pdfjs-page-rotate-cw-button">
|
||||
<span data-l10n-id="pdfjs-page-rotate-cw-button-label">Rotate Clockwise</span>
|
||||
</button>
|
||||
<button id="pageRotateCcw" class="secondaryToolbarButton" title="Rotate Counterclockwise" tabindex="59"
|
||||
data-l10n-id="pdfjs-page-rotate-ccw-button">
|
||||
<span data-l10n-id="pdfjs-page-rotate-ccw-button-label">Rotate Counterclockwise</span>
|
||||
</button>
|
||||
|
||||
<div class="horizontalToolbarSeparator"></div>
|
||||
|
||||
<div id="cursorToolButtons" role="radiogroup">
|
||||
<button id="cursorSelectTool" class="secondaryToolbarButton toggled" title="Enable Text Selection Tool"
|
||||
tabindex="60" data-l10n-id="pdfjs-cursor-text-select-tool-button" role="radio" aria-checked="true">
|
||||
<span data-l10n-id="pdfjs-cursor-text-select-tool-button-label">Text Selection Tool</span>
|
||||
</button>
|
||||
<button id="cursorHandTool" class="secondaryToolbarButton" title="Enable Hand Tool" tabindex="61"
|
||||
data-l10n-id="pdfjs-cursor-hand-tool-button" role="radio" aria-checked="false">
|
||||
<span data-l10n-id="pdfjs-cursor-hand-tool-button-label">Hand Tool</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="horizontalToolbarSeparator"></div>
|
||||
|
||||
<div id="scrollModeButtons" role="radiogroup">
|
||||
<button id="scrollPage" class="secondaryToolbarButton" title="Use Page Scrolling" tabindex="62"
|
||||
data-l10n-id="pdfjs-scroll-page-button" role="radio" aria-checked="false">
|
||||
<span data-l10n-id="pdfjs-scroll-page-button-label">Page Scrolling</span>
|
||||
</button>
|
||||
<button id="scrollVertical" class="secondaryToolbarButton toggled" title="Use Vertical Scrolling"
|
||||
tabindex="63" data-l10n-id="pdfjs-scroll-vertical-button" role="radio" aria-checked="true">
|
||||
<span data-l10n-id="pdfjs-scroll-vertical-button-label">Vertical Scrolling</span>
|
||||
</button>
|
||||
<button id="scrollHorizontal" class="secondaryToolbarButton" title="Use Horizontal Scrolling" tabindex="64"
|
||||
data-l10n-id="pdfjs-scroll-horizontal-button" role="radio" aria-checked="false">
|
||||
<span data-l10n-id="pdfjs-scroll-horizontal-button-label">Horizontal Scrolling</span>
|
||||
</button>
|
||||
<button id="scrollWrapped" class="secondaryToolbarButton" title="Use Wrapped Scrolling" tabindex="65"
|
||||
data-l10n-id="pdfjs-scroll-wrapped-button" role="radio" aria-checked="false">
|
||||
<span data-l10n-id="pdfjs-scroll-wrapped-button-label">Wrapped Scrolling</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="horizontalToolbarSeparator"></div>
|
||||
|
||||
<div id="spreadModeButtons" role="radiogroup">
|
||||
<button id="spreadNone" class="secondaryToolbarButton toggled" title="Do not join page spreads"
|
||||
tabindex="66" data-l10n-id="pdfjs-spread-none-button" role="radio" aria-checked="true">
|
||||
<span data-l10n-id="pdfjs-spread-none-button-label">No Spreads</span>
|
||||
</button>
|
||||
<button id="spreadOdd" class="secondaryToolbarButton"
|
||||
title="Join page spreads starting with odd-numbered pages" tabindex="67"
|
||||
data-l10n-id="pdfjs-spread-odd-button" role="radio" aria-checked="false">
|
||||
<span data-l10n-id="pdfjs-spread-odd-button-label">Odd Spreads</span>
|
||||
</button>
|
||||
<button id="spreadEven" class="secondaryToolbarButton"
|
||||
title="Join page spreads starting with even-numbered pages" tabindex="68"
|
||||
data-l10n-id="pdfjs-spread-even-button" role="radio" aria-checked="false">
|
||||
<span data-l10n-id="pdfjs-spread-even-button-label">Even Spreads</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="horizontalToolbarSeparator"></div>
|
||||
|
||||
<button id="documentProperties" class="secondaryToolbarButton" title="Document Properties…" tabindex="69"
|
||||
data-l10n-id="pdfjs-document-properties-button" aria-controls="documentPropertiesDialog">
|
||||
<span data-l10n-id="pdfjs-document-properties-button-label">Document Properties…</span>
|
||||
</button>
|
||||
</div>
|
||||
</div> <!-- secondaryToolbar -->
|
||||
|
||||
<div class="toolbar">
|
||||
<div id="toolbarContainer">
|
||||
<div id="toolbarViewer">
|
||||
<div id="toolbarViewerLeft">
|
||||
<div id="showMoreBtnContainer" class="splitToolbarButton">
|
||||
<button id="showMoreBtn" class="btn-primary">
|
||||
<span id="showMoreBtnIcon" class="material-symbols-rounded">more_horiz</span>
|
||||
</button>
|
||||
</div>
|
||||
<button id="sidebarToggle" class="toolbarButton mt-2" tabindex="11"
|
||||
data-l10n-id="pdfjs-toggle-sidebar-button" th:title="#{redact.toggleSidebar}" aria-expanded="false"
|
||||
aria-controls="sidebarContainer">
|
||||
</button>
|
||||
<div class="toolbarButtonSpacer d-none"></div>
|
||||
<button id="viewFind" class="d-none toolbarButton" title="Find in Document" tabindex="12"
|
||||
data-l10n-id="pdfjs-findbar-button" aria-expanded="false" aria-controls="findbar">
|
||||
<span data-l10n-id="pdfjs-findbar-button-label">Find</span>
|
||||
</button>
|
||||
<div class="splitToolbarButton hiddenSmallView">
|
||||
<button class="toolbarButton btn-secondary" th:title="#{redact.previousPage}" id="previous"
|
||||
tabindex="13" data-l10n-id="pdfjs-previous-button">
|
||||
</button>
|
||||
<div class="splitToolbarButtonSeparator d-none"></div>
|
||||
<button class="toolbarButton btn-secondary" th:title="#{redact.nextPage}" id="next" tabindex="14"
|
||||
data-l10n-id="pdfjs-next-button">
|
||||
</button>
|
||||
</div>
|
||||
<span class="loadingInput start">
|
||||
<input type="number" id="pageNumber" class="toolbarField" value="1" min="1" tabindex="15"
|
||||
data-l10n-id="pdfjs-page-input" autocomplete="off">
|
||||
</span>
|
||||
<span id="numPages" class="toolbarLabel"></span>
|
||||
<img class="main-icon user-select-none" th:src="@{'/favicon.svg'}" alt="icon">
|
||||
</div>
|
||||
<div id="toolbarViewerMiddle">
|
||||
<div class="splitToolbarButton">
|
||||
<button id="zoomOut" th:title="#{redact.zoomOut}" class="toolbarButton btn-primary" tabindex="21"
|
||||
data-l10n-id="pdfjs-zoom-out-button">
|
||||
</button>
|
||||
<div class="splitToolbarButtonSeparator"></div>
|
||||
<button id="zoomIn" th:title="#{redact.zoomIn}" class="toolbarButton btn-primary" tabindex="22"
|
||||
data-l10n-id="pdfjs-zoom-in-button">
|
||||
</button>
|
||||
</div>
|
||||
<span id="scaleSelectContainer" class="dropdownToolbarButton">
|
||||
<select id="scaleSelect" tabindex="23" data-l10n-id="pdfjs-zoom-select">
|
||||
<option id="pageAutoOption" value="auto" selected="selected" data-l10n-id="pdfjs-page-scale-auto">
|
||||
Automatic Zoom</option>
|
||||
<option id="pageActualOption" value="page-actual" data-l10n-id="pdfjs-page-scale-actual">
|
||||
Actual Size</option>
|
||||
<option id="pageFitOption" value="page-fit" data-l10n-id="pdfjs-page-scale-fit">Page
|
||||
Fit
|
||||
</option>
|
||||
<option id="pageWidthOption" value="page-width" data-l10n-id="pdfjs-page-scale-width">
|
||||
Page Width</option>
|
||||
<option id="customScaleOption" value="custom" disabled="disabled" hidden="true"
|
||||
data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 0 }'>0%</option>
|
||||
<option value="0.5" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 50 }'>
|
||||
50%</option>
|
||||
<option value="0.75" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 75 }'>75%
|
||||
</option>
|
||||
<option value="1" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 100 }'>
|
||||
100%</option>
|
||||
<option value="1.25" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 125 }'>125%
|
||||
</option>
|
||||
<option value="1.5" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 150 }'>150%
|
||||
</option>
|
||||
<option value="2" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 200 }'>
|
||||
200%</option>
|
||||
<option value="3" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 300 }'>
|
||||
300%</option>
|
||||
<option value="4" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 400 }'>
|
||||
400%</option>
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div id="redactionsToolbarViewer" class="splitToolbarButton d-flex">
|
||||
<button id="man-text-select-redact" th:title="#{redact.textBasedRedaction}" class="btn-primary"
|
||||
tabindex="22">
|
||||
<span id="text-selection" class="material-symbols-rounded user-select-none pe-none">text_select_start
|
||||
</span>
|
||||
</button>
|
||||
<button id="man-shape-redact" th:title="#{redact.boxRedaction}" class="btn-primary" tabindex="22">
|
||||
<span id="shape-selection" class="material-symbols-rounded user-select-none pe-none">pageless
|
||||
</span>
|
||||
</button>
|
||||
<button id="redactionsPaletteContainer" th:title="#{redact.colourPicker}" class="btn-primary">
|
||||
<label id="redactions-palette" class="material-symbols-rounded palette-color text-center"
|
||||
style="--palette-color: #000000;">
|
||||
palette
|
||||
<input type="color" name="color-picker">
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
<div id="toolbarViewerRight">
|
||||
<div class="splitToolbarButton">
|
||||
<button id="pdfToImageBtn" class="btn-success">
|
||||
<span id="pdfToImageBtnIcon" th:title="#{redact.convertPDFToImageLabel}"
|
||||
class="material-symbols-rounded">
|
||||
image
|
||||
</span>
|
||||
</button>
|
||||
<button id="pageBasedRedactionBtn" class="btn-primary">
|
||||
<span id="pageBasedRedactionBtnIcon" th:title="#{redact.pageBasedRedaction}"
|
||||
class="material-symbols-rounded">
|
||||
document_scanner
|
||||
</span>
|
||||
</button>
|
||||
<button id="uploadBtn" class="btn-primary">
|
||||
<span id="uploadBtnIcon" class="material-symbols-rounded" th:title="#{redact.upload}"> upload </span>
|
||||
</button>
|
||||
<button id="downloadBtn" class="btn-primary">
|
||||
<span id="downloadBtnIcon" th:title="#{redact.export}" class="material-symbols-rounded">
|
||||
download
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="editorModeButtons" class="splitToolbarButton toggled" role="radiogroup">
|
||||
<button id="editorHighlight" class="toolbarButton" hidden="true" disabled="disabled" title="Highlight"
|
||||
role="radio" aria-checked="false" aria-controls="editorHighlightParamsToolbar" tabindex="31"
|
||||
data-l10n-id="pdfjs-editor-highlight-button">
|
||||
<span data-l10n-id="pdfjs-editor-highlight-button-label">Highlight</span>
|
||||
</button>
|
||||
<button id="openFile" class="toolbarButton hiddenMediumView d-none" title="Open File" role="radio"
|
||||
aria-checked="false" tabindex="33" data-l10n-id="pdfjs-open-file-button">
|
||||
<span data-l10n-id="pdfjs-open-file-button-label">Open</span>
|
||||
</button>
|
||||
<button id="print" class="d-none toolbarButton hiddenMediumView d-none" title="Print" role="radio"
|
||||
aria-checked="false" tabindex="34" data-l10n-id="pdfjs-print-button">
|
||||
<span data-l10n-id="pdfjs-print-button-label">Print</span>
|
||||
</button>
|
||||
<button id="download" class="d-none toolbarButton hiddenMediumView d-none" title="Save" role="radio"
|
||||
aria-checked="false" tabindex="35" data-l10n-id="pdfjs-save-button">
|
||||
<span data-l10n-id="pdfjs-save-button-label">Save</span>
|
||||
</button>
|
||||
<button id="editorStamp" class="d-none toolbarButton hiddenMediumView d-none" title="Add or edit images"
|
||||
role="radio" aria-checked="false" aria-controls="editorStampParamsToolbar" tabindex="36"
|
||||
data-l10n-id="pdfjs-editor-stamp-button">
|
||||
<span data-l10n-id="pdfjs-editor-stamp-button-label">Add or edit images</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="editorModeSeparator" class="d-none verticalToolbarSeparator"></div>
|
||||
|
||||
<button id="editorFreeText" class="d-none toolbarButton hiddenMediumView" title="Text" tabindex="41"
|
||||
data-l10n-id="pdfjs-editor-free-text-button">
|
||||
<span data-l10n-id="pdfjs-editor-free-text-button-label">Text</span>
|
||||
</button>
|
||||
|
||||
<button id="editorInk" class="d-none toolbarButton hiddenMediumView" title="Draw" tabindex="42"
|
||||
data-l10n-id="pdfjs-editor-ink-button">
|
||||
<span data-l10n-id="pdfjs-editor-ink-button-label">Draw</span>
|
||||
</button>
|
||||
|
||||
<div class="verticalToolbarSeparator hiddenMediumView"></div>
|
||||
|
||||
<button id="secondaryToolbarToggle" class="toolbarButton d-none" title="Tools" tabindex="43"
|
||||
data-l10n-id="pdfjs-tools-button">
|
||||
<span data-l10n-id="pdfjs-tools-button-label">Tools</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="loadingBar">
|
||||
<div class="progress">
|
||||
<div class="glimmer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="viewerContainer" tabindex="0">
|
||||
<div id="viewer" class="pdfViewer"></div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</div> <!-- mainContainer -->
|
||||
|
||||
<div id="dialogContainer">
|
||||
<dialog id="passwordDialog">
|
||||
<div class="row">
|
||||
<label for="password" id="passwordText" data-l10n-id="pdfjs-password-label">Enter the password to open
|
||||
this PDF file:</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="password" id="password" class="toolbarField">
|
||||
</div>
|
||||
<div class="buttonRow">
|
||||
<button id="passwordCancel" class="dialogButton"><span
|
||||
data-l10n-id="pdfjs-password-cancel-button">Cancel</span></button>
|
||||
<button id="passwordSubmit" class="dialogButton"><span
|
||||
data-l10n-id="pdfjs-password-ok-button">OK</span></button>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="documentPropertiesDialog">
|
||||
<div class="row">
|
||||
<span id="fileNameLabel" data-l10n-id="pdfjs-document-properties-file-name">File name:</span>
|
||||
<p id="fileNameField" aria-labelledby="fileNameLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="fileSizeLabel" data-l10n-id="pdfjs-document-properties-file-size">File size:</span>
|
||||
<p id="fileSizeField" aria-labelledby="fileSizeLabel">-</p>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div class="row">
|
||||
<span id="titleLabel" data-l10n-id="pdfjs-document-properties-title">Title:</span>
|
||||
<p id="titleField" aria-labelledby="titleLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="authorLabel" data-l10n-id="pdfjs-document-properties-author">Author:</span>
|
||||
<p id="authorField" aria-labelledby="authorLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="subjectLabel" data-l10n-id="pdfjs-document-properties-subject">Subject:</span>
|
||||
<p id="subjectField" aria-labelledby="subjectLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="keywordsLabel" data-l10n-id="pdfjs-document-properties-keywords">Keywords:</span>
|
||||
<p id="keywordsField" aria-labelledby="keywordsLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="creationDateLabel" data-l10n-id="pdfjs-document-properties-creation-date">Creation
|
||||
Date:</span>
|
||||
<p id="creationDateField" aria-labelledby="creationDateLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="modificationDateLabel" data-l10n-id="pdfjs-document-properties-modification-date">Modification
|
||||
Date:</span>
|
||||
<p id="modificationDateField" aria-labelledby="modificationDateLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="creatorLabel" data-l10n-id="pdfjs-document-properties-creator">Creator:</span>
|
||||
<p id="creatorField" aria-labelledby="creatorLabel">-</p>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div class="row">
|
||||
<span id="producerLabel" data-l10n-id="pdfjs-document-properties-producer">PDF Producer:</span>
|
||||
<p id="producerField" aria-labelledby="producerLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="versionLabel" data-l10n-id="pdfjs-document-properties-version">PDF Version:</span>
|
||||
<p id="versionField" aria-labelledby="versionLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="pageCountLabel" data-l10n-id="pdfjs-document-properties-page-count">Page Count:</span>
|
||||
<p id="pageCountField" aria-labelledby="pageCountLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="pageSizeLabel" data-l10n-id="pdfjs-document-properties-page-size">Page Size:</span>
|
||||
<p id="pageSizeField" aria-labelledby="pageSizeLabel">-</p>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div class="row">
|
||||
<span id="linearizedLabel" data-l10n-id="pdfjs-document-properties-linearized">Fast Web View:</span>
|
||||
<p id="linearizedField" aria-labelledby="linearizedLabel">-</p>
|
||||
</div>
|
||||
<div class="buttonRow">
|
||||
<button id="documentPropertiesClose" class="dialogButton"><span
|
||||
data-l10n-id="pdfjs-document-properties-close-button">Close</span></button>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog class="dialog altText" id="altTextDialog" aria-labelledby="dialogLabel"
|
||||
aria-describedby="dialogDescription">
|
||||
<div id="altTextContainer" class="mainContainer">
|
||||
<div id="overallDescription">
|
||||
<span id="dialogLabel" data-l10n-id="pdfjs-editor-alt-text-dialog-label" class="title">Choose an
|
||||
option</span>
|
||||
<span id="dialogDescription" data-l10n-id="pdfjs-editor-alt-text-dialog-description">
|
||||
Alt text (alternative text) helps when people can’t see the image or when it doesn’t load.
|
||||
</span>
|
||||
</div>
|
||||
<div id="addDescription">
|
||||
<div class="radio">
|
||||
<div class="radioButton">
|
||||
<input type="radio" id="descriptionButton" name="altTextOption" tabindex="0"
|
||||
aria-describedby="descriptionAreaLabel" checked>
|
||||
<label for="descriptionButton" data-l10n-id="pdfjs-editor-alt-text-add-description-label">Add a
|
||||
description</label>
|
||||
</div>
|
||||
<div class="radioLabel">
|
||||
<span id="descriptionAreaLabel" data-l10n-id="pdfjs-editor-alt-text-add-description-description">
|
||||
Aim for 1-2 sentences that describe the subject, setting, or actions.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="descriptionArea">
|
||||
<textarea id="descriptionTextarea"
|
||||
placeholder="For example, “A young man sits down at a table to eat a meal”"
|
||||
aria-labelledby="descriptionAreaLabel" data-l10n-id="pdfjs-editor-alt-text-textarea"
|
||||
tabindex="0"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div id="markAsDecorative">
|
||||
<div class="radio">
|
||||
<div class="radioButton">
|
||||
<input type="radio" id="decorativeButton" name="altTextOption" aria-describedby="decorativeLabel">
|
||||
<label for="decorativeButton" data-l10n-id="pdfjs-editor-alt-text-mark-decorative-label">Mark as
|
||||
decorative</label>
|
||||
</div>
|
||||
<div class="radioLabel">
|
||||
<span id="decorativeLabel" data-l10n-id="pdfjs-editor-alt-text-mark-decorative-description">
|
||||
This is used for ornamental images, like borders or watermarks.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="buttons">
|
||||
<button id="altTextCancel" class="secondaryButton" tabindex="0"><span
|
||||
data-l10n-id="pdfjs-editor-alt-text-cancel-button">Cancel</span></button>
|
||||
<button id="altTextSave" class="primaryButton" tabindex="0"><span
|
||||
data-l10n-id="pdfjs-editor-alt-text-save-button">Save</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="printServiceDialog" style="min-width: 200px;">
|
||||
<div class="row">
|
||||
<span data-l10n-id="pdfjs-print-progress-message">Preparing document for printing…</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<progress value="0" max="100"></progress>
|
||||
<span data-l10n-id="pdfjs-print-progress-percent" data-l10n-args='{ "progress": 0 }'
|
||||
class="relative-progress">0%</span>
|
||||
</div>
|
||||
<div class="buttonRow">
|
||||
<button id="printCancel" class="dialogButton"><span
|
||||
data-l10n-id="pdfjs-print-progress-close-button">Cancel</span></button>
|
||||
</div>
|
||||
</dialog>
|
||||
</div> <!-- dialogContainer -->
|
||||
|
||||
</div> <!-- outerContainer -->
|
||||
<div id="printContainer" class="d-none"></div>
|
||||
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{removeCertSign.title}, header=#{removeCertSign.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon security">remove_moderator</span>
|
||||
<span class="tool-header-text" th:text="#{removeCertSign.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'api/v1/security/remove-cert-sign'}" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label th:text="#{removeCertSign.selectPDF}"></label>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
</div>
|
||||
<div class="mb-3 text-left">
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removeCertSign.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{removePassword.title}, header=#{removePassword.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon security">lock_open_right</span>
|
||||
<span class="tool-header-text" th:text="#{removePassword.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'api/v1/security/remove-password'}" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label th:text="#{removePassword.selectText.1}"></label>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label th:text="#{removePassword.selectText.2}"></label>
|
||||
<input type="password" class="form-control" id="password" name="password">
|
||||
</div>
|
||||
<br>
|
||||
<div class="mb-3 text-left">
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removePassword.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{remove-watermark.title}, header=#{remove-watermark.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon security">water_drop</span>
|
||||
<span class="tool-header-text" th:text="#{remove-watermark.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'api/v1/security/remove-watermark'}">
|
||||
<div class="mb-3">
|
||||
<label th:text="#{remove-watermark.selectText.1}"></label>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="watermarkText" th:text="#{remove-watermark.selectText.2}"></label>
|
||||
<input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required />
|
||||
</div>
|
||||
<div class="mb-3 text-center">
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{remove-watermark.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{sanitizePDF.title}, header=#{sanitizePDF.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon security">sanitizer</span>
|
||||
<span class="tool-header-text" th:text="#{sanitizePDF.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'api/v1/security/sanitize-pdf'}" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="removeJavaScript" name="removeJavaScript" checked>
|
||||
<label for="removeJavaScript" th:text="#{sanitizePDF.selectText.1}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="removeEmbeddedFiles" name="removeEmbeddedFiles" checked>
|
||||
<label for="removeEmbeddedFiles" th:text="#{sanitizePDF.selectText.2}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="removeXMPMetadata" name="removeXMPMetadata">
|
||||
<label for="removeXMPMetadata" th:text="#{sanitizePDF.selectText.3}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="removeMetadata" name="removeMetadata">
|
||||
<label for="removeMetadata" th:text="#{sanitizePDF.selectText.6}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="removeLinks" name="removeLinks">
|
||||
<label for="removeLinks" th:text="#{sanitizePDF.selectText.4}"></label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input type="checkbox" id="removeFonts" name="removeFonts">
|
||||
<label for="removeFonts" th:text="#{sanitizePDF.selectText.5}"></label>
|
||||
</div>
|
||||
<br>
|
||||
<div class="mb-3 text-left">
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{sanitizePDF.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,265 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{validateSignature.title}, header=#{validateSignature.header})}"></th:block>
|
||||
</head>
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon security">verified</span>
|
||||
<span class="tool-header-text" th:text="#{validateSignature.header}"></span>
|
||||
</div>
|
||||
<form id="pdfForm" th:action="@{'api/v1/security/validate-signature'}" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label th:text="#{validateSignature.selectPDF}"></label>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, remoteCall='false', accept='application/pdf')}"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label th:text="#{validateSignature.selectCustomCert}" ></label>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='certFile', multipleInputsForSingleRequest=false, notRequired=true, remoteCall='false', accept='.cer,.crt,.pem')}"></div>
|
||||
</div>
|
||||
<div class="mb-3 text-left">
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{validateSignature.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Results section -->
|
||||
<div id="results" style="display: none;">
|
||||
<h4 th:text="#{validateSignature.results}"></h4>
|
||||
<div id="signatures-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
|
||||
<script th:inline="javascript">
|
||||
const translations = {
|
||||
signature: /*[[#{validateSignature.signature}]]*/,
|
||||
signatureInfo: /*[[#{validateSignature.signature.info}]]*/,
|
||||
certInfo: /*[[#{validateSignature.cert.info}]]*/,
|
||||
signer: /*[[#{validateSignature.signer}]]*/,
|
||||
date: /*[[#{validateSignature.date}]]*/,
|
||||
reason: /*[[#{validateSignature.reason}]]*/,
|
||||
location: /*[[#{validateSignature.location}]]*/,
|
||||
noSignatures: /*[[#{validateSignature.noSignatures}]]*/,
|
||||
statusValid: /*[[#{validateSignature.status.valid}]]*/,
|
||||
statusInvalid: /*[[#{validateSignature.status.invalid}]]*/,
|
||||
mathValid: /*[[#{validateSignature.signature.mathValid}]]*/,
|
||||
chainInvalid: /*[[#{validateSignature.chain.invalid}]]*/,
|
||||
trustInvalid: /*[[#{validateSignature.trust.invalid}]]*/,
|
||||
certExpired: /*[[#{validateSignature.cert.expired}]]*/,
|
||||
certRevoked: /*[[#{validateSignature.cert.revoked}]]*/,
|
||||
certIssuer: /*[[#{validateSignature.cert.issuer}]]*/,
|
||||
certSubject: /*[[#{validateSignature.cert.subject}]]*/,
|
||||
certSerialNumber: /*[[#{validateSignature.cert.serialNumber}]]*/,
|
||||
certValidFrom: /*[[#{validateSignature.cert.validFrom}]]*/,
|
||||
certValidUntil: /*[[#{validateSignature.cert.validUntil}]]*/,
|
||||
certAlgorithm: /*[[#{validateSignature.cert.algorithm}]]*/,
|
||||
certKeySize: /*[[#{validateSignature.cert.keySize}]]*/,
|
||||
certBits: /*[[#{validateSignature.cert.bits}]]*/,
|
||||
certVersion: /*[[#{validateSignature.cert.version}]]*/,
|
||||
certKeyUsage: /*[[#{validateSignature.cert.keyUsage}]]*/,
|
||||
certSelfSigned: /*[[#{validateSignature.cert.selfSigned}]]*/,
|
||||
yes: /*[[#{yes}]]*/,
|
||||
no: /*[[#{no}]]*/,
|
||||
selectPDF: /*[[#{validateSignature.selectPDF}]]*/
|
||||
};
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
?.toString()
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'") || 'N/A';
|
||||
}
|
||||
|
||||
document.querySelector('#pdfForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const fileInput = document.getElementById('fileInput-input');
|
||||
const certInput = document.getElementById('certFile-input');
|
||||
if (!fileInput.files.length) {
|
||||
alert(escapeHtml(translations.selectPDF));
|
||||
return;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const file of fileInput.files) {
|
||||
const formData = new FormData();
|
||||
formData.append('fileInput', file);
|
||||
|
||||
if (certInput.files.length > 0) {
|
||||
formData.append('certFile', certInput.files[0]);
|
||||
}
|
||||
try {
|
||||
const response = await window.fetchWithCsrf(e.target.action, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const fileResults = await response.json();
|
||||
fileResults.forEach(result => {
|
||||
result.fileName = file.name;
|
||||
});
|
||||
results.push(...fileResults);
|
||||
} catch (error) {
|
||||
results.push({
|
||||
fileName: file.name,
|
||||
valid: false,
|
||||
errorMessage: `${escapeHtml(translations.statusInvalid)}: ${escapeHtml(error.message)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
displayResults(results);
|
||||
});
|
||||
|
||||
function displayResults(results) {
|
||||
const resultDiv = document.getElementById('results');
|
||||
const listDiv = document.getElementById('signatures-list');
|
||||
listDiv.innerHTML = '';
|
||||
resultDiv.style.display = 'block';
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
listDiv.innerHTML = `<div class="alert alert-warning">${escapeHtml(translations.noSignatures)}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const signatureDiv = document.createElement('div');
|
||||
signatureDiv.className = 'card mb-3';
|
||||
|
||||
let validationClass = 'alert-danger';
|
||||
let validationIssues = [];
|
||||
|
||||
if (!result.valid) {
|
||||
validationIssues.push(`${escapeHtml(translations.statusInvalid)}: ${escapeHtml(result.errorMessage || '')}`);
|
||||
} else {
|
||||
const isFullyValid = result.valid &&
|
||||
result.chainValid &&
|
||||
result.trustValid &&
|
||||
result.notExpired &&
|
||||
result.notRevoked;
|
||||
|
||||
if (isFullyValid) {
|
||||
validationClass = 'alert-success';
|
||||
validationIssues.push(escapeHtml(translations.statusValid));
|
||||
} else {
|
||||
validationClass = 'alert-warning';
|
||||
validationIssues.push(escapeHtml(translations.mathValid));
|
||||
|
||||
if (!result.chainValid) {
|
||||
validationIssues.push(escapeHtml(translations.chainInvalid));
|
||||
}
|
||||
if (!result.trustValid) {
|
||||
validationIssues.push(escapeHtml(translations.trustInvalid));
|
||||
}
|
||||
if (!result.notExpired) {
|
||||
validationIssues.push(escapeHtml(translations.certExpired));
|
||||
}
|
||||
if (result.trustValid && result.chainValid && !result.notRevoked) {
|
||||
validationIssues.push(escapeHtml(translations.certRevoked));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let statusMessage = validationIssues[0];
|
||||
if (validationIssues.length > 1) {
|
||||
statusMessage += '<ul class="mb-0 mt-2">';
|
||||
for (let i = 1; i < validationIssues.length; i++) {
|
||||
statusMessage += `<li>${validationIssues[i]}</li>`;
|
||||
}
|
||||
statusMessage += '</ul>';
|
||||
}
|
||||
|
||||
let content = `
|
||||
<div class="card-body">
|
||||
${results.length > 1 ? `<h4 class="mb-3">${escapeHtml(translations.signature)} ${index + 1}</h4>` : ''}
|
||||
<div class="alert ${validationClass}">
|
||||
${statusMessage}
|
||||
</div>
|
||||
<div class="card-text">
|
||||
<h5>${escapeHtml(translations.signatureInfo)}</h5>
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(translations.signer)}:</strong></td>
|
||||
<td>${escapeHtml(result.signerName)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(translations.date)}:</strong></td>
|
||||
<td>${escapeHtml(result.signatureDate)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(translations.reason)}:</strong></td>
|
||||
<td>${escapeHtml(result.reason)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(translations.location)}:</strong></td>
|
||||
<td>${escapeHtml(result.location)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h5>${escapeHtml(translations.certInfo)}</h5>
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(translations.certIssuer)}:</strong></td>
|
||||
<td>${escapeHtml(result.issuerDN)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(translations.certSubject)}:</strong></td>
|
||||
<td>${escapeHtml(result.subjectDN)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(translations.certSerialNumber)}:</strong></td>
|
||||
<td>${escapeHtml(result.serialNumber)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(translations.certValidFrom)}:</strong></td>
|
||||
<td>${escapeHtml(result.validFrom)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(translations.certValidUntil)}:</strong></td>
|
||||
<td>${escapeHtml(result.validUntil)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(translations.certAlgorithm)}:</strong></td>
|
||||
<td>${escapeHtml(result.signatureAlgorithm)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(translations.certKeySize)}:</strong></td>
|
||||
<td>${result.keySize ? escapeHtml(result.keySize) + ' ' + escapeHtml(translations.certBits) : 'N/A'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(translations.certVersion)}:</strong></td>
|
||||
<td>${escapeHtml(result.version)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(translations.certKeyUsage)}:</strong></td>
|
||||
<td>${result.keyUsages ? result.keyUsages.map(usage => escapeHtml(usage)).join(', ') : 'N/A'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(translations.certSelfSigned)}:</strong></td>
|
||||
<td>${result.selfSigned ? escapeHtml(translations.yes) : escapeHtml(translations.no)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
signatureDiv.innerHTML = content;
|
||||
listDiv.appendChild(signatureDiv);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,241 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{sign.title}, header=#{sign.header})}"></th:block>
|
||||
<link rel="stylesheet" th:href="@{'/css/sign.css'}">
|
||||
|
||||
<th:block th:each="font : ${fonts}">
|
||||
<style th:inline="text">
|
||||
@font-face {
|
||||
font-family: "[[${font.name}]]";
|
||||
src: url('fonts/[[${font.name}]].[[${font.extension}]]') format('[[${font.type}]]');
|
||||
}
|
||||
|
||||
#font-select option[value="[[${font.name}]]"] {
|
||||
font-family: "[[${font.name}]]",
|
||||
cursive
|
||||
!important;
|
||||
}
|
||||
</style>
|
||||
</th:block>
|
||||
<script th:src="@{'/js/thirdParty/signature_pad.umd.min.js'}"></script>
|
||||
<script th:src="@{'/js/thirdParty/interact.min.js'}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-7 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon sign">signature</span>
|
||||
<span class="tool-header-text" th:text="#{sign.header}"></span>
|
||||
</div>
|
||||
|
||||
<!-- pdf selector -->
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, disableMultipleFiles=true, accept='application/pdf')}">
|
||||
</div>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<div class="tab-group show-on-file-selected">
|
||||
<div class="tab-container"th:data-title="#{sign.upload}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=false, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-container drawing-pad-container" th:data-title="#{sign.draw}">
|
||||
<canvas id="drawing-pad-canvas"></canvas>
|
||||
<br>
|
||||
<button id="clear-signature" class="btn btn-outline-danger mt-2" onclick="signaturePad.clear()"
|
||||
th:text="#{sign.clear}"></button>
|
||||
<button id="save-signature" class="btn btn-outline-success mt-2" onclick="addDraggableFromPad()"
|
||||
th:text="#{sign.add}"></button>
|
||||
<button id="signature-undo-button" class="btn btn-outline-secondary mt-2" th:text="#{sign.undo}"
|
||||
onclick="undoDraw()"></button>
|
||||
<button id="signature-redo-button" class="btn btn-outline-secondary mt-2" th:text="#{sign.redo}"
|
||||
onclick="redoDraw()"></button>
|
||||
</div>
|
||||
|
||||
<div class="tab-container" th:data-title="#{sign.saved}">
|
||||
<div class="saved-signatures-section" th:if="${not #lists.isEmpty(signatures)}">
|
||||
|
||||
<!-- Preview Modal -->
|
||||
<div class="modal fade" id="signaturePreview" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><span id="previewFileName"></span></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<img id="previewImage" src="" alt="Signature Preview" style="max-width: 100%;">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
|
||||
th:text="#{close}"></button>
|
||||
<button type="button" class="btn btn-primary" onclick="addSignatureFromPreview()"
|
||||
th:text="#{addToDoc}">Add to Document</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Personal Signatures -->
|
||||
<div th:if="${not #lists.isEmpty(signatures.?[category == 'Personal'])}">
|
||||
<h5 th:text="#{sign.personalSigs}"></h5>
|
||||
<div class="selected-files flex-wrap" style="position: relative; display:flex">
|
||||
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Personal'}"
|
||||
class="small-file-container-saved d-flex flex-column justify-content-center align-items-center">
|
||||
<div class="drag-icon"><svg xmlns="http://www.w3.org/2000/svg" height="24px"
|
||||
viewBox="0 -960 960 960" width="24px" fill="#e8eaed">
|
||||
<path
|
||||
d="M360-160q-33 0-56.5-23.5T280-240q0-33 23.5-56.5T360-320q33 0 56.5 23.5T440-240q0 33-23.5 56.5T360-160Zm240 0q-33 0-56.5-23.5T520-240q0-33 23.5-56.5T600-320q33 0 56.5 23.5T680-240q0 33-23.5 56.5T600-160ZM360-400q-33 0-56.5-23.5T280-480q0-33 23.5-56.5T360-560q33 0 56.5 23.5T440-480q0 33-23.5 56.5T360-400Zm240 0q-33 0-56.5-23.5T520-480q0-33 23.5-56.5T600-560q33 0 56.5 23.5T680-480q0 33-23.5 56.5T600-400ZM360-640q-33 0-56.5-23.5T280-720q0-33 23.5-56.5T360-800q33 0 56.5 23.5T440-720q0 33-23.5 56.5T360-640Zm240 0q-33 0-56.5-23.5T520-720q0-33 23.5-56.5T600-800q33 0 56.5 23.5T680-720q0 33-23.5 56.5T600-640Z">
|
||||
</path>
|
||||
</svg></div>
|
||||
<img th:src="@{'/api/v1/general/sign/' + ${sig.fileName}}" th:alt="${sig.fileName}"
|
||||
th:data-filename="${sig.fileName}" style="width: 50px; height: 50px; object-fit: cover;" />
|
||||
<div class="file-info d-flex flex-column align-items-center justify-content-center">
|
||||
<div class="signature-name" th:text="${sig.fileName}"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shared Signatures -->
|
||||
<div th:if="${not #lists.isEmpty(signatures.?[category == 'Shared'])}">
|
||||
<h5 th:text="#{sign.sharedSigs}"></h5>
|
||||
<div class="selected-files flex-wrap" style="position: relative; display: flex;">
|
||||
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Shared'}"
|
||||
class="small-file-container-saved d-flex flex-column justify-content-center align-items-center">
|
||||
<div class="drag-icon"><svg xmlns="http://www.w3.org/2000/svg" height="24px"
|
||||
viewBox="0 -960 960 960" width="24px" fill="#e8eaed">
|
||||
<path
|
||||
d="M360-160q-33 0-56.5-23.5T280-240q0-33 23.5-56.5T360-320q33 0 56.5 23.5T440-240q0 33-23.5 56.5T360-160Zm240 0q-33 0-56.5-23.5T520-240q0-33 23.5-56.5T600-320q33 0 56.5 23.5T680-240q0 33-23.5 56.5T600-160ZM360-400q-33 0-56.5-23.5T280-480q0-33 23.5-56.5T360-560q33 0 56.5 23.5T440-480q0 33-23.5 56.5T360-400Zm240 0q-33 0-56.5-23.5T520-480q0-33 23.5-56.5T600-560q33 0 56.5 23.5T680-480q0 33-23.5 56.5T600-400ZM360-640q-33 0-56.5-23.5T280-720q0-33 23.5-56.5T360-800q33 0 56.5 23.5T440-720q0 33-23.5 56.5T360-640Zm240 0q-33 0-56.5-23.5T520-720q0-33 23.5-56.5T600-800q33 0 56.5 23.5T680-720q0 33-23.5 56.5T600-640Z">
|
||||
</path>
|
||||
</svg></div>
|
||||
<img th:src="@{'/api/v1/general/sign/' + ${sig.fileName}}" th:alt="${sig.fileName}"
|
||||
th:data-filename="${sig.fileName}" style="width: 50px; height: 50px; object-fit: cover;" />
|
||||
<div class="file-info d-flex flex-column align-items-center justify-content-center">
|
||||
<div class="signature-name" th:text="${sig.fileName}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div th:if="${#lists.isEmpty(signatures)}" class="text-center p-3">
|
||||
<p th:text="#{sign.noSavedSigs}">No saved signatures found</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-container" th:data-title="#{sign.text}">
|
||||
<label class="form-check-label" for="sigText" th:text="#{text}"></label>
|
||||
<textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea>
|
||||
<label th:text="#{font}"></label>
|
||||
<div id="signFontSelection" class="custom-select form-control">
|
||||
<select class="form-control" name="font" id="font-select">
|
||||
<option th:each="font : ${fonts}" th:value="${font.name}" th:text="${font.name}"
|
||||
th:class="${font.name.toLowerCase()+'-font'}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="margin-auto-parent">
|
||||
<button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center"
|
||||
onclick="addDraggableFromText()" th:text="#{sign.add}"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script th:src="@{'/js/sign/signature-canvas.js'}"></script>
|
||||
<!-- draggables box -->
|
||||
<div class="draggable-buttons-box ignore-rtl">
|
||||
<button class="btn btn-outline-secondary" th:title="#{sign.delete}"
|
||||
onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())"
|
||||
style="border-color: #C02223; color: #C02223; background: rgba(255, 0, 0, 0.1)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash"
|
||||
viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z" />
|
||||
<path
|
||||
d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" th:title="#{sign.addToAll}"
|
||||
onclick="DraggableUtils.addAllPagesDraggableCanvas(DraggableUtils.getLastInteracted())">
|
||||
<span class="material-symbols-rounded">
|
||||
content_copy
|
||||
</span>
|
||||
</button>
|
||||
<div id="rotation-controls" class="align-items-center" style="display: none;">
|
||||
<div class="input-with-icon">
|
||||
<span class="material-symbols-rounded icon" style="margin-right: 3px;">
|
||||
screen_rotation
|
||||
</span>
|
||||
<input type="number" id="rotation-input" class="form-control form-control-sm me-2" value="0" step="10"
|
||||
style="width: 6rem" />
|
||||
</div>
|
||||
</div>
|
||||
<button id="ratioToggleBtn" th:title="#{sign.maintainRatio}" class="btn btn-outline-secondary"
|
||||
onclick="DraggableUtils.toggleMaintainRatio()">
|
||||
<span class="material-symbols-rounded">
|
||||
Aspect_Ratio
|
||||
</span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" th:title="#{sign.first}" onclick="goToFirstOrLastPage(false)"
|
||||
style="margin-left:auto">
|
||||
<span class="material-symbols-rounded">
|
||||
keyboard_double_arrow_left
|
||||
</span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" th:title="#{sign.previous}" id="incrementPage"
|
||||
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-chevron-left" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd"
|
||||
d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" id="decrementPage" th:title="#{sign.next}"
|
||||
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-chevron-right" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd"
|
||||
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" th:title="#{sign.last}" onclick="goToFirstOrLastPage(true)">
|
||||
<span class="material-symbols-rounded">
|
||||
keyboard_double_arrow_right
|
||||
</span>
|
||||
</button>
|
||||
<button id="download-pdf" th:title="#{downloadPdf}" class="btn btn-outline-secondary"
|
||||
style="border-color: green; color:#b2e3a8; background: rgba(24, 122, 5, 1)">
|
||||
<span class="material-symbols-rounded">
|
||||
download
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="box-drag-container" class="show-on-file-selected">
|
||||
|
||||
<canvas id="pdf-canvas"></canvas>
|
||||
<script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fabric@latest/dist/index.min.js"></script>
|
||||
<script th:src="@{'/js/draggable-utils.js'}"></script>
|
||||
<script type="module" th:src="@{'/js/pages/sign.js'}"></script>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block
|
||||
th:insert="~{fragments/common :: head(title=#{split-by-size-or-count.title}, header=#{split-by-size-or-count.header})}">
|
||||
</th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<svg class="material-symbols-rounded tool-header-icon advance">
|
||||
<use xlink:href="/images/split-size.svg#icon-split-size"></use>
|
||||
</svg>
|
||||
<span class="tool-header-text" th:text="#{split-by-size-or-count.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/general/split-by-size-or-count'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
<label for="splitType" th:text="#{split-by-size-or-count.type.label}">Split Type</label>
|
||||
<select id="splitType" name="splitType" class="form-control">
|
||||
<option value="0" th:text="#{split-by-size-or-count.type.size}">Size</option>
|
||||
<option value="1" th:text="#{split-by-size-or-count.type.pageCount}">Page Count</option>
|
||||
<option value="2" th:text="#{split-by-size-or-count.type.docCount}">Document Count</option>
|
||||
</select>
|
||||
<br>
|
||||
<label for="splitValue" th:text="#{split-by-size-or-count.value.label}">Split Value</label>
|
||||
<input type="text" id="splitValue" name="splitValue" class="form-control" required
|
||||
th:placeholder="#{split-by-size-or-count.value.placeholder}">
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary"
|
||||
th:text="#{split-by-size-or-count.submit}">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{splitByChapters.title}, header=#{splitByChapters.header})}">
|
||||
</th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<svg class="material-symbols-rounded tool-header-icon advance">
|
||||
<use xlink:href="/images/split-chapters.svg#icon-split-chapters"></use>
|
||||
</svg>
|
||||
<span class="tool-header-text" th:text="#{splitByChapters.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'/api/v1/general/split-pdf-by-chapters'}" method="post" enctype="multipart/form-data">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="bookmarkLevel" th:text="#{splitByChapters.bookmarkLevel}"></label>
|
||||
<input type="number" class="form-control" id="bookmarkLevel" name="bookmarkLevel" min="0" value="0"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="includeMetadata" name="includeMetadata">
|
||||
<label class="form-check-label" for="includeMetadata"
|
||||
th:text="#{splitByChapters.includeMetadata}"></label>
|
||||
<input type="hidden" name="includeMetadata" value="false" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="allowDuplicates" name="allowDuplicates">
|
||||
<label class="form-check-label" for="allowDuplicates"
|
||||
th:text="#{splitByChapters.allowDuplicates}"></label>
|
||||
<input type="hidden" name="allowDuplicates" value="false" />
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<a class="btn btn-outline-primary" data-bs-toggle="collapse" href="#info" role="button"
|
||||
aria-expanded="false" aria-controls="info" th:text="#{info}"></a>
|
||||
</p>
|
||||
<div class="collapse" id="info">
|
||||
<p th:text="#{splitByChapters.desc.1}"></p>
|
||||
<p th:text="#{splitByChapters.desc.2}"></p>
|
||||
<p th:text="#{splitByChapters.desc.3}"></p>
|
||||
<p th:text="#{splitByChapters.desc.4}"></p>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{splitByChapters.submit}"></button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,88 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{split-by-sections.title}, header=#{split-by-sections.header})}"></th:block>
|
||||
<link rel="stylesheet" th:href="@{'/css/split-pdf-by-sections.css'}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon advance">grid_on</span>
|
||||
<span class="tool-header-text" th:text="#{split-by-sections.header}"></span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/general/split-pdf-by-sections'}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<label for="horizontalDivisions" th:text="#{split-by-sections.horizontal.label}">Horizontal Divisions</label>
|
||||
<input type="number" id="horizontalDivisions" name="horizontalDivisions" class="form-control" min="0" max="300" value="0" required th:placeholder="#{split-by-sections.horizontal.placeholder}">
|
||||
<br>
|
||||
<label for="verticalDivisions" th:text="#{split-by-sections.vertical.label}">Vertical Divisions</label>
|
||||
<input type="number" id="verticalDivisions" name="verticalDivisions" class="form-control" min="0" max="300" required value="1" th:placeholder="#{split-by-sections.vertical.placeholder}">
|
||||
<br>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" id="merge" name="merge">
|
||||
<label for="merge" th:text="#{split-by-sections.merge}">merge PDFs into one</label>
|
||||
</div>
|
||||
<br>
|
||||
<div id="pdfVisualAid" class="pdf-visual-aid"></div>
|
||||
<script>
|
||||
function updateVisualAid() {
|
||||
const horizontalDivisions = document.getElementById('horizontalDivisions').value;
|
||||
const verticalDivisions = document.getElementById('verticalDivisions').value;
|
||||
const aid = document.getElementById('pdfVisualAid');
|
||||
|
||||
if(horizontalDivisions > 300) {
|
||||
horizontalDivisions = 300
|
||||
}
|
||||
if(verticalDivisions > 300) {
|
||||
verticalDivisions = 300
|
||||
}
|
||||
// Clear existing lines
|
||||
aid.innerHTML = '';
|
||||
|
||||
// Add horizontal lines
|
||||
for (let i = 0; i < horizontalDivisions; i++) {
|
||||
const line = document.createElement('div');
|
||||
line.classList.add('line');
|
||||
line.style.width = '100%';
|
||||
line.style.height = '1px';
|
||||
line.style.top = `${((i + 1) / (parseInt(horizontalDivisions) + 1)) * 100}%`;
|
||||
aid.appendChild(line);
|
||||
}
|
||||
|
||||
// Add vertical lines
|
||||
for (let i = 0; i < verticalDivisions; i++) {
|
||||
const line = document.createElement('div');
|
||||
line.classList.add('line');
|
||||
line.style.height = '100%';
|
||||
line.style.width = '1px';
|
||||
line.style.left = `${((i + 1) / (parseInt(verticalDivisions) + 1)) * 100}%`;
|
||||
aid.appendChild(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('horizontalDivisions').addEventListener('input', updateVisualAid);
|
||||
document.getElementById('verticalDivisions').addEventListener('input', updateVisualAid);
|
||||
|
||||
// Initial draw
|
||||
updateVisualAid();
|
||||
</script>
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{split-by-sections.submit}">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{split.title}, header=#{split.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<th:block th:insert="~{fragments/multi-toolAdvert.html :: multi-toolAdvert}"></th:block>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">cut</span>
|
||||
<span class="tool-header-text" th:text="#{split.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'/api/v1/general/split-pages'}" method="post" enctype="multipart/form-data">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="pageNumbers" th:text="#{split.splitPages}"></label>
|
||||
<input type="text" class="form-control" id="pageNumbers" name="pageNumbers" placeholder="1,3,5-10"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<a class="btn btn-outline-primary" data-bs-toggle="collapse" href="#info" role="button"
|
||||
aria-expanded="false" aria-controls="info" th:text="#{info}"></a>
|
||||
</p>
|
||||
<div class="collapse" id="info">
|
||||
<p th:text="#{split.desc.1}"></p>
|
||||
<p th:text="#{split.desc.2}"></p>
|
||||
<p th:text="#{split.desc.3}"></p>
|
||||
<p th:text="#{split.desc.4}"></p>
|
||||
<p th:text="#{split.desc.5}"></p>
|
||||
<p th:text="#{split.desc.6}"></p>
|
||||
<p th:text="#{split.desc.7}"></p>
|
||||
<p th:text="#{split.desc.8}"></p>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{split.submit}"></button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{endpointStatistics.title}, header=#{endpointStatistics.header})}"></th:block>
|
||||
<script th:src="@{'/js/thirdParty/chart.umd.min.js'}"></script>
|
||||
<link rel="stylesheet" th:href="@{'/css/usage.css'}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-9 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon">analytics</span>
|
||||
<span class="tool-header-text" th:text="#{endpointStatistics.header}">Endpoint Statistics</span>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Summary Box -->
|
||||
<div class="stats-box">
|
||||
<div class="chart-controls">
|
||||
<button id="top10Btn" class="btn btn-outline-primary active">
|
||||
<span class="material-symbols-rounded">bar_chart</span>
|
||||
<span th:text="#{endpointStatistics.top10}">Top 10</span>
|
||||
</button>
|
||||
<button id="top20Btn" class="btn btn-outline-primary">
|
||||
<span class="material-symbols-rounded">data_usage</span>
|
||||
<span th:text="#{endpointStatistics.top20}">Top 20</span>
|
||||
</button>
|
||||
<button id="allBtn" class="btn btn-outline-primary">
|
||||
<span class="material-symbols-rounded">insights</span>
|
||||
<span th:text="#{endpointStatistics.all}">All</span>
|
||||
</button>
|
||||
<button id="refreshBtn" class="btn btn-outline-secondary">
|
||||
<span class="material-symbols-rounded">refresh</span>
|
||||
<span th:text="#{endpointStatistics.refresh}">Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-controls">
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="hideHomeCheckbox" checked>
|
||||
<span th:text="#{endpointStatistics.includeHomepage}">Include Homepage ('/')</span>
|
||||
</label>
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="hideLoginCheckbox" checked>
|
||||
<span th:text="#{endpointStatistics.includeLoginPage}">Include Login Page ('/login')</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="my-4" style="color: var(--md-sys-color-on-surface); font-weight: 500;">
|
||||
<span style="margin: 0 10px;"><strong th:text="#{endpointStatistics.totalEndpoints} + ':'">Total Endpoints:</strong> <span id="totalEndpoints">0</span></span>
|
||||
<span style="margin: 0 10px;"><strong th:text="#{endpointStatistics.totalVisits} + ':'">Total Visits:</strong> <span id="totalVisits">0</span></span>
|
||||
<span style="margin: 0 10px;"><strong th:text="#{endpointStatistics.showing} + ':'">Showing:</strong> <span id="currentlyShowing">Top 10</span></span>
|
||||
<span style="margin: 0 10px;"><strong th:text="#{endpointStatistics.selectedVisits} + ':'">Selected Visits:</strong> <span id="displayedVisits">0</span> (<span id="displayedPercentage">0</span>%)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart Container -->
|
||||
<div class="bg-card mt-3 mb-3">
|
||||
<div class="chart-container">
|
||||
<canvas id="endpointChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table for detailed data -->
|
||||
<div class="bg-card mt-3 mb-3">
|
||||
<table class="endpoint-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 5%;">#</th>
|
||||
<th scope="col" style="width: 55%;" th:text="#{endpointStatistics.endpoint}">Endpoint</th>
|
||||
<th scope="col" style="width: 20%;" th:text="#{endpointStatistics.visits}">Visits</th>
|
||||
<th scope="col" style="width: 20%;" th:text="#{endpointStatistics.percentage}">Percentage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="endpointTableBody">
|
||||
<!-- Table rows will be dynamically generated -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" th:src="@{'/js/usage.js'}"></script>
|
||||
<script th:inline="javascript">
|
||||
const endpointStatsTranslations = {
|
||||
all: /*[[#{endpointStatistics.all}]]*/ 'All',
|
||||
top20: /*[[#{endpointStatistics.top20}]]*/ 'Top 20',
|
||||
loading: /*[[#{endpointStatistics.loading}]]*/ 'Loading...',
|
||||
failedToLoad: /*[[#{endpointStatistics.failedToLoad}]]*/ 'Failed to load endpoint data. Please try refreshing.',
|
||||
home: /*[[#{endpointStatistics.home}]]*/ 'Home',
|
||||
login: /*[[#{endpointStatistics.login}]]*/ 'Login',
|
||||
top: /*[[#{endpointStatistics.top}]]*/ 'Top ',
|
||||
numberOfVisits: /*[[#{endpointStatistics.numberOfVisits}]]*/ 'Number of Visits',
|
||||
visitsTooltip: /*[[#{endpointStatistics.visitsTooltip}]]*/ 'Visits: {0} ({1}% of total)',
|
||||
retry: /*[[#{endpointStatistics.retry}]]*/ 'Retry'
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,631 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
Copyright 2012 Mozilla Foundation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Adobe CMap resources are covered by their own copyright but the same license:
|
||||
|
||||
Copyright 1990-2015 Adobe Systems Incorporated.
|
||||
|
||||
See https://github.com/adobe-type-tools/cmap-resources
|
||||
-->
|
||||
<html dir="ltr" mozdisallowselectionprint th:lang="${#locale.language}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{redact.title}, header=#{redact.header})}"></th:block>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<meta name="google" content="notranslate">
|
||||
<title>PDF.js viewer</title>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<script th:src="@{'/js/thirdParty/popper.min.js'}"></script>
|
||||
<script th:src="@{'/js/thirdParty/bootstrap.min.js'}"></script>
|
||||
|
||||
<link rel="stylesheet" th:href="@{'/css/theme/componentes.css'}">
|
||||
<link rel="stylesheet" th:href="@{'/css/navbar.css'}">
|
||||
|
||||
<!-- This snippet is used in production (included from view-pdf.html) -->
|
||||
<link rel="resource" type="application/l10n" th:href="@{'/pdfjs-legacy/locale/locale.json'}">
|
||||
<script th:src="@{'/pdfjs-legacy/pdf.mjs'}" type="module"></script>
|
||||
|
||||
<link rel="stylesheet" th:href="@{'/pdfjs-legacy/css/viewer.css'}">
|
||||
<script th:src="@{'/pdfjs-legacy/js/viewer.mjs'}" type="module"></script>
|
||||
</head>
|
||||
|
||||
<body tabindex="1" style="overflow: hidden;">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<div id="outerContainer">
|
||||
<div id="sidebarContainer">
|
||||
<div id="toolbarSidebar">
|
||||
<div id="toolbarSidebarLeft">
|
||||
<div id="sidebarViewButtons" class="splitToolbarButton toggled" role="radiogroup">
|
||||
<button id="viewThumbnail" class="toolbarButton toggled" title="Show Thumbnails" tabindex="2"
|
||||
data-l10n-id="pdfjs-thumbs-button" role="radio" aria-checked="true" aria-controls="thumbnailView">
|
||||
<span data-l10n-id="pdfjs-thumbs-button-label">Thumbnails</span>
|
||||
</button>
|
||||
<button id="viewOutline" class="toolbarButton"
|
||||
title="Show Document Outline (double-click to expand/collapse all items)" tabindex="3"
|
||||
data-l10n-id="pdfjs-document-outline-button" role="radio" aria-checked="false"
|
||||
aria-controls="outlineView">
|
||||
<span data-l10n-id="pdfjs-document-outline-button-label">Document Outline</span>
|
||||
</button>
|
||||
<button id="viewAttachments" class="toolbarButton" title="Show Attachments" tabindex="4"
|
||||
data-l10n-id="pdfjs-attachments-button" role="radio" aria-checked="false" aria-controls="attachmentsView">
|
||||
<span data-l10n-id="pdfjs-attachments-button-label">Attachments</span>
|
||||
</button>
|
||||
<button id="viewLayers" class="toolbarButton"
|
||||
title="Show Layers (double-click to reset all layers to the default state)" tabindex="5"
|
||||
data-l10n-id="pdfjs-layers-button" role="radio" aria-checked="false" aria-controls="layersView">
|
||||
<span data-l10n-id="pdfjs-layers-button-label">Layers</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toolbarSidebarRight">
|
||||
<div id="outlineOptionsContainer">
|
||||
<div class="verticalToolbarSeparator"></div>
|
||||
|
||||
<button id="currentOutlineItem" class="toolbarButton" disabled="disabled" title="Find Current Outline Item"
|
||||
tabindex="6" data-l10n-id="pdfjs-current-outline-item-button">
|
||||
<span data-l10n-id="pdfjs-current-outline-item-button-label">Current Outline Item</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sidebarContent">
|
||||
<div id="thumbnailView">
|
||||
</div>
|
||||
<div id="outlineView" class="hidden">
|
||||
</div>
|
||||
<div id="attachmentsView" class="hidden">
|
||||
</div>
|
||||
<div id="layersView" class="hidden">
|
||||
</div>
|
||||
</div>
|
||||
<div id="sidebarResizer"></div>
|
||||
</div> <!-- sidebarContainer -->
|
||||
|
||||
<div id="mainContainer">
|
||||
<div class="findbar hidden doorHanger" id="findbar">
|
||||
<div id="findbarInputContainer">
|
||||
<span class="loadingInput end">
|
||||
<input id="findInput" class="toolbarField" title="Find" placeholder="Find in document…" tabindex="91"
|
||||
data-l10n-id="pdfjs-find-input" aria-invalid="false">
|
||||
</span>
|
||||
<div class="splitToolbarButton">
|
||||
<button id="findPrevious" class="toolbarButton" title="Find the previous occurrence of the phrase"
|
||||
tabindex="92" data-l10n-id="pdfjs-find-previous-button">
|
||||
<span data-l10n-id="pdfjs-find-previous-button-label">Previous</span>
|
||||
</button>
|
||||
<div class="splitToolbarButtonSeparator"></div>
|
||||
<button id="findNext" class="toolbarButton" title="Find the next occurrence of the phrase" tabindex="93"
|
||||
data-l10n-id="pdfjs-find-next-button">
|
||||
<span data-l10n-id="pdfjs-find-next-button-label">Next</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="findbarOptionsOneContainer">
|
||||
<input type="checkbox" id="findHighlightAll" class="toolbarField" tabindex="94">
|
||||
<label for="findHighlightAll" class="toolbarLabel" data-l10n-id="pdfjs-find-highlight-checkbox">Highlight
|
||||
All</label>
|
||||
<input type="checkbox" id="findMatchCase" class="toolbarField" tabindex="95">
|
||||
<label for="findMatchCase" class="toolbarLabel" data-l10n-id="pdfjs-find-match-case-checkbox-label">Match
|
||||
Case</label>
|
||||
</div>
|
||||
<div id="findbarOptionsTwoContainer">
|
||||
<input type="checkbox" id="findMatchDiacritics" class="toolbarField" tabindex="96">
|
||||
<label for="findMatchDiacritics" class="toolbarLabel"
|
||||
data-l10n-id="pdfjs-find-match-diacritics-checkbox-label">Match Diacritics</label>
|
||||
<input type="checkbox" id="findEntireWord" class="toolbarField" tabindex="97">
|
||||
<label for="findEntireWord" class="toolbarLabel" data-l10n-id="pdfjs-find-entire-word-checkbox-label">Whole
|
||||
Words</label>
|
||||
</div>
|
||||
|
||||
<div id="findbarMessageContainer" aria-live="polite">
|
||||
<span id="findResultsCount" class="toolbarLabel"></span>
|
||||
<span id="findMsg" class="toolbarLabel"></span>
|
||||
</div>
|
||||
</div> <!-- findbar -->
|
||||
|
||||
<div class="editorParamsToolbar hidden doorHangerRight" id="editorHighlightParamsToolbar">
|
||||
<div id="highlightParamsToolbarContainer" class="editorParamsToolbarContainer">
|
||||
<div id="editorHighlightColorPicker" class="colorPicker">
|
||||
<span id="highlightColorPickerLabel" class="editorParamsLabel"
|
||||
data-l10n-id="pdfjs-editor-highlight-colorpicker-label">Highlight color</span>
|
||||
</div>
|
||||
<div id="editorHighlightThickness">
|
||||
<label for="editorFreeHighlightThickness" class="editorParamsLabel"
|
||||
data-l10n-id="pdfjs-editor-free-highlight-thickness-input">Thickness</label>
|
||||
<div class="thicknessPicker">
|
||||
<input type="range" id="editorFreeHighlightThickness" class="editorParamsSlider"
|
||||
data-l10n-id="pdfjs-editor-free-highlight-thickness-title" value="12" min="8" max="24" step="1"
|
||||
tabindex="101">
|
||||
</div>
|
||||
</div>
|
||||
<div id="editorHighlightVisibility">
|
||||
<div class="divider"></div>
|
||||
<div class="toggler">
|
||||
<label for="editorHighlightShowAll" class="editorParamsLabel"
|
||||
data-l10n-id="pdfjs-editor-highlight-show-all-button-label">Show all</label>
|
||||
<button id="editorHighlightShowAll" class="toggle-button"
|
||||
data-l10n-id="pdfjs-editor-highlight-show-all-button" aria-pressed="true" tabindex="102"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editorParamsToolbar hidden doorHangerRight" id="editorFreeTextParamsToolbar">
|
||||
<div class="editorParamsToolbarContainer">
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorFreeTextColor" class="editorParamsLabel"
|
||||
data-l10n-id="pdfjs-editor-free-text-color-input">Color</label>
|
||||
<input type="color" id="editorFreeTextColor" class="editorParamsColor" tabindex="103">
|
||||
</div>
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorFreeTextFontSize" class="editorParamsLabel"
|
||||
data-l10n-id="pdfjs-editor-free-text-size-input">Size</label>
|
||||
<input type="range" id="editorFreeTextFontSize" class="editorParamsSlider" value="10" min="5" max="100"
|
||||
step="1" tabindex="104">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editorParamsToolbar hidden doorHangerRight" id="editorInkParamsToolbar">
|
||||
<div class="editorParamsToolbarContainer">
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorInkColor" class="editorParamsLabel"
|
||||
data-l10n-id="pdfjs-editor-ink-color-input">Color</label>
|
||||
<input type="color" id="editorInkColor" class="editorParamsColor" tabindex="105">
|
||||
</div>
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorInkThickness" class="editorParamsLabel"
|
||||
data-l10n-id="pdfjs-editor-ink-thickness-input">Thickness</label>
|
||||
<input type="range" id="editorInkThickness" class="editorParamsSlider" value="1" min="1" max="20" step="1"
|
||||
tabindex="106">
|
||||
</div>
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorInkOpacity" class="editorParamsLabel"
|
||||
data-l10n-id="pdfjs-editor-ink-opacity-input">Opacity</label>
|
||||
<input type="range" id="editorInkOpacity" class="editorParamsSlider" value="100" min="1" max="100" step="1"
|
||||
tabindex="107">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editorParamsToolbar hidden doorHangerRight" id="editorStampParamsToolbar">
|
||||
<div class="editorParamsToolbarContainer">
|
||||
<button id="editorStampAddImage" class="secondaryToolbarButton" title="Add image" tabindex="108"
|
||||
data-l10n-id="pdfjs-editor-stamp-add-image-button">
|
||||
<span class="editorParamsLabel" data-l10n-id="pdfjs-editor-stamp-add-image-button-label">Add image</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="secondaryToolbar" class="secondaryToolbar hidden doorHangerRight">
|
||||
<div id="secondaryToolbarButtonContainer">
|
||||
<button id="secondaryOpenFile" class="secondaryToolbarButton visibleMediumView" title="Open File"
|
||||
tabindex="51" data-l10n-id="pdfjs-open-file-button">
|
||||
<span data-l10n-id="pdfjs-open-file-button-label">Open</span>
|
||||
</button>
|
||||
|
||||
<button id="secondaryPrint" class="secondaryToolbarButton visibleMediumView" title="Print" tabindex="52"
|
||||
data-l10n-id="pdfjs-print-button">
|
||||
<span data-l10n-id="pdfjs-print-button-label">Print</span>
|
||||
</button>
|
||||
|
||||
<button id="secondaryDownload" class="secondaryToolbarButton visibleMediumView" title="Save" tabindex="53"
|
||||
data-l10n-id="pdfjs-save-button">
|
||||
<span data-l10n-id="pdfjs-save-button-label">Save</span>
|
||||
</button>
|
||||
|
||||
<div class="horizontalToolbarSeparator"></div>
|
||||
|
||||
<button id="presentationMode" class="secondaryToolbarButton" title="Switch to Presentation Mode" tabindex="54"
|
||||
data-l10n-id="pdfjs-presentation-mode-button">
|
||||
<span data-l10n-id="pdfjs-presentation-mode-button-label">Presentation Mode</span>
|
||||
</button>
|
||||
|
||||
<a href="#" id="viewBookmark" class="secondaryToolbarButton" title="Current Page (View URL from Current Page)"
|
||||
tabindex="55" data-l10n-id="pdfjs-bookmark-button">
|
||||
<span data-l10n-id="pdfjs-bookmark-button-label">Current Page</span>
|
||||
</a>
|
||||
|
||||
<div id="viewBookmarkSeparator" class="horizontalToolbarSeparator"></div>
|
||||
|
||||
<button id="firstPage" class="secondaryToolbarButton" title="Go to First Page" tabindex="56"
|
||||
data-l10n-id="pdfjs-first-page-button">
|
||||
<span data-l10n-id="pdfjs-first-page-button-label">Go to First Page</span>
|
||||
</button>
|
||||
<button id="lastPage" class="secondaryToolbarButton" title="Go to Last Page" tabindex="57"
|
||||
data-l10n-id="pdfjs-last-page-button">
|
||||
<span data-l10n-id="pdfjs-last-page-button-label">Go to Last Page</span>
|
||||
</button>
|
||||
|
||||
<div class="horizontalToolbarSeparator"></div>
|
||||
|
||||
<button id="pageRotateCw" class="secondaryToolbarButton" title="Rotate Clockwise" tabindex="58"
|
||||
data-l10n-id="pdfjs-page-rotate-cw-button">
|
||||
<span data-l10n-id="pdfjs-page-rotate-cw-button-label">Rotate Clockwise</span>
|
||||
</button>
|
||||
<button id="pageRotateCcw" class="secondaryToolbarButton" title="Rotate Counterclockwise" tabindex="59"
|
||||
data-l10n-id="pdfjs-page-rotate-ccw-button">
|
||||
<span data-l10n-id="pdfjs-page-rotate-ccw-button-label">Rotate Counterclockwise</span>
|
||||
</button>
|
||||
|
||||
<div class="horizontalToolbarSeparator"></div>
|
||||
|
||||
<div id="cursorToolButtons" role="radiogroup">
|
||||
<button id="cursorSelectTool" class="secondaryToolbarButton toggled" title="Enable Text Selection Tool"
|
||||
tabindex="60" data-l10n-id="pdfjs-cursor-text-select-tool-button" role="radio" aria-checked="true">
|
||||
<span data-l10n-id="pdfjs-cursor-text-select-tool-button-label">Text Selection Tool</span>
|
||||
</button>
|
||||
<button id="cursorHandTool" class="secondaryToolbarButton" title="Enable Hand Tool" tabindex="61"
|
||||
data-l10n-id="pdfjs-cursor-hand-tool-button" role="radio" aria-checked="false">
|
||||
<span data-l10n-id="pdfjs-cursor-hand-tool-button-label">Hand Tool</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="horizontalToolbarSeparator"></div>
|
||||
|
||||
<div id="scrollModeButtons" role="radiogroup">
|
||||
<button id="scrollPage" class="secondaryToolbarButton" title="Use Page Scrolling" tabindex="62"
|
||||
data-l10n-id="pdfjs-scroll-page-button" role="radio" aria-checked="false">
|
||||
<span data-l10n-id="pdfjs-scroll-page-button-label">Page Scrolling</span>
|
||||
</button>
|
||||
<button id="scrollVertical" class="secondaryToolbarButton toggled" title="Use Vertical Scrolling"
|
||||
tabindex="63" data-l10n-id="pdfjs-scroll-vertical-button" role="radio" aria-checked="true">
|
||||
<span data-l10n-id="pdfjs-scroll-vertical-button-label">Vertical Scrolling</span>
|
||||
</button>
|
||||
<button id="scrollHorizontal" class="secondaryToolbarButton" title="Use Horizontal Scrolling" tabindex="64"
|
||||
data-l10n-id="pdfjs-scroll-horizontal-button" role="radio" aria-checked="false">
|
||||
<span data-l10n-id="pdfjs-scroll-horizontal-button-label">Horizontal Scrolling</span>
|
||||
</button>
|
||||
<button id="scrollWrapped" class="secondaryToolbarButton" title="Use Wrapped Scrolling" tabindex="65"
|
||||
data-l10n-id="pdfjs-scroll-wrapped-button" role="radio" aria-checked="false">
|
||||
<span data-l10n-id="pdfjs-scroll-wrapped-button-label">Wrapped Scrolling</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="horizontalToolbarSeparator"></div>
|
||||
|
||||
<div id="spreadModeButtons" role="radiogroup">
|
||||
<button id="spreadNone" class="secondaryToolbarButton toggled" title="Do not join page spreads"
|
||||
tabindex="66" data-l10n-id="pdfjs-spread-none-button" role="radio" aria-checked="true">
|
||||
<span data-l10n-id="pdfjs-spread-none-button-label">No Spreads</span>
|
||||
</button>
|
||||
<button id="spreadOdd" class="secondaryToolbarButton"
|
||||
title="Join page spreads starting with odd-numbered pages" tabindex="67"
|
||||
data-l10n-id="pdfjs-spread-odd-button" role="radio" aria-checked="false">
|
||||
<span data-l10n-id="pdfjs-spread-odd-button-label">Odd Spreads</span>
|
||||
</button>
|
||||
<button id="spreadEven" class="secondaryToolbarButton"
|
||||
title="Join page spreads starting with even-numbered pages" tabindex="68"
|
||||
data-l10n-id="pdfjs-spread-even-button" role="radio" aria-checked="false">
|
||||
<span data-l10n-id="pdfjs-spread-even-button-label">Even Spreads</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="horizontalToolbarSeparator"></div>
|
||||
|
||||
<button id="documentProperties" class="secondaryToolbarButton" title="Document Properties…" tabindex="69"
|
||||
data-l10n-id="pdfjs-document-properties-button" aria-controls="documentPropertiesDialog">
|
||||
<span data-l10n-id="pdfjs-document-properties-button-label">Document Properties…</span>
|
||||
</button>
|
||||
</div>
|
||||
</div> <!-- secondaryToolbar -->
|
||||
|
||||
<div class="toolbar">
|
||||
<div id="toolbarContainer">
|
||||
<div id="toolbarViewer">
|
||||
<div id="toolbarViewerLeft">
|
||||
<button id="sidebarToggle" class="toolbarButton" title="Toggle Sidebar" tabindex="11"
|
||||
data-l10n-id="pdfjs-toggle-sidebar-button" aria-expanded="false" aria-controls="sidebarContainer">
|
||||
<span data-l10n-id="pdfjs-toggle-sidebar-button-label">Toggle Sidebar</span>
|
||||
</button>
|
||||
<div class="toolbarButtonSpacer"></div>
|
||||
<button id="viewFind" class="toolbarButton" title="Find in Document" tabindex="12"
|
||||
data-l10n-id="pdfjs-findbar-button" aria-expanded="false" aria-controls="findbar">
|
||||
<span data-l10n-id="pdfjs-findbar-button-label">Find</span>
|
||||
</button>
|
||||
<div class="splitToolbarButton hiddenSmallView">
|
||||
<button class="toolbarButton" title="Previous Page" id="previous" tabindex="13"
|
||||
data-l10n-id="pdfjs-previous-button">
|
||||
<span data-l10n-id="pdfjs-previous-button-label">Previous</span>
|
||||
</button>
|
||||
<div class="splitToolbarButtonSeparator"></div>
|
||||
<button class="toolbarButton" title="Next Page" id="next" tabindex="14"
|
||||
data-l10n-id="pdfjs-next-button">
|
||||
<span data-l10n-id="pdfjs-next-button-label">Next</span>
|
||||
</button>
|
||||
</div>
|
||||
<span class="loadingInput start">
|
||||
<input type="number" id="pageNumber" class="toolbarField" title="Page" value="1" min="1" tabindex="15"
|
||||
data-l10n-id="pdfjs-page-input" autocomplete="off">
|
||||
</span>
|
||||
<span id="numPages" class="toolbarLabel"></span>
|
||||
</div>
|
||||
<div id="toolbarViewerRight">
|
||||
<div id="editorModeButtons" class="splitToolbarButton toggled" role="radiogroup">
|
||||
<a id="backToHome" class="toolbarButton hiddenMediumView" title="Back to Main Page" role="radio"
|
||||
aria-checked="false" tabindex="31" th:href="@{'/'}">
|
||||
<span data-l10n-id="pdfjs-open-file-button-label">Back to Main Page</span>
|
||||
</a>
|
||||
<button id="editorHighlight" class="toolbarButton hiddenMediumView" title="Highlight" role="radio"
|
||||
aria-checked="false" tabindex="32" aria-expanded="false" aria-controls="editorHighlightParamsToolbar">
|
||||
<span data-l10n-id="pdfjs-editor-highlight-button-label">Highlight</span>
|
||||
</button>
|
||||
<button id="openFile" class="toolbarButton hiddenMediumView" title="Open File" role="radio"
|
||||
aria-checked="false" tabindex="33" data-l10n-id="pdfjs-open-file-button">
|
||||
<span data-l10n-id="pdfjs-open-file-button-label">Open</span>
|
||||
</button>
|
||||
<button id="print" class="toolbarButton hiddenMediumView" title="Print" role="radio"
|
||||
aria-checked="false" tabindex="34" data-l10n-id="pdfjs-print-button">
|
||||
<span data-l10n-id="pdfjs-print-button-label">Print</span>
|
||||
</button>
|
||||
<button id="download" class="toolbarButton hiddenMediumView" title="Save" role="radio"
|
||||
aria-checked="false" tabindex="35" data-l10n-id="pdfjs-save-button">
|
||||
<span data-l10n-id="pdfjs-save-button-label">Save</span>
|
||||
</button>
|
||||
<button id="editorStamp" class="toolbarButton hiddenMediumView" title="Add or edit images" role="radio"
|
||||
aria-checked="false" aria-controls="editorStampParamsToolbar" tabindex="36"
|
||||
data-l10n-id="pdfjs-editor-stamp-button">
|
||||
<span data-l10n-id="pdfjs-editor-stamp-button-label">Add or edit images</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="editorModeSeparator" class="verticalToolbarSeparator"></div>
|
||||
<button id="editorFreeText" class="toolbarButton hiddenMediumView" title="Text" tabindex="41"
|
||||
data-l10n-id="pdfjs-editor-free-text-button">
|
||||
<span data-l10n-id="pdfjs-editor-free-text-button-label">Text</span>
|
||||
</button>
|
||||
|
||||
<button id="editorInk" class="toolbarButton hiddenMediumView" title="Draw" tabindex="42"
|
||||
data-l10n-id="pdfjs-editor-ink-button">
|
||||
<span data-l10n-id="pdfjs-editor-ink-button-label">Draw</span>
|
||||
</button>
|
||||
|
||||
<div class="verticalToolbarSeparator hiddenMediumView"></div>
|
||||
|
||||
<button id="secondaryToolbarToggle" class="toolbarButton" title="Tools" tabindex="43"
|
||||
data-l10n-id="pdfjs-tools-button">
|
||||
<span data-l10n-id="pdfjs-tools-button-label">Tools</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="toolbarViewerMiddle">
|
||||
<div class="splitToolbarButton">
|
||||
<button id="zoomOut" class="toolbarButton" title="Zoom Out" tabindex="21"
|
||||
data-l10n-id="pdfjs-zoom-out-button">
|
||||
<span data-l10n-id="pdfjs-zoom-out-button-label">Zoom Out</span>
|
||||
</button>
|
||||
<div class="splitToolbarButtonSeparator"></div>
|
||||
<button id="zoomIn" class="toolbarButton" title="Zoom In" tabindex="22"
|
||||
data-l10n-id="pdfjs-zoom-in-button">
|
||||
<span data-l10n-id="pdfjs-zoom-in-button-label">Zoom In</span>
|
||||
</button>
|
||||
</div>
|
||||
<span id="scaleSelectContainer" class="dropdownToolbarButton">
|
||||
<select id="scaleSelect" title="Zoom" tabindex="23" data-l10n-id="pdfjs-zoom-select">
|
||||
<option id="pageAutoOption" title="" value="auto" selected="selected"
|
||||
data-l10n-id="pdfjs-page-scale-auto">Automatic Zoom</option>
|
||||
<option id="pageActualOption" title="" value="page-actual" data-l10n-id="pdfjs-page-scale-actual">
|
||||
Actual Size</option>
|
||||
<option id="pageFitOption" title="" value="page-fit" data-l10n-id="pdfjs-page-scale-fit">Page Fit
|
||||
</option>
|
||||
<option id="pageWidthOption" title="" value="page-width" data-l10n-id="pdfjs-page-scale-width">Page
|
||||
Width</option>
|
||||
<option id="customScaleOption" title="" value="custom" disabled="disabled" hidden="true"
|
||||
data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 0 }'>0%</option>
|
||||
<option title="" value="0.5" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 50 }'>
|
||||
50%</option>
|
||||
<option title="" value="0.75" data-l10n-id="pdfjs-page-scale-percent"
|
||||
data-l10n-args='{ "scale": 75 }'>75%</option>
|
||||
<option title="" value="1" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 100 }'>
|
||||
100%</option>
|
||||
<option title="" value="1.25" data-l10n-id="pdfjs-page-scale-percent"
|
||||
data-l10n-args='{ "scale": 125 }'>125%</option>
|
||||
<option title="" value="1.5" data-l10n-id="pdfjs-page-scale-percent"
|
||||
data-l10n-args='{ "scale": 150 }'>150%</option>
|
||||
<option title="" value="2" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 200 }'>
|
||||
200%</option>
|
||||
<option title="" value="3" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 300 }'>
|
||||
300%</option>
|
||||
<option title="" value="4" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 400 }'>
|
||||
400%</option>
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="loadingBar">
|
||||
<div class="progress">
|
||||
<div class="glimmer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="viewerContainer" tabindex="0">
|
||||
<div id="viewer" class="pdfViewer">
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
|
||||
</div>
|
||||
</div> <!-- mainContainer -->
|
||||
|
||||
<div id="dialogContainer">
|
||||
<dialog id="passwordDialog">
|
||||
<div class="row">
|
||||
<label for="password" id="passwordText" data-l10n-id="pdfjs-password-label">Enter the password to open this
|
||||
PDF file:</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="password" id="password" class="toolbarField">
|
||||
</div>
|
||||
<div class="buttonRow">
|
||||
<button id="passwordCancel" class="dialogButton"><span
|
||||
data-l10n-id="pdfjs-password-cancel-button">Cancel</span></button>
|
||||
<button id="passwordSubmit" class="dialogButton"><span
|
||||
data-l10n-id="pdfjs-password-ok-button">OK</span></button>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="documentPropertiesDialog">
|
||||
<div class="row">
|
||||
<span id="fileNameLabel" data-l10n-id="pdfjs-document-properties-file-name">File name:</span>
|
||||
<p id="fileNameField" aria-labelledby="fileNameLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="fileSizeLabel" data-l10n-id="pdfjs-document-properties-file-size">File size:</span>
|
||||
<p id="fileSizeField" aria-labelledby="fileSizeLabel">-</p>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div class="row">
|
||||
<span id="titleLabel" data-l10n-id="pdfjs-document-properties-title">Title:</span>
|
||||
<p id="titleField" aria-labelledby="titleLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="authorLabel" data-l10n-id="pdfjs-document-properties-author">Author:</span>
|
||||
<p id="authorField" aria-labelledby="authorLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="subjectLabel" data-l10n-id="pdfjs-document-properties-subject">Subject:</span>
|
||||
<p id="subjectField" aria-labelledby="subjectLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="keywordsLabel" data-l10n-id="pdfjs-document-properties-keywords">Keywords:</span>
|
||||
<p id="keywordsField" aria-labelledby="keywordsLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="creationDateLabel" data-l10n-id="pdfjs-document-properties-creation-date">Creation Date:</span>
|
||||
<p id="creationDateField" aria-labelledby="creationDateLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="modificationDateLabel" data-l10n-id="pdfjs-document-properties-modification-date">Modification
|
||||
Date:</span>
|
||||
<p id="modificationDateField" aria-labelledby="modificationDateLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="creatorLabel" data-l10n-id="pdfjs-document-properties-creator">Creator:</span>
|
||||
<p id="creatorField" aria-labelledby="creatorLabel">-</p>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div class="row">
|
||||
<span id="producerLabel" data-l10n-id="pdfjs-document-properties-producer">PDF Producer:</span>
|
||||
<p id="producerField" aria-labelledby="producerLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="versionLabel" data-l10n-id="pdfjs-document-properties-version">PDF Version:</span>
|
||||
<p id="versionField" aria-labelledby="versionLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="pageCountLabel" data-l10n-id="pdfjs-document-properties-page-count">Page Count:</span>
|
||||
<p id="pageCountField" aria-labelledby="pageCountLabel">-</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="pageSizeLabel" data-l10n-id="pdfjs-document-properties-page-size">Page Size:</span>
|
||||
<p id="pageSizeField" aria-labelledby="pageSizeLabel">-</p>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div class="row">
|
||||
<span id="linearizedLabel" data-l10n-id="pdfjs-document-properties-linearized">Fast Web View:</span>
|
||||
<p id="linearizedField" aria-labelledby="linearizedLabel">-</p>
|
||||
</div>
|
||||
<div class="buttonRow">
|
||||
<button id="documentPropertiesClose" class="dialogButton"><span
|
||||
data-l10n-id="pdfjs-document-properties-close-button">Close</span></button>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog class="dialog altText" id="altTextDialog" aria-labelledby="dialogLabel"
|
||||
aria-describedby="dialogDescription">
|
||||
<div id="altTextContainer" class="mainContainer">
|
||||
<div id="overallDescription">
|
||||
<span id="dialogLabel" data-l10n-id="pdfjs-editor-alt-text-dialog-label" class="title">Choose an
|
||||
option</span>
|
||||
<span id="dialogDescription" data-l10n-id="pdfjs-editor-alt-text-dialog-description">
|
||||
Alt text (alternative text) helps when people can’t see the image or when it doesn’t load.
|
||||
</span>
|
||||
</div>
|
||||
<div id="addDescription">
|
||||
<div class="radio">
|
||||
<div class="radioButton">
|
||||
<input type="radio" id="descriptionButton" name="altTextOption" tabindex="0"
|
||||
aria-describedby="descriptionAreaLabel" checked>
|
||||
<label for="descriptionButton" data-l10n-id="pdfjs-editor-alt-text-add-description-label">Add a
|
||||
description</label>
|
||||
</div>
|
||||
<div class="radioLabel">
|
||||
<span id="descriptionAreaLabel" data-l10n-id="pdfjs-editor-alt-text-add-description-description">
|
||||
Aim for 1-2 sentences that describe the subject, setting, or actions.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="descriptionArea">
|
||||
<textarea id="descriptionTextarea"
|
||||
placeholder="For example, “A young man sits down at a table to eat a meal”"
|
||||
aria-labelledby="descriptionAreaLabel" data-l10n-id="pdfjs-editor-alt-text-textarea"
|
||||
tabindex="0"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div id="markAsDecorative">
|
||||
<div class="radio">
|
||||
<div class="radioButton">
|
||||
<input type="radio" id="decorativeButton" name="altTextOption" aria-describedby="decorativeLabel">
|
||||
<label for="decorativeButton" data-l10n-id="pdfjs-editor-alt-text-mark-decorative-label">Mark as
|
||||
decorative</label>
|
||||
</div>
|
||||
<div class="radioLabel">
|
||||
<span id="decorativeLabel" data-l10n-id="pdfjs-editor-alt-text-mark-decorative-description">
|
||||
This is used for ornamental images, like borders or watermarks.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="buttons">
|
||||
<button id="altTextCancel" class="secondaryButton" tabindex="0"><span
|
||||
data-l10n-id="pdfjs-editor-alt-text-cancel-button">Cancel</span></button>
|
||||
<button id="altTextSave" class="primaryButton" tabindex="0"><span
|
||||
data-l10n-id="pdfjs-editor-alt-text-save-button">Save</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="printServiceDialog" style="min-width: 200px;">
|
||||
<div class="row">
|
||||
<span data-l10n-id="pdfjs-print-progress-message">Preparing document for printing…</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<progress value="0" max="100"></progress>
|
||||
<span data-l10n-id="pdfjs-print-progress-percent" data-l10n-args='{ "progress": 0 }'
|
||||
class="relative-progress">0%</span>
|
||||
</div>
|
||||
<div class="buttonRow">
|
||||
<button id="printCancel" class="dialogButton"><span
|
||||
data-l10n-id="pdfjs-print-progress-close-button">Cancel</span></button>
|
||||
</div>
|
||||
</dialog>
|
||||
</div> <!-- dialogContainer -->
|
||||
|
||||
</div> <!-- outerContainer -->
|
||||
<div id="printContainer"></div>
|
||||
</body>
|
||||
<style>
|
||||
*,
|
||||
::after,
|
||||
::before {
|
||||
box-sizing: content-box !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user