mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
refactor: remove legacy Thymeleaf web UI controllers and templates (#5406)
# Description of Changes This pull request completes the removal of Thymeleaf template engine support and documentation from the Stirling-PDF codebase and developer guides, reflecting the project's full migration to a React-based frontend. It removes all references to Thymeleaf in code, configuration, and documentation, and updates guides to focus exclusively on the React SPA architecture. This streamlines the codebase and clarifies the development workflow for contributors. **Codebase cleanup: Thymeleaf removal** - Removed all commented-out Thymeleaf dependencies and related configuration beans from `build.gradle` and `AppConfig.java` (`app/common/build.gradle`, `app/common/src/main/java/stirling/software/common/configuration/AppConfig.java`) [[1]](diffhunk://#diff-2a1a21726f33b05d16451237c68d6df91a5f4a58419d839715f3f1538a9a14aeL32) [[2]](diffhunk://#diff-70792df9a0ab5675ded888c9eb8e2815c780d7b39f4bda8cf2da51d1b336899aL67-L76). - Fully commented out (as a precursor to future deletion) the `FileFallbackTemplateResolver` and `InputStreamTemplateResource` classes, which were only used for Thymeleaf template resolution (`app/common/src/main/java/stirling/software/common/configuration/FileFallbackTemplateResolver.java`, `app/common/src/main/java/stirling/software/common/model/InputStreamTemplateResource.java`) [[1]](diffhunk://#diff-e2bc7614074316b972355cb7dda47b98f75b00eb6b2ca4f143a680ab2803dcd8L1-L49) [[2]](diffhunk://#diff-ab10ee12d8de8fb77759e931170373d388bde04bad6d0e42a0ab674355ef7ef3L1-L40). **Documentation updates: React-only focus** - Removed all instructions and references to migrating or developing with Thymeleaf templates in `DeveloperGuide.md` and `ADDING_TOOLS.md`, including detailed Thymeleaf usage examples, migration steps, and translation key usage in templates [[1]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL5-R5) [[2]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL41-L43) [[3]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL103-L105) [[4]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL157) [[5]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL312) [[6]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL404-R396) [[7]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL451-L505) [[8]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL530-R467) [[9]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL585-L669) [[10]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL699-L709) [[11]](diffhunk://#diff-e2f8148ea620602b7761e8ee24afeac1c577476630528e210fe0b22e950016ddL3-R3) [[12]](diffhunk://#diff-e2f8148ea620602b7761e8ee24afeac1c577476630528e210fe0b22e950016ddL267-R267). - Updated architecture descriptions in `CLAUDE.md` to reflect that the frontend is now exclusively a React SPA and that Thymeleaf templates have been fully replaced [[1]](diffhunk://#diff-6ebdb617a8104a7756d0cf36578ab01103dc9f07e4dc6feb751296b9c402faf7L131-R132) [[2]](diffhunk://#diff-6ebdb617a8104a7756d0cf36578ab01103dc9f07e4dc6feb751296b9c402faf7L143-L144). **Labeler configuration update** - Removed labeler rules for files related to the old Thymeleaf-based web controllers and UI directories, as these are now obsolete (`.github/labeler-config-srvaroa.yml`). These changes ensure the codebase and documentation are consistent with the new React-only frontend approach, reducing maintenance overhead and potential confusion for contributors. --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### 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:
@@ -63,9 +63,6 @@ labels:
|
||||
files:
|
||||
- 'app/core/src/main/resources/static/.*'
|
||||
- 'app/proprietary/src/main/resources/static/.*'
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/controller/web/.*'
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/UI/.*'
|
||||
- 'app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/.*'
|
||||
- 'frontend/**'
|
||||
- 'frontend/.*'
|
||||
- 'frontend/**/.*'
|
||||
|
||||
+4
-12
@@ -1,6 +1,6 @@
|
||||
# Adding New React Tools to Stirling PDF
|
||||
|
||||
This guide covers how to add new PDF tools to the React frontend, either by migrating existing Thymeleaf templates or creating entirely new tools.
|
||||
This guide covers how to add new PDF tools to the React frontend.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -188,7 +188,7 @@ import { use[ToolName]Tips } from "../components/tooltips/use[ToolName]Tips";
|
||||
|
||||
const [ToolName] = (props: BaseToolProps) => {
|
||||
const tips = use[ToolName]Tips();
|
||||
|
||||
|
||||
// In your steps array:
|
||||
steps: [
|
||||
{
|
||||
@@ -257,22 +257,14 @@ Update translation files. **Important: Only update `en-GB` files** - other langu
|
||||
- Add `options.*` keys if your tool has settings with descriptions
|
||||
|
||||
**Tooltip Writing Guidelines:**
|
||||
- **Use simple, everyday language** - avoid technical terms like "converts interactive elements"
|
||||
- **Use simple, everyday language** - avoid technical terms like "converts interactive elements"
|
||||
- **Focus on benefits** - explain what the user gains, not how it works internally
|
||||
- **Use concrete examples** - "text boxes become regular text" vs "form fields are flattened"
|
||||
- **Answer user questions** - "What does this do?", "When should I use this?", "What's this option for?"
|
||||
- **Keep descriptions concise** - 1-2 sentences maximum per section
|
||||
- **Use bullet points** for multiple benefits or features
|
||||
|
||||
## 6. Migration from Thymeleaf
|
||||
When migrating existing Thymeleaf templates:
|
||||
|
||||
1. **Identify Form Parameters**: Look at the original `<form>` inputs to determine parameter structure
|
||||
2. **Extract Translation Keys**: Find `#{key.name}` references and add them to JSON translations (For many tools these translations will already exist but some parts will be missing)
|
||||
3. **Map API Endpoint**: Note the `th:action` URL for the operation hook
|
||||
4. **Preserve Functionality**: Ensure all original form behaviour is replicated which is applicable to V2 react UI
|
||||
|
||||
## 7. Testing Your Tool
|
||||
## 6. Testing Your Tool
|
||||
- Verify tool appears in UI with correct icon and description
|
||||
- Test with various file sizes and types
|
||||
- Confirm translations work
|
||||
|
||||
@@ -128,8 +128,8 @@ return useToolOperation({
|
||||
## Architecture Overview
|
||||
|
||||
### Project Structure
|
||||
- **Backend**: Spring Boot application with Thymeleaf templating
|
||||
- **Frontend**: React-based SPA in `/frontend` directory (Thymeleaf templates fully replaced)
|
||||
- **Backend**: Spring Boot application
|
||||
- **Frontend**: React-based SPA in `/frontend` directory
|
||||
- **File Storage**: IndexedDB for client-side file persistence and thumbnails
|
||||
- **Internationalization**: JSON-based translations (converted from backend .properties)
|
||||
- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering
|
||||
@@ -140,8 +140,6 @@ return useToolOperation({
|
||||
- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations
|
||||
- Organized by function: converters, security, misc, pipeline
|
||||
- Follow pattern: `@RestController` + `@RequestMapping("/api/v1/...")`
|
||||
- **Web Controllers** (`src/main/java/.../controller/web/`): Serve Thymeleaf templates
|
||||
- Pattern: `@Controller` + return template names
|
||||
|
||||
### Key Components
|
||||
- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic
|
||||
|
||||
+3
-162
@@ -2,7 +2,7 @@
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
Stirling-PDF is a robust, locally hosted, web-based PDF manipulation tool. **Stirling 2.0** represents a complete frontend rewrite, replacing the legacy Thymeleaf-based UI with a modern React SPA (Single Page Application).
|
||||
Stirling-PDF is a robust, locally hosted, web-based PDF manipulation tool. **Stirling 2.0** represents a complete frontend rewrite with a modern React SPA (Single Page Application).
|
||||
|
||||
This guide focuses on developing for Stirling 2.0, including both the React frontend and Spring Boot backend development workflows.
|
||||
|
||||
@@ -38,9 +38,6 @@ This guide focuses on developing for Stirling 2.0, including both the React fron
|
||||
- PDF file association support
|
||||
- Self-contained JRE bundling with JLink
|
||||
|
||||
**Legacy (reference only during development):**
|
||||
- Thymeleaf templates (being completely replaced in 2.0)
|
||||
|
||||
## 3. Development Environment Setup
|
||||
|
||||
### Prerequisites
|
||||
@@ -100,9 +97,6 @@ Stirling 2.0 uses client-side file storage:
|
||||
- **PDF.js**: Handles client-side PDF rendering and processing
|
||||
- **URL Parameters**: Support for deep linking and tool state persistence
|
||||
|
||||
### Legacy Code Reference
|
||||
The existing Thymeleaf templates remain in the codebase during development as reference material but will be completely removed for the 2.0 release.
|
||||
|
||||
### Tauri Desktop App Development
|
||||
Stirling-PDF can be packaged as a cross-platform desktop application using Tauri with PDF file association support and bundled JRE.
|
||||
See [the frontend README](frontend/README.md#tauri) for build instructions.
|
||||
@@ -154,7 +148,6 @@ Stirling-PDF/
|
||||
│ │ │ ├── css/
|
||||
│ │ │ ├── js/
|
||||
│ │ │ └── pdfjs/
|
||||
│ │ └── templates/ # Legacy Thymeleaf templates (reference only)
|
||||
│ └── test/
|
||||
├── testing/ # Cucumber and integration tests
|
||||
│ └── cucumber/ # Cucumber test files
|
||||
@@ -309,7 +302,6 @@ For quick iterations and development of Java backend, JavaScript, and UI compone
|
||||
- RESTful API endpoints
|
||||
- JavaScript functionality
|
||||
- User interface components and styling
|
||||
- Thymeleaf templates
|
||||
|
||||
To run Stirling-PDF locally:
|
||||
|
||||
@@ -401,7 +393,7 @@ Remember to test your changes thoroughly to ensure they don't break any existing
|
||||
|
||||
### React Component Development (Stirling 2.0)
|
||||
|
||||
For Stirling 2.0, new features are built as React components instead of Thymeleaf templates:
|
||||
For Stirling 2.0, new features are built as React components:
|
||||
|
||||
#### Creating a New Tool Component
|
||||
|
||||
@@ -448,61 +440,6 @@ For Stirling 2.0, new features are built as React components instead of Thymelea
|
||||
3. **Register in Tool Picker:**
|
||||
Update the tool picker component to include the new tool with proper routing and URL parameter support.
|
||||
|
||||
### Legacy Reference: Overview of Thymeleaf
|
||||
|
||||
Thymeleaf is a server-side Java HTML template engine. It is used in Stirling-PDF to render dynamic web pages. Thymeleaf integrates heavily with Spring Boot.
|
||||
|
||||
### Thymeleaf overview
|
||||
|
||||
In Stirling-PDF, Thymeleaf is used to create HTML templates that are rendered on the server side. These templates are located in the `stirling-pdf/src/main/resources/templates` directory. Thymeleaf templates use a combination of HTML and special Thymeleaf attributes to dynamically generate content.
|
||||
|
||||
Some examples of this are:
|
||||
|
||||
```html
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
```
|
||||
or
|
||||
```html
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
```
|
||||
|
||||
Where it uses the `th:block`, `th:` indicating it's a special Thymeleaf element to be used server-side in generating the HTML, and block being the actual element type.
|
||||
In this case, we are inserting the `navbar` entry within the `fragments/navbar.html` fragment into the `th:block` element.
|
||||
|
||||
They can be more complex, such as:
|
||||
|
||||
```html
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block>
|
||||
```
|
||||
|
||||
Which is the same as above but passes the parameters title and header into the fragment `common.html` to be used in its HTML generation.
|
||||
|
||||
Thymeleaf can also be used to loop through objects or pass things from the Java side into the HTML side.
|
||||
|
||||
```java
|
||||
@GetMapping
|
||||
public String newFeaturePage(Model model) {
|
||||
model.addAttribute("exampleData", exampleData);
|
||||
return "new-feature";
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, if exampleData is a list of plain java objects of class Person and within it, you had id, name, age, etc. You can reference it like so
|
||||
|
||||
```html
|
||||
<tbody>
|
||||
<!-- Use th:each to iterate over the list -->
|
||||
<tr th:each="person : ${exampleData}">
|
||||
<td th:text="${person.id}"></td>
|
||||
<td th:text="${person.name}"></td>
|
||||
<td th:text="${person.age}"></td>
|
||||
<td th:text="${person.email}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
```
|
||||
|
||||
This would generate n entries of tr for each person in exampleData
|
||||
|
||||
### Adding a New Feature to the Backend (API)
|
||||
|
||||
1. **Create a New Controller:**
|
||||
@@ -527,7 +464,7 @@ This would generate n entries of tr for each person in exampleData
|
||||
@GetMapping
|
||||
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
|
||||
public String newFeature() {
|
||||
return "NewFeatureResponse"; // This refers to the NewFeatureResponse.html template presenting the user with the generated html from that file when they navigate to /api/v1/new-feature
|
||||
return "NewFeatureResponse";
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -582,91 +519,6 @@ This would generate n entries of tr for each person in exampleData
|
||||
}
|
||||
```
|
||||
|
||||
### Adding a New Feature to the Frontend (UI)
|
||||
|
||||
1. **Create a New Thymeleaf Template:**
|
||||
- Create a new HTML file in the `stirling-pdf/src/main/resources/templates` directory.
|
||||
- Use Thymeleaf attributes to dynamically generate content.
|
||||
- Use `extract-page.html` as a base example for the HTML template, which is useful to ensure importing of the general layout, navbar, and footer.
|
||||
|
||||
```html
|
||||
<!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=#{newFeature.title}, header=#{newFeature.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">upload</span>
|
||||
<span class="tool-header-text" th:text="#{newFeature.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'/api/v1/new-feature'}" 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="featureInput" th:text="#{newFeature.prompt}"></label>
|
||||
<input type="text" class="form-control" id="featureInput" name="featureInput" th:placeholder="#{newFeature.placeholder}" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{newFeature.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
2. **Create a New Controller for the UI:**
|
||||
- Create a new Java class in the `stirling-pdf/src/main/java/stirling/software/SPDF/controller/ui` directory.
|
||||
- Annotate the class with `@Controller` and `@RequestMapping` to define the UI endpoint.
|
||||
|
||||
```java
|
||||
package stirling.software.SPDF.controller.ui;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import stirling.software.SPDF.service.NewFeatureService;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/new-feature")
|
||||
public class NewFeatureUIController {
|
||||
|
||||
@Autowired
|
||||
private NewFeatureService newFeatureService;
|
||||
|
||||
@GetMapping
|
||||
public String newFeaturePage(Model model) {
|
||||
model.addAttribute("newFeatureData", newFeatureService.getNewFeatureData());
|
||||
return "new-feature";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Update the Navigation Bar:**
|
||||
- Add a link to the new feature page in the navigation bar.
|
||||
- Update the `stirling-pdf/src/main/resources/templates/fragments/navbar.html` file.
|
||||
|
||||
```html
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" th:href="@{'/new-feature'}">New Feature</a>
|
||||
</li>
|
||||
```
|
||||
|
||||
## Adding New Translations to Existing Language Files in Stirling-PDF
|
||||
|
||||
When adding a new feature or modifying existing ones in Stirling-PDF, you'll need to add new translation entries to the existing language files. Here's a step-by-step guide:
|
||||
@@ -696,15 +548,4 @@ pdfSplitter.input.pages=Enter page numbers to split
|
||||
|
||||
Add these entries to the default GB language file and any others you wish, translating the values as appropriate for each language.
|
||||
|
||||
### 3. Use Translations in Thymeleaf Templates
|
||||
|
||||
In your Thymeleaf templates, use the `#{key}` syntax to reference the new translations:
|
||||
|
||||
```html
|
||||
<h1 th:text="#{pdfSplitter.title}">PDF Splitter</h1>
|
||||
<p th:text="#{pdfSplitter.description}">Split your PDF into multiple documents</p>
|
||||
<input type="text" th:placeholder="#{pdfSplitter.input.pages}">
|
||||
<button th:text="#{pdfSplitter.button.split}">Split PDF</button>
|
||||
```
|
||||
|
||||
Remember, never hard-code text in your templates or Java code. Always use translation keys to ensure proper localization.
|
||||
|
||||
@@ -29,7 +29,6 @@ spotless {
|
||||
dependencies {
|
||||
api 'org.springframework.boot:spring-boot-starter-web'
|
||||
api 'org.springframework.boot:spring-boot-starter-aop'
|
||||
// api 'org.springframework.boot:spring-boot-starter-thymeleaf' // Deprecated - UI moved to React frontend
|
||||
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||
api 'com.fathzer:javaluator:3.0.6'
|
||||
api 'com.posthog.java:posthog:1.2.0'
|
||||
|
||||
@@ -64,16 +64,6 @@ public class AppConfig {
|
||||
return v2Enabled;
|
||||
}
|
||||
|
||||
/* Commented out Thymeleaf template engine bean - to be removed when frontend migration is complete
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
|
||||
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
||||
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
||||
templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader));
|
||||
return templateEngine;
|
||||
}
|
||||
*/
|
||||
|
||||
@Bean(name = "loginEnabled")
|
||||
public boolean loginEnabled() {
|
||||
return applicationProperties.getSecurity().isEnableLogin();
|
||||
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
/* Commented out entire FileFallbackTemplateResolver class - Thymeleaf dependency removed
|
||||
* This class will be removed when frontend migration to React is complete
|
||||
|
||||
|
||||
@Slf4j
|
||||
public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateResolver {
|
||||
|
||||
private final ResourceLoader resourceLoader;
|
||||
|
||||
public FileFallbackTemplateResolver(ResourceLoader resourceLoader) {
|
||||
super();
|
||||
this.resourceLoader = resourceLoader;
|
||||
setSuffix(".html");
|
||||
}
|
||||
|
||||
// Note this does not work in local IDE, Prod jar only.
|
||||
@Override
|
||||
protected ITemplateResource computeTemplateResource(
|
||||
IEngineConfiguration configuration,
|
||||
String ownerTemplate,
|
||||
String template,
|
||||
String resourceName,
|
||||
String characterEncoding,
|
||||
Map<String, Object> templateResolutionAttributes) {
|
||||
Resource resource =
|
||||
resourceLoader.getResource(
|
||||
"file:" + InstallationPathConfig.getTemplatesPath() + resourceName);
|
||||
try {
|
||||
if (resource.exists() && resource.isReadable()) {
|
||||
return new FileTemplateResource(resource.getFile().getPath(), characterEncoding);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Log the exception to help with debugging issues loading external templates
|
||||
log.warn("Unable to read template '{}' from file system", resourceName, e);
|
||||
}
|
||||
|
||||
InputStream inputStream =
|
||||
Thread.currentThread()
|
||||
.getContextClassLoader()
|
||||
.getResourceAsStream("templates/" + resourceName);
|
||||
if (inputStream != null) {
|
||||
return new InputStreamTemplateResource(inputStream, "UTF-8");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
*/
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
/* Commented out entire InputStreamTemplateResource class - Thymeleaf dependency removed
|
||||
* This class will be removed when frontend migration to React is complete
|
||||
|
||||
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public class InputStreamTemplateResource implements ITemplateResource {
|
||||
private final InputStream inputStream;
|
||||
private final String characterEncoding;
|
||||
|
||||
@Override
|
||||
public Reader reader() throws IOException {
|
||||
return new InputStreamReader(inputStream, characterEncoding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ITemplateResource relative(String relativeLocation) {
|
||||
// Implement logic for relative resources, if needed
|
||||
throw new UnsupportedOperationException("Relative resources not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "InputStream resource [Stream]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseName() {
|
||||
return "streamResource";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return inputStream != null;
|
||||
}
|
||||
}
|
||||
*/
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
/* Commented out - InputStreamTemplateResource class removed with Thymeleaf migration
|
||||
* This test will be removed when frontend migration to React is complete
|
||||
|
||||
|
||||
public class InputStreamTemplateResourceTest {
|
||||
|
||||
@Test
|
||||
void gettersReturnProvidedFields() {
|
||||
byte[] data = {1, 2, 3};
|
||||
InputStream is = new ByteArrayInputStream(data);
|
||||
String encoding = "UTF-8";
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, encoding);
|
||||
|
||||
assertSame(is, resource.getInputStream());
|
||||
assertEquals(encoding, resource.getCharacterEncoding());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldsAreFinal() throws NoSuchFieldException {
|
||||
Field inputStreamField = InputStreamTemplateResource.class.getDeclaredField("inputStream");
|
||||
Field characterEncodingField =
|
||||
InputStreamTemplateResource.class.getDeclaredField("characterEncoding");
|
||||
|
||||
assertTrue(Modifier.isFinal(inputStreamField.getModifiers()));
|
||||
assertTrue(Modifier.isFinal(characterEncodingField.getModifiers()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noSetterMethodsPresent() {
|
||||
long setterCount =
|
||||
Arrays.stream(InputStreamTemplateResource.class.getDeclaredMethods())
|
||||
.filter(method -> method.getName().startsWith("set"))
|
||||
.count();
|
||||
|
||||
assertEquals(0, setterCount, "InputStreamTemplateResource should not have setter methods");
|
||||
}
|
||||
|
||||
@Test
|
||||
void readerReturnsCorrectContent() throws Exception {
|
||||
String content = "Hello, world!";
|
||||
InputStream is = new ByteArrayInputStream(content.getBytes("UTF-8"));
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||
|
||||
try (Reader reader = resource.reader()) {
|
||||
char[] buffer = new char[content.length()];
|
||||
int read = reader.read(buffer);
|
||||
assertEquals(content.length(), read);
|
||||
assertEquals(content, new String(buffer));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void relativeThrowsUnsupportedOperationException() {
|
||||
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||
assertThrows(UnsupportedOperationException.class, () -> resource.relative("other"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDescriptionReturnsExpectedString() {
|
||||
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||
assertEquals("InputStream resource [Stream]", resource.getDescription());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBaseNameReturnsExpectedString() {
|
||||
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||
assertEquals("streamResource", resource.getBaseName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsReturnsTrueWhenInputStreamNotNull() {
|
||||
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||
assertTrue(resource.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsReturnsFalseWhenInputStreamIsNull() {
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(null, "UTF-8");
|
||||
assertFalse(resource.exists());
|
||||
}
|
||||
}
|
||||
*/
|
||||
-221
@@ -1,221 +0,0 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.ApplicationContextProvider;
|
||||
import stirling.software.common.util.CheckProgramInstall;
|
||||
|
||||
@Controller
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ConverterWebController {
|
||||
|
||||
@GetMapping("/img-to-pdf")
|
||||
@Hidden
|
||||
public String convertImgToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "img-to-pdf");
|
||||
return "convert/img-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/cbz-to-pdf")
|
||||
@Hidden
|
||||
public String convertCbzToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "cbz-to-pdf");
|
||||
return "convert/cbz-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-cbz")
|
||||
@Hidden
|
||||
public String convertPdfToCbzForm(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-cbz");
|
||||
return "convert/pdf-to-cbz";
|
||||
}
|
||||
|
||||
@GetMapping("/cbr-to-pdf")
|
||||
@Hidden
|
||||
public String convertCbrToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "cbr-to-pdf");
|
||||
return "convert/cbr-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/ebook-to-pdf")
|
||||
@Hidden
|
||||
public String convertEbookToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "ebook-to-pdf");
|
||||
return "convert/ebook-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-epub")
|
||||
@Hidden
|
||||
public String convertPdfToEpubForm(Model model) {
|
||||
if (!ApplicationContextProvider.getBean(EndpointConfiguration.class)
|
||||
.isEndpointEnabled("pdf-to-epub")) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
model.addAttribute("currentPage", "pdf-to-epub");
|
||||
return "convert/pdf-to-epub";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-cbr")
|
||||
@Hidden
|
||||
public String convertPdfToCbrForm(Model model) {
|
||||
if (!ApplicationContextProvider.getBean(EndpointConfiguration.class)
|
||||
.isEndpointEnabled("pdf-to-cbr")) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
model.addAttribute("currentPage", "pdf-to-cbr");
|
||||
return "convert/pdf-to-cbr";
|
||||
}
|
||||
|
||||
@GetMapping("/html-to-pdf")
|
||||
@Hidden
|
||||
public String convertHTMLToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "html-to-pdf");
|
||||
return "convert/html-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/markdown-to-pdf")
|
||||
@Hidden
|
||||
public String convertMarkdownToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "markdown-to-pdf");
|
||||
return "convert/markdown-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-markdown")
|
||||
@Hidden
|
||||
public String convertPdfToMarkdownForm(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-markdown");
|
||||
return "convert/pdf-to-markdown";
|
||||
}
|
||||
|
||||
@GetMapping("/url-to-pdf")
|
||||
@Hidden
|
||||
public String convertURLToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "url-to-pdf");
|
||||
return "convert/url-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/file-to-pdf")
|
||||
@Hidden
|
||||
public String convertToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "file-to-pdf");
|
||||
return "convert/file-to-pdf";
|
||||
}
|
||||
|
||||
// PDF TO......
|
||||
|
||||
@GetMapping("/pdf-to-img")
|
||||
@Hidden
|
||||
public String pdfToimgForm(Model model) {
|
||||
boolean isPython = CheckProgramInstall.isPythonAvailable();
|
||||
ApplicationProperties properties =
|
||||
ApplicationContextProvider.getBean(ApplicationProperties.class);
|
||||
if (properties != null && properties.getSystem() != null) {
|
||||
model.addAttribute("maxDPI", properties.getSystem().getMaxDPI());
|
||||
} else {
|
||||
model.addAttribute("maxDPI", 500); // Default value if not set
|
||||
}
|
||||
model.addAttribute("isPython", isPython);
|
||||
model.addAttribute("currentPage", "pdf-to-img");
|
||||
return "convert/pdf-to-img";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-html")
|
||||
@Hidden
|
||||
public ModelAndView pdfToHTML() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-html");
|
||||
modelAndView.addObject("currentPage", "pdf-to-html");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-presentation")
|
||||
@Hidden
|
||||
public ModelAndView pdfToPresentation() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-presentation");
|
||||
modelAndView.addObject("currentPage", "pdf-to-presentation");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-text")
|
||||
@Hidden
|
||||
public ModelAndView pdfToText() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-text");
|
||||
modelAndView.addObject("currentPage", "pdf-to-text");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-word")
|
||||
@Hidden
|
||||
public ModelAndView pdfToWord() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-word");
|
||||
modelAndView.addObject("currentPage", "pdf-to-word");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-xml")
|
||||
@Hidden
|
||||
public ModelAndView pdfToXML() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-xml");
|
||||
modelAndView.addObject("currentPage", "pdf-to-xml");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-csv")
|
||||
@Hidden
|
||||
public ModelAndView pdfToCSV() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-csv");
|
||||
modelAndView.addObject("currentPage", "pdf-to-csv");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-pdfa")
|
||||
@Hidden
|
||||
public String pdfToPdfAForm(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-pdfa");
|
||||
return "convert/pdf-to-pdfa";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-vector")
|
||||
@Hidden
|
||||
public String pdfToVectorForm(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-vector");
|
||||
return "convert/pdf-to-vector";
|
||||
}
|
||||
|
||||
@GetMapping("/vector-to-pdf")
|
||||
@Hidden
|
||||
public String vectorToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "vector-to-pdf");
|
||||
return "convert/vector-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/eml-to-pdf")
|
||||
@Hidden
|
||||
public String convertEmlToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "eml-to-pdf");
|
||||
return "convert/eml-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-video")
|
||||
@Hidden
|
||||
public String pdfToVideo(Model model) {
|
||||
ApplicationProperties properties =
|
||||
ApplicationContextProvider.getBean(ApplicationProperties.class);
|
||||
if (properties != null && properties.getSystem() != null) {
|
||||
model.addAttribute("maxDPI", properties.getSystem().getMaxDPI());
|
||||
} else {
|
||||
model.addAttribute("maxDPI", 500);
|
||||
}
|
||||
model.addAttribute("currentPage", "pdf-to-video");
|
||||
return "convert/pdf-to-video";
|
||||
}
|
||||
}
|
||||
-352
@@ -1,352 +0,0 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.ui.Model;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.SignatureFile;
|
||||
import stirling.software.SPDF.service.SharedSignatureService;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.service.UserServiceInterface;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@Slf4j
|
||||
public class GeneralWebController {
|
||||
|
||||
private final SharedSignatureService signatureService;
|
||||
private final UserServiceInterface userService;
|
||||
private final ResourceLoader resourceLoader;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
|
||||
public GeneralWebController(
|
||||
SharedSignatureService signatureService,
|
||||
@Autowired(required = false) UserServiceInterface userService,
|
||||
ResourceLoader resourceLoader,
|
||||
RuntimePathConfig runtimePathConfig) {
|
||||
this.signatureService = signatureService;
|
||||
this.userService = userService;
|
||||
this.resourceLoader = resourceLoader;
|
||||
this.runtimePathConfig = runtimePathConfig;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/pipeline")
|
||||
@Hidden
|
||||
public String pipelineForm(Model model) {
|
||||
model.addAttribute("currentPage", "pipeline");
|
||||
List<String> pipelineConfigs = new ArrayList<>();
|
||||
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
|
||||
if (new File(runtimePathConfig.getPipelineDefaultWebUiConfigs()).exists()) {
|
||||
try (Stream<Path> paths =
|
||||
Files.walk(Paths.get(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) {
|
||||
List<Path> jsonFiles =
|
||||
paths.filter(Files::isRegularFile)
|
||||
.filter(p -> p.toString().endsWith(".json"))
|
||||
.toList();
|
||||
for (Path jsonFile : jsonFiles) {
|
||||
String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
|
||||
pipelineConfigs.add(content);
|
||||
}
|
||||
for (String config : pipelineConfigs) {
|
||||
Map<String, Object> jsonContent =
|
||||
new ObjectMapper()
|
||||
.readValue(config, new TypeReference<Map<String, Object>>() {});
|
||||
String name = (String) jsonContent.get("name");
|
||||
if (name == null || name.isEmpty()) {
|
||||
String filename =
|
||||
jsonFiles
|
||||
.get(pipelineConfigs.indexOf(config))
|
||||
.getFileName()
|
||||
.toString();
|
||||
name = filename.substring(0, filename.lastIndexOf('.'));
|
||||
}
|
||||
Map<String, String> configWithName = new HashMap<>();
|
||||
configWithName.put("json", config);
|
||||
configWithName.put("name", name);
|
||||
pipelineConfigsWithNames.add(configWithName);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
}
|
||||
if (pipelineConfigsWithNames.isEmpty()) {
|
||||
Map<String, String> configWithName = new HashMap<>();
|
||||
configWithName.put("json", "");
|
||||
configWithName.put("name", "No preloaded configs found");
|
||||
pipelineConfigsWithNames.add(configWithName);
|
||||
}
|
||||
model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames);
|
||||
model.addAttribute("pipelineConfigs", pipelineConfigs);
|
||||
return "pipeline";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/merge-pdfs")
|
||||
@Hidden
|
||||
public String mergePdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "merge-pdfs");
|
||||
return "merge-pdfs";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/split-pdf-by-sections")
|
||||
@Hidden
|
||||
public String splitPdfBySections(Model model) {
|
||||
model.addAttribute("currentPage", "split-pdf-by-sections");
|
||||
return "split-pdf-by-sections";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/split-pdf-by-chapters")
|
||||
@Hidden
|
||||
public String splitPdfByChapters(Model model) {
|
||||
model.addAttribute("currentPage", "split-pdf-by-chapters");
|
||||
return "split-pdf-by-chapters";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/view-pdf")
|
||||
@Hidden
|
||||
public String ViewPdfForm2(Model model) {
|
||||
model.addAttribute("currentPage", "view-pdf");
|
||||
return "view-pdf";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/edit-table-of-contents")
|
||||
@Hidden
|
||||
public String editTableOfContents(Model model) {
|
||||
model.addAttribute("currentPage", "edit-table-of-contents");
|
||||
return "edit-table-of-contents";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/multi-tool")
|
||||
@Hidden
|
||||
public String multiToolForm(Model model) {
|
||||
model.addAttribute("currentPage", "multi-tool");
|
||||
return "multi-tool";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-pages")
|
||||
@Hidden
|
||||
public String pageDeleter(Model model) {
|
||||
model.addAttribute("currentPage", "remove-pages");
|
||||
return "remove-pages";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/pdf-organizer")
|
||||
@Hidden
|
||||
public String pageOrganizer(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-organizer");
|
||||
return "pdf-organizer";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/extract-page")
|
||||
@Hidden
|
||||
public String extractPages(Model model) {
|
||||
model.addAttribute("currentPage", "extract-page");
|
||||
return "extract-page";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/pdf-to-single-page")
|
||||
@Hidden
|
||||
public String pdfToSinglePage(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-single-page");
|
||||
return "pdf-to-single-page";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/rotate-pdf")
|
||||
@Hidden
|
||||
public String rotatePdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "rotate-pdf");
|
||||
return "rotate-pdf";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/split-pdfs")
|
||||
@Hidden
|
||||
public String splitPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "split-pdfs");
|
||||
return "split-pdfs";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/sign")
|
||||
@Hidden
|
||||
public String signForm(Model model) {
|
||||
String username = "";
|
||||
if (userService != null) {
|
||||
username = userService.getCurrentUsername();
|
||||
}
|
||||
// Get signatures from both personal and ALL_USERS folders
|
||||
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
|
||||
model.addAttribute("currentPage", "sign");
|
||||
model.addAttribute("fonts", getFontNames());
|
||||
model.addAttribute("signatures", signatures);
|
||||
return "sign";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/multi-page-layout")
|
||||
@Hidden
|
||||
public String multiPageLayoutForm(Model model) {
|
||||
model.addAttribute("currentPage", "multi-page-layout");
|
||||
return "multi-page-layout";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/scale-pages")
|
||||
@Hidden
|
||||
public String scalePagesFrom(Model model) {
|
||||
model.addAttribute("currentPage", "scale-pages");
|
||||
return "scale-pages";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/split-by-size-or-count")
|
||||
@Hidden
|
||||
public String splitBySizeOrCount(Model model) {
|
||||
model.addAttribute("currentPage", "split-by-size-or-count");
|
||||
return "split-by-size-or-count";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/overlay-pdf")
|
||||
@Hidden
|
||||
public String overlayPdf(Model model) {
|
||||
model.addAttribute("currentPage", "overlay-pdf");
|
||||
return "overlay-pdf";
|
||||
}
|
||||
|
||||
private List<FontResource> getFontNames() {
|
||||
List<FontResource> fontNames = new ArrayList<>();
|
||||
// Extract font names from classpath
|
||||
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
|
||||
// Extract font names from external directory
|
||||
fontNames.addAll(
|
||||
getFontNamesFromLocation(
|
||||
"file:"
|
||||
+ InstallationPathConfig.getStaticPath()
|
||||
+ "fonts"
|
||||
+ File.separator
|
||||
+ "*"));
|
||||
return fontNames;
|
||||
}
|
||||
|
||||
private List<FontResource> getFontNamesFromLocation(String locationPattern) {
|
||||
try {
|
||||
Resource[] resources =
|
||||
GeneralUtils.getResourcesFromLocationPattern(locationPattern, resourceLoader);
|
||||
return Arrays.stream(resources)
|
||||
.map(
|
||||
resource -> {
|
||||
try {
|
||||
String filename = resource.getFilename();
|
||||
if (filename != null) {
|
||||
int lastDotIndex = filename.lastIndexOf('.');
|
||||
if (lastDotIndex != -1) {
|
||||
String name = filename.substring(0, lastDotIndex);
|
||||
String extension = filename.substring(lastDotIndex + 1);
|
||||
return new FontResource(name, extension);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
throw ExceptionUtils.createRuntimeException(
|
||||
"error.fontLoadingFailed",
|
||||
"Error processing font file",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
} catch (Exception e) {
|
||||
throw ExceptionUtils.createRuntimeException(
|
||||
"error.fontDirectoryReadFailed", "Failed to read font directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getFormatFromExtension(String extension) {
|
||||
return switch (extension) {
|
||||
case "ttf" -> "truetype";
|
||||
case "woff" -> "woff";
|
||||
case "woff2" -> "woff2";
|
||||
case "eot" -> "embedded-opentype";
|
||||
case "svg" -> "svg";
|
||||
default ->
|
||||
// or throw an exception if an unexpected extension is encountered
|
||||
"";
|
||||
};
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/crop")
|
||||
@Hidden
|
||||
public String cropForm(Model model) {
|
||||
model.addAttribute("currentPage", "crop");
|
||||
return "crop";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/auto-split-pdf")
|
||||
@Hidden
|
||||
public String autoSPlitPDFForm(Model model) {
|
||||
model.addAttribute("currentPage", "auto-split-pdf");
|
||||
return "auto-split-pdf";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-image-pdf")
|
||||
@Hidden
|
||||
public String removeImagePdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-image-pdf");
|
||||
return "remove-image-pdf";
|
||||
}
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
public class FontResource {
|
||||
|
||||
private String name;
|
||||
|
||||
private String extension;
|
||||
|
||||
private String type;
|
||||
|
||||
public FontResource(String name, String extension) {
|
||||
this.name = name;
|
||||
this.extension = extension;
|
||||
this.type = getFormatFromExtension(extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.Dependency;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
@Slf4j
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@RequiredArgsConstructor
|
||||
public class HomeWebController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/about")
|
||||
@Hidden
|
||||
public String gameForm(Model model) {
|
||||
model.addAttribute("currentPage", "about");
|
||||
return "about";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/licenses")
|
||||
@Hidden
|
||||
public String licensesForm(Model model) {
|
||||
model.addAttribute("currentPage", "licenses");
|
||||
Resource resource = new ClassPathResource("static/3rdPartyLicenses.json");
|
||||
try (InputStream is = resource.getInputStream()) {
|
||||
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
Map<String, List<Dependency>> data = mapper.readValue(json, new TypeReference<>() {});
|
||||
model.addAttribute("dependencies", data.get("dependencies"));
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
return "licenses";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/releases")
|
||||
public String getReleaseNotes(Model model) {
|
||||
return "releases";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/")
|
||||
public String home(Model model) {
|
||||
model.addAttribute("currentPage", "home");
|
||||
String showSurvey = System.getenv("SHOW_SURVEY");
|
||||
boolean showSurveyValue = showSurvey == null || "true".equalsIgnoreCase(showSurvey);
|
||||
model.addAttribute("showSurveyFromDocker", showSurveyValue);
|
||||
return "home";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/home")
|
||||
public String root(Model model) {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/home-legacy")
|
||||
public String redirectHomeLegacy() {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@ResponseBody
|
||||
@Hidden
|
||||
public String getRobotsTxt() {
|
||||
boolean allowGoogle = applicationProperties.getSystem().isGooglevisibility();
|
||||
if (allowGoogle) {
|
||||
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /";
|
||||
} else {
|
||||
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.CheckProgramInstall;
|
||||
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class OtherWebController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/compress-pdf")
|
||||
@Hidden
|
||||
public String compressPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "compress-pdf");
|
||||
return "misc/compress-pdf";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/replace-and-invert-color-pdf")
|
||||
@Hidden
|
||||
public String replaceAndInvertColorPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "replace-invert-color-pdf");
|
||||
return "misc/replace-color";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/extract-image-scans")
|
||||
@Hidden
|
||||
public ModelAndView extractImageScansForm() {
|
||||
ModelAndView modelAndView = new ModelAndView("misc/extract-image-scans");
|
||||
boolean isPython = CheckProgramInstall.isPythonAvailable();
|
||||
modelAndView.addObject("isPython", isPython);
|
||||
modelAndView.addObject("currentPage", "extract-image-scans");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/show-javascript")
|
||||
@Hidden
|
||||
public String extractJavascriptForm(Model model) {
|
||||
model.addAttribute("currentPage", "show-javascript");
|
||||
return "misc/show-javascript";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/stamp")
|
||||
@Hidden
|
||||
public String stampForm(Model model) {
|
||||
model.addAttribute("currentPage", "stamp");
|
||||
return "misc/stamp";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/add-page-numbers")
|
||||
@Hidden
|
||||
public String addPageNumbersForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-page-numbers");
|
||||
return "misc/add-page-numbers";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/scanner-effect")
|
||||
@Hidden
|
||||
public String scannerEffectForm(Model model) {
|
||||
model.addAttribute("currentPage", "scanner-effect");
|
||||
return "misc/scanner-effect";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/extract-images")
|
||||
@Hidden
|
||||
public String extractImagesForm(Model model) {
|
||||
model.addAttribute("currentPage", "extract-images");
|
||||
return "misc/extract-images";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/flatten")
|
||||
@Hidden
|
||||
public String flattenForm(Model model) {
|
||||
model.addAttribute("currentPage", "flatten");
|
||||
return "misc/flatten";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/change-metadata")
|
||||
@Hidden
|
||||
public String addWatermarkForm(Model model) {
|
||||
model.addAttribute("currentPage", "change-metadata");
|
||||
return "misc/change-metadata";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/unlock-pdf-forms")
|
||||
@Hidden
|
||||
public String unlockPDFForms(Model model) {
|
||||
model.addAttribute("currentPage", "unlock-pdf-forms");
|
||||
return "misc/unlock-pdf-forms";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/compare")
|
||||
@Hidden
|
||||
public String compareForm(Model model) {
|
||||
model.addAttribute("currentPage", "compare");
|
||||
return "misc/compare";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/print-file")
|
||||
@Hidden
|
||||
public String printFileForm(Model model) {
|
||||
model.addAttribute("currentPage", "print-file");
|
||||
return "misc/print-file";
|
||||
}
|
||||
|
||||
public List<String> getAvailableTesseractLanguages() {
|
||||
String tessdataDir = runtimePathConfig.getTessDataPath();
|
||||
File[] files = new File(tessdataDir).listFiles();
|
||||
if (files == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Arrays.stream(files)
|
||||
.filter(file -> file.getName().endsWith(".traineddata"))
|
||||
.map(file -> file.getName().replace(".traineddata", ""))
|
||||
.filter(lang -> !"osd".equalsIgnoreCase(lang))
|
||||
.sorted()
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/ocr-pdf")
|
||||
@Hidden
|
||||
public ModelAndView ocrPdfPage() {
|
||||
ModelAndView modelAndView = new ModelAndView("misc/ocr-pdf");
|
||||
List<String> languages = getAvailableTesseractLanguages();
|
||||
modelAndView.addObject("languages", languages);
|
||||
modelAndView.addObject("currentPage", "ocr-pdf");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/add-image")
|
||||
@Hidden
|
||||
public String overlayImage(Model model) {
|
||||
model.addAttribute("currentPage", "add-image");
|
||||
return "misc/add-image";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/adjust-contrast")
|
||||
@Hidden
|
||||
public String contrast(Model model) {
|
||||
model.addAttribute("currentPage", "adjust-contrast");
|
||||
return "misc/adjust-contrast";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/repair")
|
||||
@Hidden
|
||||
public String repairForm(Model model) {
|
||||
model.addAttribute("currentPage", "repair");
|
||||
return "misc/repair";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-blanks")
|
||||
@Hidden
|
||||
public String removeBlanksForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-blanks");
|
||||
return "misc/remove-blanks";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-annotations")
|
||||
@Hidden
|
||||
public String removeAnnotationsForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-annotations");
|
||||
return "misc/remove-annotations";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/auto-crop")
|
||||
@Hidden
|
||||
public String autoCropForm(Model model) {
|
||||
model.addAttribute("currentPage", "auto-crop");
|
||||
return "misc/auto-crop";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/auto-rename")
|
||||
@Hidden
|
||||
public String autoRenameForm(Model model) {
|
||||
model.addAttribute("currentPage", "auto-rename");
|
||||
return "misc/auto-rename";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/add-attachments")
|
||||
@Hidden
|
||||
public String attachmentsForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-attachments");
|
||||
return "misc/add-attachments";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/extract-attachments")
|
||||
@Hidden
|
||||
public String extractAttachmentsForm(Model model) {
|
||||
model.addAttribute("currentPage", "extract-attachments");
|
||||
return "misc/extract-attachments";
|
||||
}
|
||||
}
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import org.springframework.ui.Model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
public class SecurityWebController {
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/auto-redact")
|
||||
@Hidden
|
||||
public String autoRedactForm(Model model) {
|
||||
model.addAttribute("currentPage", "auto-redact");
|
||||
return "security/auto-redact";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/redact")
|
||||
public String redactForm(Model model) {
|
||||
model.addAttribute("currentPage", "redact");
|
||||
return "security/redact";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/add-password")
|
||||
@Hidden
|
||||
public String addPasswordForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-password");
|
||||
return "security/add-password";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/change-permissions")
|
||||
@Hidden
|
||||
public String permissionsForm(Model model) {
|
||||
model.addAttribute("currentPage", "change-permissions");
|
||||
return "security/change-permissions";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-password")
|
||||
@Hidden
|
||||
public String removePasswordForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-password");
|
||||
return "security/remove-password";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/add-watermark")
|
||||
@Hidden
|
||||
public String addWatermarkForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-watermark");
|
||||
return "security/add-watermark";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/cert-sign")
|
||||
@Hidden
|
||||
public String certSignForm(Model model) {
|
||||
model.addAttribute("currentPage", "cert-sign");
|
||||
return "security/cert-sign";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/validate-signature")
|
||||
@Hidden
|
||||
public String certSignVerifyForm(Model model) {
|
||||
model.addAttribute("currentPage", "validate-signature");
|
||||
return "security/validate-signature";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/remove-cert-sign")
|
||||
@Hidden
|
||||
public String certUnSignForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-cert-sign");
|
||||
return "security/remove-cert-sign";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/sanitize-pdf")
|
||||
@Hidden
|
||||
public String sanitizeForm(Model model) {
|
||||
model.addAttribute("currentPage", "sanitize-pdf");
|
||||
return "security/sanitize-pdf";
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
// @GetMapping("/get-info-on-pdf")
|
||||
@Hidden
|
||||
public String getInfo(Model model) {
|
||||
model.addAttribute("currentPage", "get-info-on-pdf");
|
||||
return "security/get-info-on-pdf";
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ server.servlet.context-path=${SYSTEM_ROOTURIPATH:/}
|
||||
spring.devtools.restart.enabled=true
|
||||
spring.devtools.livereload.enabled=true
|
||||
spring.devtools.restart.exclude=stirling.software.proprietary.security/**
|
||||
# spring.thymeleaf.encoding=UTF-8 # Disabled - React frontend replaces Thymeleaf
|
||||
spring.web.resources.mime-mappings.webmanifest=application/manifest+json
|
||||
spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,479 +0,0 @@
|
||||
<!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('stirling_jwt') ||
|
||||
key.includes('JSESSIONID') ||
|
||||
key.includes('XSRF-TOKEN') ||
|
||||
key.includes('remember-me') ||
|
||||
key.includes('auth') ||
|
||||
key.includes('token') ||
|
||||
key.includes('session') ||
|
||||
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>
|
||||
@@ -1,458 +0,0 @@
|
||||
<!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 id="forceChange" name="forceChange" type="checkbox">
|
||||
<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>
|
||||
@@ -1,62 +0,0 @@
|
||||
<!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 id="duplexMode" name="duplexMode" type="checkbox">
|
||||
<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>
|
||||
@@ -1,88 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,44 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:data-language="${#locale.toString()}" th:dir="#{language.direction}" th:lang="${#locale.language}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{cbrToPDF.title}, header=#{cbrToPDF.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">auto_stories</span>
|
||||
<span class="tool-header-text" th:text="#{cbrToPDF.header}"></span>
|
||||
</div>
|
||||
<form enctype="multipart/form-data" id="cbrToPDFForm" method="post"
|
||||
th:action="@{'/api/v1/convert/cbr/pdf'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.cbr,.rar', inputText=#{cbrToPDF.selectText})}">
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3" th:if="${@endpointConfiguration.isGroupEnabled('Ghostscript')}">
|
||||
<input id="optimizeForEbook" name="optimizeForEbook" type="checkbox" value="true">
|
||||
<label for="optimizeForEbook" th:text="#{cbrToPDF.optimizeForEbook}">Optimize PDF for ebook readers (uses Ghostscript)</label>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button class="btn btn-primary" id="submitBtn" th:text="#{cbrToPDF.submit}" type="submit">Convert to PDF</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,44 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:data-language="${#locale.toString()}" th:dir="#{language.direction}" th:lang="${#locale.language}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{cbzToPDF.title}, header=#{cbzToPDF.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">auto_stories</span>
|
||||
<span class="tool-header-text" th:text="#{cbzToPDF.header}"></span>
|
||||
</div>
|
||||
<form enctype="multipart/form-data" id="cbzToPDFForm" method="post"
|
||||
th:action="@{'/api/v1/convert/cbz/pdf'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.cbz,.zip', inputText=#{cbzToPDF.selectText})}">
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3" th:if="${@endpointConfiguration.isGroupEnabled('Ghostscript')}">
|
||||
<input id="optimizeForEbook" name="optimizeForEbook" type="checkbox" value="true">
|
||||
<label for="optimizeForEbook" th:text="#{cbzToPDF.optimizeForEbook}">Optimize PDF for ebook readers (uses Ghostscript)</label>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button class="btn btn-primary" id="submitBtn" th:text="#{cbzToPDF.submit}" type="submit">Convert to PDF</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,82 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:data-language="${#locale.toString()}"
|
||||
th:dir="#{language.direction}"
|
||||
th:lang="${#locale.language}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{ebookToPDF.title}, header=#{ebookToPDF.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">menu_book</span>
|
||||
<span class="tool-header-text"
|
||||
th:text="#{ebookToPDF.header}"></span>
|
||||
</div>
|
||||
<p th:text="#{processTimeWarning}"></p>
|
||||
|
||||
<div class="alert alert-warning"
|
||||
th:if="${[email protected]('Calibre')}">
|
||||
<span th:text="#{ebookToPDF.calibreDisabled}">Calibre support is disabled.</span>
|
||||
</div>
|
||||
|
||||
<form enctype="multipart/form-data"
|
||||
id="ebookToPDFForm"
|
||||
method="post"
|
||||
th:action="@{'/api/v1/convert/ebook/pdf'}"
|
||||
th:if="${@endpointConfiguration.isGroupEnabled('Calibre')}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.epub,.mobi,.azw3,.fb2,.txt,.docx', inputText=#{ebookToPDF.selectText})}">
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input id="embedAllFonts" name="embedAllFonts" type="checkbox" value="true">
|
||||
<label for="embedAllFonts" th:text="#{ebookToPDF.embedAllFonts}"></label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input id="includeTableOfContents"
|
||||
name="includeTableOfContents"
|
||||
type="checkbox"
|
||||
value="true">
|
||||
<label for="includeTableOfContents" th:text="#{ebookToPDF.includeTableOfContents}"></label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input id="includePageNumbers" name="includePageNumbers" type="checkbox" value="true">
|
||||
<label for="includePageNumbers" th:text="#{ebookToPDF.includePageNumbers}"></label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3"
|
||||
th:if="${@endpointConfiguration.isGroupEnabled('Ghostscript')}">
|
||||
<input id="optimizeForEbook"
|
||||
name="optimizeForEbook"
|
||||
type="checkbox"
|
||||
value="true">
|
||||
<label for="optimizeForEbook" th:text="#{ebookToPDF.optimizeForEbook}"></label>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary"
|
||||
id="submitBtn"
|
||||
th:text="#{ebookToPDF.submit}"
|
||||
type="submit">Convert to
|
||||
PDF</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,97 +0,0 @@
|
||||
<!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 id="downloadHtml" name="downloadHtml" type="checkbox">
|
||||
<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 checked id="includeAttachments" name="includeAttachments" type="checkbox">
|
||||
<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>
|
||||
@@ -1,60 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,43 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,127 +0,0 @@
|
||||
<!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/*,.psd', 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 id="autoRotate" name="autoRotate" type="checkbox">
|
||||
<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>
|
||||
@@ -1,39 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,45 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:data-language="${#locale.toString()}" th:dir="#{language.direction}" th:lang="${#locale.language}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pdfToCBR.title}, header=#{pdfToCBR.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">auto_stories</span>
|
||||
<span class="tool-header-text" th:text="#{pdfToCBR.header}"></span>
|
||||
</div>
|
||||
<form enctype="multipart/form-data" id="pdfToCBRForm" method="post"
|
||||
th:action="@{'/api/v1/convert/pdf/cbr'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.pdf', inputText=#{pdfToCBR.selectText})}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="dpi" th:text="#{pdfToCBR.dpi}">DPI:</label>
|
||||
<input class="form-control" id="dpi" max="600" min="50" name="dpi" type="number" value="300">
|
||||
<div class="form-text" th:text="#{pdfToCBR.dpiHelp}">Higher DPI results in better quality but larger file size.</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button class="btn btn-primary" id="submitBtn" th:text="#{pdfToCBR.submit}" type="submit">Convert to CBR</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,42 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:data-language="${#locale.toString()}" th:dir="#{language.direction}" th:lang="${#locale.language}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pdfToCBZ.title}, header=#{pdfToCBZ.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">auto_stories</span>
|
||||
<span class="tool-header-text" th:text="#{pdfToCBZ.header}"></span>
|
||||
</div>
|
||||
<form enctype="multipart/form-data" id="pdfToCBZForm" method="post"
|
||||
th:action="@{'/api/v1/convert/pdf/cbz'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf', inputText=#{pdfToCBZ.selectText})}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dpi" th:text="#{pdfToCBZ.dpi}">DPI:</label>
|
||||
<input class="form-control" id="dpi" max="600" min="72" name="dpi" required step="1" type="number" value="300">
|
||||
</div>
|
||||
<br>
|
||||
<button class="btn btn-primary" id="submitBtn" th:text="#{pdfToCBZ.submit}" type="submit">Convert to CBZ</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,46 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,87 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:data-language="${#locale.toString()}"
|
||||
th:dir="#{language.direction}"
|
||||
th:lang="${#locale.language}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pdfToEpub.title}, header=#{pdfToEpub.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">menu_book</span>
|
||||
<span class="tool-header-text"
|
||||
th:text="#{pdfToEpub.header}"></span>
|
||||
</div>
|
||||
<p th:text="#{processTimeWarning}"></p>
|
||||
|
||||
<div class="alert alert-warning"
|
||||
th:if="${[email protected]('Calibre')}">
|
||||
<span th:text="#{pdfToEpub.calibreDisabled}">Calibre support is disabled.</span>
|
||||
</div>
|
||||
|
||||
<form enctype="multipart/form-data"
|
||||
id="pdfToEpubForm"
|
||||
method="post"
|
||||
th:action="@{'/api/v1/convert/pdf/epub'}"
|
||||
th:if="${@endpointConfiguration.isGroupEnabled('Calibre')}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.pdf', inputText=#{pdfToEpub.selectText})}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="outputFormat"
|
||||
th:text="#{pdfToEpub.outputFormat}"></label>
|
||||
<select class="form-select"
|
||||
id="outputFormat"
|
||||
name="outputFormat">
|
||||
<option selected
|
||||
th:text="#{pdfToEpub.outputFormat.epub}"
|
||||
value="EPUB"></option>
|
||||
<option th:text="#{pdfToEpub.outputFormat.azw3}"
|
||||
value="AZW3"></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input checked id="detectChapters" name="detectChapters" type="checkbox">
|
||||
<label for="detectChapters" th:text="#{pdfToEpub.detectChapters}"></label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="targetDevice"
|
||||
th:text="#{pdfToEpub.targetDevice}"></label>
|
||||
<select class="form-select"
|
||||
id="targetDevice"
|
||||
name="targetDevice">
|
||||
<option selected
|
||||
th:text="#{pdfToEpub.targetDevice.tablet}"
|
||||
value="TABLET_PHONE_IMAGES"></option>
|
||||
<option th:text="#{pdfToEpub.targetDevice.kindle}"
|
||||
value="KINDLE_EINK_TEXT"></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary"
|
||||
id="submitBtn"
|
||||
th:text="#{pdfToEpub.submit}"
|
||||
type="submit">Convert</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,33 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,95 +0,0 @@
|
||||
<!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>
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const maxDPI = /*[[${maxDPI}]]*/ 500; // Maximum DPI for PDF to image conversion
|
||||
const maxDPILabelRaw = /*[[#{pdfToImage.dpi}]]*/ "DPI (The server limit is {0} dpi)";
|
||||
const maxDPILabel = maxDPILabelRaw.replace("{0}", maxDPI); // Replace with actual value from properties
|
||||
const dpilabel = document.querySelector('label[for="dpi"]');
|
||||
if (dpilabel) {
|
||||
dpilabel.textContent = maxDPILabel;
|
||||
}
|
||||
const maxDPIInput = document.getElementById("dpi");
|
||||
if (maxDPIInput) {
|
||||
maxDPIInput.setAttribute("max", maxDPI); // Set the maximum value for DPI input
|
||||
}
|
||||
});
|
||||
</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 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="form-check mb-3">
|
||||
<input id="includeAnnotations" name="includeAnnotations" type="checkbox">
|
||||
<label for="includeAnnotations" th:text="#{pdfToImage.includeAnnotations}"></label>
|
||||
</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>
|
||||
@@ -1,31 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,86 +0,0 @@
|
||||
<!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">
|
||||
<optgroup th:label="#{pdfToPDFA.pdfaFormats}">
|
||||
<option value="pdfa-1">PDF/A-1b</option>
|
||||
<option selected="selected" value="pdfa">PDF/A-2b (default)</option>
|
||||
<option value="pdfa-3">PDF/A-3b</option>
|
||||
</optgroup>
|
||||
<optgroup th:label="#{pdfToPDFA.pdfxFormats}">
|
||||
<option value="pdfx-1">PDF/X-1</option>
|
||||
<option value="pdfx-3">PDF/X-3</option>
|
||||
<option value="pdfx-4">PDF/X-4</option>
|
||||
</optgroup>
|
||||
</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 = pdfjsPath + '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>
|
||||
@@ -1,49 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,39 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,46 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:data-language="${#locale.toString()}" th:dir="#{language.direction}" th:lang="${#locale.language}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pdfToVector.title}, header=#{pdfToVector.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">stroke_full</span>
|
||||
<span class="tool-header-text" th:text="#{pdfToVector.header}"></span>
|
||||
</div>
|
||||
<form enctype="multipart/form-data" method="post" th:action="@{'/api/v1/convert/pdf/vector'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="outputFormat" th:text="#{pdfToVector.outputFormat}"></label>
|
||||
<select class="form-control" id="outputFormat" name="outputFormat">
|
||||
<option selected value="eps">EPS (Encapsulated PostScript)</option>
|
||||
<option value="ps">PS (PostScript)</option>
|
||||
<option value="pcl">PCL (Printer Command Language)</option>
|
||||
<option value="xps">XPS (XML Paper Specification)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="submitBtn" th:text="#{pdfToVector.submit}" type="submit"></button>
|
||||
</form>
|
||||
<p class="mt-3" th:text="#{pdfToVector.description}"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,84 +0,0 @@
|
||||
<!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=#{pdfToVideo.title}, header=#{pdfToVideo.header})}"></th:block>
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const maxDPI = /*[[${maxDPI}]]*/ 500;
|
||||
const labelTemplate = /*[[#{pdfToVideo.dpiLabel}]]*/ "DPI (The server limit is {0} dpi)";
|
||||
const dpiLabel = document.querySelector('label[for="dpi"]');
|
||||
if (dpiLabel) {
|
||||
dpiLabel.textContent = labelTemplate.replace("{0}", maxDPI);
|
||||
}
|
||||
const dpiInput = document.getElementById("dpi");
|
||||
if (dpiInput) {
|
||||
dpiInput.setAttribute("max", maxDPI);
|
||||
}
|
||||
});
|
||||
</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 convert">movie</span>
|
||||
<span class="tool-header-text" th:text="#{pdfToVideo.header}"></span>
|
||||
</div>
|
||||
<p th:text="#{processTimeWarning}"></p>
|
||||
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/video'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="videoFormat" th:text="#{pdfToVideo.videoFormat}"></label>
|
||||
<select id="videoFormat" class="form-control" name="videoFormat">
|
||||
<option value="mp4">MP4</option>
|
||||
<option value="webm">WebM</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="secondsPerPage" th:text="#{pdfToVideo.secondsPerPage}"></label>
|
||||
<input id="secondsPerPage" type="number" class="form-control" name="secondsPerPage" min="1" max="30" value="3"
|
||||
required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="resolution" th:text="#{pdfToVideo.resolution}"></label>
|
||||
<select id="resolution" class="form-control" name="resolution">
|
||||
<option value="ORIGINAL" th:text="#{pdfToVideo.resolutionOriginal}"></option>
|
||||
<option value="1080p">1080p</option>
|
||||
<option value="720p">720p</option>
|
||||
<option value="480p">480p</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dpi">DPI</label>
|
||||
<input id="dpi" type="number" class="form-control" name="dpi" min="72" max="500" value="150" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="watermarkText" th:text="#{pdfToVideo.watermarkText}"></label>
|
||||
<input id="watermarkText" type="text" class="form-control" name="watermarkText" placeholder="Stirling Software">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="opacity" th:text="#{pdfToVideo.opacity}"></label>
|
||||
<input id="opacity" type="number" step="0.01" class="form-control" name="opacity" min="0.0" max="1.0" value="0.1">
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfToVideo.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,46 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,33 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,37 +0,0 @@
|
||||
<!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>
|
||||
<p th:if="${not #lists.isEmpty(param.error)}" th:text="#{${param.error[0]}}" class="alert alert-danger text-center"></p>
|
||||
<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>
|
||||
@@ -1,41 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:data-language="${#locale.toString()}" th:dir="#{language.direction}" th:lang="${#locale.language}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{vectorToPdf.title}, header=#{vectorToPdf.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="#{vectorToPdf.header}"></span>
|
||||
</div>
|
||||
<form enctype="multipart/form-data" method="post" th:action="@{'/api/v1/convert/vector/pdf'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.ps,.eps,.epsf,application/pdf')}">
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input id="prepress" name="prepress" type="checkbox" value="true">
|
||||
<label for="prepress" th:text="#{vectorToPdf.prepress}"></label>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="submitBtn" th:text="#{vectorToPdf.submit}" type="submit"></button>
|
||||
</form>
|
||||
<p class="mt-3" th:text="#{vectorToPdf.description}"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,48 +0,0 @@
|
||||
<!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">
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input id="autoCrop" name="autoCrop" type="checkbox">
|
||||
<label for="autoCrop" th:text="#{crop.autoCrop}"></label>
|
||||
<input name="autoCrop" type="hidden" value="false" />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
@@ -1,73 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,175 +0,0 @@
|
||||
<!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="form-check mb-3">
|
||||
<input type="checkbox"
|
||||
id="replaceExisting"
|
||||
name="replaceExisting"
|
||||
checked>
|
||||
<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="error-message-container">
|
||||
<!-- Error messages will be added here dynamically -->
|
||||
</div>
|
||||
|
||||
<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 class="d-flex flex-wrap justify-content-end gap-2">
|
||||
<!-- Import Split Button -->
|
||||
<div class="btn-group">
|
||||
<label
|
||||
id="importDefaultBtn"
|
||||
for="importUploadJsonFileInput"
|
||||
class="btn btn-outline-primary"
|
||||
style="border-top-left-radius: 1.25rem !important; border-bottom-left-radius: 1.25rem !important;"
|
||||
th:text="#{editTableOfContents.importBookmarksDefault}">
|
||||
</label>
|
||||
<button type="button"
|
||||
class="btn btn-outline-primary dropdown-toggle dropdown-toggle-split"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<span class="visually-hidden">Toggle Import Options</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><label class="dropdown-item"
|
||||
id="importUploadJsonFileBtn"
|
||||
for="importUploadJsonFileInput"
|
||||
style="cursor: pointer;"
|
||||
th:text="#{editTableOfContents.importBookmarksFromJsonFile}"></label></li>
|
||||
<li class="d-none"><a class="dropdown-item"
|
||||
href="#bookmarks-container"
|
||||
id="importPasteFromClipboardBtn"
|
||||
th:text="#{editTableOfContents.importBookmarksFromClipboard}"></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Export Split Button -->
|
||||
<div class="btn-group">
|
||||
<button type="button"
|
||||
id="exportDefaultBtn"
|
||||
class="btn btn-outline-primary"
|
||||
th:text="#{editTableOfContents.exportBookmarksDefault}"></button>
|
||||
<button type="button"
|
||||
class="btn btn-outline-primary dropdown-toggle dropdown-toggle-split"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<span class="visually-hidden">Toggle Export Options</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item"
|
||||
href="#bookmarks-container"
|
||||
id="exportDownloadJsonFileBtn"
|
||||
th:text="#{editTableOfContents.exportBookmarksAsJson}"></a></li>
|
||||
<li class="d-none"><a class="dropdown-item"
|
||||
href="#bookmarks-container"
|
||||
id="exportCopyToClipboardBtn"
|
||||
th:text="#{editTableOfContents.exportBookmarksAsText}"></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- Hidden file input for JSON import (outside of form)-->
|
||||
<input type="file"
|
||||
id="importUploadJsonFileInput"
|
||||
accept="application/json"
|
||||
style="display: none;">
|
||||
</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>
|
||||
@@ -1,31 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,44 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,21 +0,0 @@
|
||||
<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>
|
||||
@@ -1,496 +0,0 @@
|
||||
<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;
|
||||
|
||||
// Reset all navbar and dropdown scaling styles
|
||||
function resetNavScaling() {
|
||||
const navbarElement = document.querySelector('.navbar');
|
||||
if (navbarElement) {
|
||||
navbarElement.style.transform = '';
|
||||
navbarElement.style.transformOrigin = '';
|
||||
navbarElement.style.width = '';
|
||||
navbarElement.style.left = '';
|
||||
navbarElement.style.right = '';
|
||||
navbarElement.style.marginBottom = '';
|
||||
navbarElement.classList.remove('navbar-expand-lg');
|
||||
navbarElement.classList.remove('navbar-expand-xl');
|
||||
}
|
||||
// Reset dropdown scaling
|
||||
const dropdowns = document.querySelectorAll('.dropdown-menu');
|
||||
dropdowns.forEach(dropdown => {
|
||||
dropdown.style.transform = '';
|
||||
dropdown.style.transformOrigin = '';
|
||||
});
|
||||
// Reset CSS custom property
|
||||
document.documentElement.style.setProperty('--navbar-height', '');
|
||||
}
|
||||
|
||||
function scaleNav() {
|
||||
resetNavScaling();
|
||||
if (window.innerWidth < 1200) {
|
||||
const navbarElement = document.querySelector('.navbar');
|
||||
if (navbarElement) {
|
||||
navbarElement.classList.remove('navbar-expand-lg');
|
||||
navbarElement.classList.add('navbar-expand-xl');
|
||||
}
|
||||
return;
|
||||
}
|
||||
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 = 64;
|
||||
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'}">
|
||||
|
||||
<!-- Scarf Pixel - loaded dynamically based on cookie consent -->
|
||||
<script th:inline="javascript">
|
||||
const posthogEnabled = /*[[${@posthogEnabled}]]*/ false;
|
||||
const scarfEnabled = /*[[${@scarfEnabled}]]*/ false;
|
||||
const scarfPixelUrl = /*[['https://pixel.stirlingpdf.com/a.png?x-pxid=4f5fa02f-a065-4efb-bb2c-24509a4b6b92'
|
||||
+ '&machineType=' + ${@machineType}
|
||||
+ '&appVersion=' + ${@appVersion}
|
||||
+ '&licenseType=' + ${@license}
|
||||
+ '&loginEnabled=' + ${@loginEnabled}]]*/ '';
|
||||
|
||||
function loadScarfPixel() {
|
||||
if (!scarfEnabled || !window.CookieConsent) return;
|
||||
|
||||
// Check if scarf service is accepted
|
||||
if (window.CookieConsent.acceptedService('scarf', 'analytics')) {
|
||||
// Remove any existing scarf pixel
|
||||
const existingPixel = document.getElementById('scarf-pixel');
|
||||
if (existingPixel) existingPixel.remove();
|
||||
|
||||
// Create and inject the pixel
|
||||
const img = document.createElement('img');
|
||||
img.id = 'scarf-pixel';
|
||||
img.src = scarfPixelUrl;
|
||||
img.referrerPolicy = 'no-referrer-when-downgrade';
|
||||
img.style.position = 'absolute';
|
||||
img.style.visibility = 'hidden';
|
||||
document.body.appendChild(img);
|
||||
} else {
|
||||
// Remove pixel if user rejected
|
||||
const existingPixel = document.getElementById('scarf-pixel');
|
||||
if (existingPixel) existingPixel.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Load pixel on consent events
|
||||
window.addEventListener('cc:onConsent', loadScarfPixel);
|
||||
window.addEventListener('cc:onChange', loadScarfPixel);
|
||||
|
||||
// Try to load on page load if consent already given
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', loadScarfPixel);
|
||||
} else {
|
||||
loadScarfPixel();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- 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'}" th:if="${@analyticsEnabled}">
|
||||
<link rel="stylesheet" th:href="@{'/css/cookieconsentCustomisation.css'}" th:if="${@analyticsEnabled}">
|
||||
<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.acceptedService('posthog', '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}]]*/ "";
|
||||
const cookieBannerPreferencesModalPosthogLabel = /*[[#{cookieBanner.preferencesModal.analytics.posthog.label}]]*/ "";
|
||||
const cookieBannerPreferencesModalScarfLabel = /*[[#{cookieBanner.preferencesModal.analytics.scarf.label}]]*/ "";
|
||||
|
||||
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.';
|
||||
window.stirlingPDF.pdfCorruptedTitle = /*[[#{error.pdfCorrupted.title}]]*/ 'PDF File Corrupted';
|
||||
window.stirlingPDF.pdfCorruptedHint1 = /*[[#{error.E001.hint.1}]]*/ 'Try the \'Repair PDF\' feature, then retry this operation.';
|
||||
window.stirlingPDF.pdfCorruptedHint2 = /*[[#{error.E001.hint.2}]]*/ 'Re-export the PDF from the original application if possible.';
|
||||
window.stirlingPDF.pdfCorruptedHint3 = /*[[#{error.E001.hint.3}]]*/ 'Avoid transferring the file through tools that modify PDFs (e.g., fax/email converters).';
|
||||
window.stirlingPDF.pdfCorruptedAction = /*[[#{error.E001.action}]]*/ 'Repair the PDF and retry the operation.';
|
||||
window.stirlingPDF.pdfPasswordTitle = /*[[#{error.pdfPassword.title}]]*/ 'PDF Password Required';
|
||||
window.stirlingPDF.pdfPasswordDetail = /*[[#{error.pdfPassword}]]*/ 'The PDF Document is passworded and either the password was not provided or was incorrect';
|
||||
window.stirlingPDF.pdfPasswordHint1 = /*[[#{error.E004.hint.1}]]*/ 'PDFs can have two passwords: a user password (opens the document) and an owner password (controls permissions). This operation requires the owner password.';
|
||||
window.stirlingPDF.pdfPasswordHint2 = /*[[#{error.E004.hint.2}]]*/ 'If you can open the PDF without a password, it may only have an owner password set. Try submitting the permissions password.';
|
||||
window.stirlingPDF.pdfPasswordHint3 = /*[[#{error.E004.hint.3}]]*/ 'Digitally signed PDFs cannot have security removed until the signature is removed.';
|
||||
window.stirlingPDF.pdfPasswordHint4 = /*[[#{error.E004.hint.4}]]*/ 'Passwords are case-sensitive. Verify capitalisation, spaces, and special characters.';
|
||||
window.stirlingPDF.pdfPasswordHint5 = /*[[#{error.E004.hint.5}]]*/ 'Some creators use different encryption standards (40-bit, 128-bit, 256-bit AES). Ensure your password matches the encryption used.';
|
||||
window.stirlingPDF.pdfPasswordHint6 = /*[[#{error.E004.hint.6}]]*/ 'If you only have the user password, you cannot remove security restrictions. Contact the document owner for the permissions password.';
|
||||
window.stirlingPDF.pdfPasswordAction = /*[[#{error.E004.action}]]*/ 'Provide the owner/permissions password, not just the document open password.';
|
||||
window.stirlingPDF.currentPage = /*[[${currentPage}]]*/ '';
|
||||
})();
|
||||
</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>
|
||||
</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>
|
||||
</th:block>
|
||||
@@ -1,258 +0,0 @@
|
||||
<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} ?: '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 class="error-message mb-2" style="white-space: pre-wrap;" th:if="${message}" th:text="${message}"></p>
|
||||
<p class="error-path small text-muted mb-0" th:if="${path}" th:text="'Path: ' + ${path}"></p>
|
||||
<!-- Stack trace section -->
|
||||
<div id="trace" th:if="${trace}" style="max-height: 0; overflow: hidden;">
|
||||
<div>
|
||||
<pre id="traceContent" style="white-space: pre-wrap; word-break: break-word; overflow: auto; max-height: 60vh; padding: 0.5rem; border-radius: 0.25rem; background: rgba(0,0,0,0.05);" 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 = "60vh";
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
normalizeServerRenderedProblemDetail();
|
||||
} catch (err) {
|
||||
console.error('Failed to normalize server-rendered error banner', err);
|
||||
}
|
||||
|
||||
function normalizeServerRenderedProblemDetail() {
|
||||
var container = document.getElementById('errorContainer');
|
||||
if (!container || container.style.display === 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
var messageEl = container.querySelector('.error-message');
|
||||
if (!messageEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
var rawMessage = messageEl.textContent || '';
|
||||
var traceEl = document.getElementById('traceContent');
|
||||
var parseResult = extractProblemDetail(rawMessage);
|
||||
|
||||
if (!parseResult && traceEl) {
|
||||
parseResult = extractProblemDetail(traceEl.textContent || '');
|
||||
}
|
||||
|
||||
if (!parseResult) {
|
||||
messageEl.textContent = rawMessage.trim();
|
||||
return;
|
||||
}
|
||||
|
||||
var problem = parseResult.json;
|
||||
var heading = container.querySelector('.alert-heading');
|
||||
var formattedMessage = buildUserFriendlyProblemMessage(problem, parseResult.prefix);
|
||||
messageEl.textContent = formattedMessage;
|
||||
|
||||
if (heading && problem.title) {
|
||||
heading.textContent = problem.title;
|
||||
}
|
||||
|
||||
if (traceEl) {
|
||||
traceEl.textContent = JSON.stringify(orderProblemDetail(problem), null, 2);
|
||||
}
|
||||
|
||||
// Ensure trace toggle buttons appear when JSON is available
|
||||
var toggleButton = container.querySelector('.btn.btn-danger');
|
||||
var copyButton = container.querySelector('.btn.btn-secondary');
|
||||
if (traceEl && traceEl.textContent.trim().length > 0) {
|
||||
if (toggleButton) {
|
||||
toggleButton.style.display = '';
|
||||
}
|
||||
if (copyButton) {
|
||||
copyButton.style.display = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function extractProblemDetail(text) {
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var trimmed = text.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var braceStart = trimmed.indexOf('{');
|
||||
var braceEnd = trimmed.lastIndexOf('}');
|
||||
|
||||
if (braceStart !== -1 && braceEnd !== -1 && braceEnd > braceStart) {
|
||||
var jsonCandidate = trimmed.substring(braceStart, braceEnd + 1);
|
||||
var parsedCandidate = tryParseJson(jsonCandidate);
|
||||
if (parsedCandidate) {
|
||||
var prefix = trimmed.substring(0, braceStart).trim();
|
||||
return { json: parsedCandidate, prefix: prefix };
|
||||
}
|
||||
}
|
||||
|
||||
var parsed = tryParseJson(trimmed);
|
||||
if (parsed) {
|
||||
return { json: parsed, prefix: '' };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function tryParseJson(candidate) {
|
||||
if (!candidate) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(candidate);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildUserFriendlyProblemMessage(problem, prefix) {
|
||||
var lines = [];
|
||||
var title = (problem && (problem.title || problem.error)) || '';
|
||||
var detail = (problem && (problem.detail || problem.message)) || '';
|
||||
var primaryLine = '';
|
||||
|
||||
if (title && detail) {
|
||||
primaryLine = title + ': ' + detail;
|
||||
} else if (title || detail) {
|
||||
primaryLine = title || detail;
|
||||
}
|
||||
|
||||
if (primaryLine) {
|
||||
if (lines.length > 0) {
|
||||
lines.push('');
|
||||
}
|
||||
lines.push(primaryLine);
|
||||
}
|
||||
|
||||
if (problem && problem.errorCode) {
|
||||
lines.push('');
|
||||
lines.push('Error Code: ' + problem.errorCode);
|
||||
}
|
||||
|
||||
var detailAlreadyIncluded = detail && primaryLine && primaryLine.indexOf(detail) !== -1;
|
||||
if (detail && !detailAlreadyIncluded) {
|
||||
lines.push('');
|
||||
lines.push(detail);
|
||||
}
|
||||
|
||||
if (problem && Array.isArray(problem.hints) && problem.hints.length > 0) {
|
||||
lines.push('');
|
||||
lines.push('How to fix:');
|
||||
for (var i = 0; i < problem.hints.length; i++) {
|
||||
lines.push(' ' + (i + 1) + '. ' + problem.hints[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (problem && problem.actionRequired) {
|
||||
lines.push('');
|
||||
lines.push(problem.actionRequired);
|
||||
}
|
||||
|
||||
if (problem && problem.supportId) {
|
||||
lines.push('');
|
||||
lines.push('Support ID: ' + problem.supportId);
|
||||
}
|
||||
|
||||
return lines
|
||||
.filter(function (line, index, arr) {
|
||||
if (line !== '') {
|
||||
return true;
|
||||
}
|
||||
if (index === 0 || index === arr.length - 1) {
|
||||
return false;
|
||||
}
|
||||
return arr[index - 1] !== '';
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function orderProblemDetail(problem) {
|
||||
if (!problem || typeof problem !== 'object') {
|
||||
return problem;
|
||||
}
|
||||
|
||||
var preferredOrder = ['errorCode', 'title', 'status', 'type', 'detail', 'instance', 'path', 'timestamp', 'hints', 'actionRequired'];
|
||||
var ordered = {};
|
||||
|
||||
for (var i = 0; i < preferredOrder.length; i++) {
|
||||
var key = preferredOrder[i];
|
||||
if (Object.prototype.hasOwnProperty.call(problem, key)) {
|
||||
ordered[key] = problem[key];
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(problem).forEach(function (key) {
|
||||
if (!Object.prototype.hasOwnProperty.call(ordered, key)) {
|
||||
ordered[key] = problem[key];
|
||||
}
|
||||
});
|
||||
|
||||
return ordered;
|
||||
}
|
||||
</script>
|
||||
</th:block>
|
||||
@@ -1,53 +0,0 @@
|
||||
<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" style="white-space: pre-wrap; word-break: break-word; overflow: auto; max-height: 60vh; padding: 0.5rem; border-radius: 0.25rem; background: rgba(0,0,0,0.05);"></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>
|
||||
@@ -1,3 +0,0 @@
|
||||
<div th:fragment="featureGroupHeader" class="feature-group-header">
|
||||
<h6 class="menu-title" th:text="${groupTitle}"></h6>
|
||||
</div>
|
||||
@@ -1,6 +0,0 @@
|
||||
<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>
|
||||
@@ -1,26 +0,0 @@
|
||||
<footer th:fragment="footer" id="footer" class="text-center">
|
||||
|
||||
<script type="module" th:src="@{'/js/thirdParty/cookieconsent-config.js'}" th:if="${@analyticsEnabled}"></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 th:if="${@analyticsEnabled}"><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>
|
||||
@@ -1,7 +0,0 @@
|
||||
<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>
|
||||
@@ -1,44 +0,0 @@
|
||||
<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>
|
||||
@@ -1,65 +0,0 @@
|
||||
<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>
|
||||
@@ -1,342 +0,0 @@
|
||||
<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('cbz-to-pdf', 'auto_stories', 'home.cbzToPdf.title', 'home.cbzToPdf.desc', 'cbzToPdf.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('cbr-to-pdf', 'auto_stories', 'home.cbrToPdf.title', 'home.cbrToPdf.desc', 'cbrToPdf.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('ebook-to-pdf', 'menu_book', 'home.ebookToPdf.title', 'home.ebookToPdf.desc', 'ebookToPdf.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
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('vector-to-pdf', 'picture_as_pdf', 'home.vectorToPdf.title', 'home.vectorToPdf.desc', 'vectorToPdf.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-cbz', 'auto_stories', 'home.pdfToCbz.title', 'home.pdfToCbz.desc', 'pdfToCbz.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-cbr', 'auto_stories', 'home.pdfToCbr.title', 'home.pdfToCbr.desc', 'pdfToCbr.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-epub', 'menu_book', 'home.pdfToEpub.title', 'home.pdfToEpub.desc', 'pdfToEpub.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
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-vector', 'stroke_full', 'home.pdfToVector.title', 'home.pdfToVector.desc', 'pdfToVector.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-video', 'movie', 'home.PDFToVideo.title', 'home.PDFToVideo.desc', 'PDFToVideo.tags', 'convert')}">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-group" id="convertGroup">
|
||||
<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('cbz-to-pdf', 'auto_stories', 'home.cbzToPdf.title', 'home.cbzToPdf.desc', 'cbzToPdf.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('cbr-to-pdf', 'auto_stories', 'home.cbrToPdf.title', 'home.cbrToPdf.desc', 'cbrToPdf.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('ebook-to-pdf', 'menu_book', 'home.ebookToPdf.title', 'home.ebookToPdf.desc', 'ebookToPdf.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('vector-to-pdf', 'picture_as_pdf', 'home.vectorToPdf.title', 'home.vectorToPdf.desc', 'vectorToPdf.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-epub', 'menu_book', 'home.pdfToEpub.title', 'home.pdfToEpub.desc', 'pdfToEpub.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-vector', 'stroke_full', 'home.pdfToVector.title', 'home.pdfToVector.desc', 'pdfToVector.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-video', 'movie', 'home.PDFToVideo.title', 'home.PDFToVideo.desc', 'PDFToVideo.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
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('extract-attachments', 'folder_zip', 'home.extractAttachments.title', 'home.extractAttachments.desc', 'extractAttachments.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('scanner-effect', 'scanner', 'scannerEffect.title', 'scannerEffect.description', 'scannerEffect.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>
|
||||
@@ -1,325 +0,0 @@
|
||||
<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 licenseType = /*[[${@license}]]*/ '';
|
||||
const machineType = /*[[${@machineType}]]*/ '';
|
||||
const activeSecurity = /*[[${@activeSecurity}]]*/ false;
|
||||
const noFavourites = /*[[#{noFavourites}]]*/ '';
|
||||
console.log(noFavourites);
|
||||
const updateAvailable = /*[[#{settings.updateAvailable}]]*/ '';
|
||||
|
||||
// Update notification i18n constants
|
||||
const urgentUpdateAvailable = /*[[#{update.urgentUpdateAvailable}]]*/ '🚨 Update Available';
|
||||
const updateAvailableText = /*[[#{update.updateAvailable}]]*/ 'Update Available';
|
||||
const updateModalTitle = /*[[#{update.modalTitle}]]*/ 'Update Available';
|
||||
const updateCurrent = /*[[#{update.current}]]*/ 'Current';
|
||||
const updateLatest = /*[[#{update.latest}]]*/ 'Latest';
|
||||
const updateLatestStable = /*[[#{update.latestStable}]]*/ 'Latest Stable';
|
||||
const updatePriority = /*[[#{update.priority}]]*/ 'Priority';
|
||||
const updateRecommendedAction = /*[[#{update.recommendedAction}]]*/ 'Recommended Action';
|
||||
const updateBreakingChangesDetected = /*[[#{update.breakingChangesDetected}]]*/ '⚠️ Breaking Changes Detected';
|
||||
const updateBreakingChangesMessage = /*[[#{update.breakingChangesMessage}]]*/ 'This update contains breaking changes. Please review the migration guides below.';
|
||||
const updateMigrationGuides = /*[[#{update.migrationGuides}]]*/ 'Migration Guides:';
|
||||
const updateViewGuide = /*[[#{update.viewGuide}]]*/ 'View Guide';
|
||||
const updateLoadingDetailedInfo = /*[[#{update.loadingDetailedInfo}]]*/ 'Loading detailed version information...';
|
||||
const updateClose = /*[[#{update.close}]]*/ 'Close';
|
||||
const updateViewAllReleases = /*[[#{update.viewAllReleases}]]*/ 'View All Releases';
|
||||
const updateDownloadLatest = /*[[#{update.downloadLatest}]]*/ 'Download Latest';
|
||||
const updateAvailableUpdates = /*[[#{update.availableUpdates}]]*/ 'Available Updates:';
|
||||
const updateUnableToLoadDetails = /*[[#{update.unableToLoadDetails}]]*/ 'Unable to load detailed version information.';
|
||||
const updateVersion = /*[[#{update.version}]]*/ 'Version';
|
||||
|
||||
// Update priority levels
|
||||
const updatePriorityUrgent = /*[[#{update.priority.urgent}]]*/ 'URGENT';
|
||||
const updatePriorityNormal = /*[[#{update.priority.normal}]]*/ 'NORMAL';
|
||||
const updatePriorityMinor = /*[[#{update.priority.minor}]]*/ 'MINOR';
|
||||
const updatePriorityLow = /*[[#{update.priority.low}]]*/ 'LOW';
|
||||
|
||||
// Breaking changes text
|
||||
const updateBreakingChanges = /*[[#{update.breakingChanges}]]*/ 'Breaking Changes:';
|
||||
const updateBreakingChangesDefault = /*[[#{update.breakingChangesDefault}]]*/ 'This version contains breaking changes';
|
||||
const updateMigrationGuide = /*[[#{update.migrationGuide}]]*/ 'Migration Guide';
|
||||
|
||||
// PDF.js path
|
||||
const pdfjsPath = /*[[@{'/pdfjs-legacy/'}]]*/ './pdfjs-legacy/';
|
||||
</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>
|
||||
<span class="material-symbols-rounded chevron-icon">expand_more</span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-tp" aria-labelledby="navbarDropdown-1">
|
||||
<div class="dropdown-menu-wrapper scroll-lock-y" style="max-width: 95vw !important;">
|
||||
<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>
|
||||
<span class="material-symbols-rounded chevron-icon">expand_more</span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-tp dropdown-mw-28" role="menu" aria-labelledby="navbarDropdown-5">
|
||||
<div class="dropdown-menu-wrapper px-xl-2 px-2 scroll-lock-y" id="favoritesDropdown" style="max-width: 95vw !important; ">
|
||||
<!-- 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>
|
||||
<span class="material-symbols-rounded chevron-icon">expand_more</span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-tp" aria-labelledby="languageDropdown">
|
||||
<div class="dropdown-menu-wrapper px-xl-2 px-2 scroll-lock-y" style="max-width: 95vw !important;">
|
||||
<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>
|
||||
<span class="material-symbols-rounded chevron-icon">expand_more</span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-tp" aria-labelledby="searchDropdown">
|
||||
<div class="dropdown-menu-wrapper px-xl-2 px-2" style="max-width: 95vw !important;">
|
||||
<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 scroll-lock-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 id="boredWaiting" th:title="#{settings.bored.help}" type="checkbox">
|
||||
<label for="boredWaiting" th:text="#{bored}"></label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input id="cacheInputs" th:title="#{settings.cacheInputs.help}" type="checkbox">
|
||||
<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>
|
||||
@@ -1,18 +0,0 @@
|
||||
<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>
|
||||
@@ -1,21 +0,0 @@
|
||||
<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>
|
||||
@@ -1,534 +0,0 @@
|
||||
<!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
|
||||
th:replace="~{fragments/card :: card(id='pdf-to-video', cardTitle=#{home.PDFToVideo.title}, cardText=#{home.PDFToVideo.desc}, cardLink='pdf-to-video', toolIcon='movie', tags=#{PDFToVideo.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
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('extract-attachments', 'folder_zip', 'home.extractAttachments.title', 'home.extractAttachments.desc', 'extractAttachments.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: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>
|
||||
@@ -1,293 +0,0 @@
|
||||
<!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('scanner-effect', 'scanner', 'scannerEffect.title', 'scannerEffect.description', 'scannerEffect.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 id="dontShowAgain" type="checkbox">
|
||||
<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: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>
|
||||
@@ -1,42 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,178 +0,0 @@
|
||||
<!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 id="remember-me" name="remember-me" type="checkbox">
|
||||
<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>
|
||||
@@ -1,72 +0,0 @@
|
||||
<!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">
|
||||
<input id="fileOrder" name="fileOrder" type="hidden" />
|
||||
<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="form-check mb-3">
|
||||
<input id="removeCertSign" name="removeCertSign" type="checkbox">
|
||||
<label for="removeCertSign" th:text="#{merge.removeCertSign}">Remove digital signature in the merged
|
||||
file?</label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input id="generateToc" name="generateToc" type="checkbox">
|
||||
<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 = pdfjsPath + '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>
|
||||
@@ -1,42 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,116 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,170 +0,0 @@
|
||||
<!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="fontColor" th:text="#{addPageNumbers.fontColor}"></label>
|
||||
<div class="form-control form-control-color" style="background-color: #000000;">
|
||||
<input type="color" id="fontColor" name="fontColor" value="#000000">
|
||||
</div>
|
||||
<script>
|
||||
let colorInput = document.getElementById("fontColor");
|
||||
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">
|
||||
<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>
|
||||
@@ -1,65 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,33 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,35 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,98 +0,0 @@
|
||||
<!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 ms-3">
|
||||
<input id="deleteAll" name="deleteAll" type="checkbox">
|
||||
<label for="deleteAll" th:text="#{changeMetadata.selectText.2}" ></label>
|
||||
</div>
|
||||
<div class="form-check mb-3 ms-3">
|
||||
<input id="customModeCheckbox" type="checkbox">
|
||||
<label for="customModeCheckbox" th:text="#{changeMetadata.selectText.3}"></label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="author" th:text="#{changeMetadata.author}"></label>
|
||||
<input type="text" class="form-control" id="author" name="author">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<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 for="creator" th:text="#{changeMetadata.creator}"></label>
|
||||
<input type="text" class="form-control" id="creator" name="creator">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="keywords" th:text="#{changeMetadata.keywords}"></label>
|
||||
<input type="text" class="form-control" id="keywords" name="keywords">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<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 for="producer" th:text="#{changeMetadata.producer}"></label>
|
||||
<input type="text" class="form-control" id="producer" name="producer">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="subject" th:text="#{changeMetadata.subject}"></label>
|
||||
<input type="text" class="form-control" id="subject" name="subject">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="title" th:text="#{changeMetadata.title}"></label>
|
||||
<input type="text" class="form-control" id="title" name="title">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
@@ -1,336 +0,0 @@
|
||||
<!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" id="compareBtn" onclick="comparePDFs(event)" 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(event) {
|
||||
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.';
|
||||
const invalidPdfMessage = /*[[#{compare.invalid.pdf.message}]]*/ 'One or both files are not valid PDFs. Please check and re-upload.';
|
||||
const submitText = /*[[#{compare.submit}]]*/ 'Compare';
|
||||
|
||||
if (!file1 || !file2) {
|
||||
alert('Please select two PDF files to compare');
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic checks
|
||||
if (file1.size === 0 || file2.size === 0) {
|
||||
alert('One or both files are empty.');
|
||||
return;
|
||||
}
|
||||
if (file1.size > 100 * 1024 * 1024 || file2.size > 100 * 1024 * 1024) {
|
||||
alert(largeFilesMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// PDF.js setup (Legacy-safe: Worker disabled)
|
||||
const PDFJS_DEFAULT_OPTIONS = {
|
||||
cMapUrl: pdfjsPath + 'cmaps/',
|
||||
cMapPacked: true,
|
||||
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
|
||||
disableWorker: true // Avoids Legacy CMap errors without changing PDF.js
|
||||
};
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
|
||||
|
||||
const button = event.target;
|
||||
button.disabled = true;
|
||||
button.textContent = 'Processing...';
|
||||
|
||||
try {
|
||||
// Load ArrayBuffer
|
||||
const [data1, data2] = await Promise.all([
|
||||
readFileAsArrayBuffer(file1),
|
||||
readFileAsArrayBuffer(file2)
|
||||
]);
|
||||
|
||||
// Header validation (prevents InvalidPDFException)
|
||||
await validatePdfHeader(data1, 'File 1');
|
||||
await validatePdfHeader(data2, 'File 2');
|
||||
|
||||
// Load PDFs
|
||||
const [pdf1, pdf2] = await Promise.all([
|
||||
loadPdfWithErrorHandling({ ...PDFJS_DEFAULT_OPTIONS, data: data1 }, 'File 1'),
|
||||
loadPdfWithErrorHandling({ ...PDFJS_DEFAULT_OPTIONS, data: data2 }, 'File 2')
|
||||
]);
|
||||
|
||||
// Extract text
|
||||
result1.innerHTML = 'Extracting text from File 1...';
|
||||
result2.innerHTML = 'Extracting text from File 2...';
|
||||
const [text1, text2] = await Promise.all([
|
||||
extractText(pdf1, 'File 1', result1),
|
||||
extractText(pdf2, 'File 2', result2)
|
||||
]);
|
||||
|
||||
if (text1.trim() === "" || text2.trim() === "") {
|
||||
throw new Error(noTextMessage);
|
||||
}
|
||||
|
||||
// Worker diff
|
||||
await processWithWorker(text1, text2, color1, color2, complexMessage, largeFilesMessage);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Comparison failed:', error);
|
||||
alert(error.message || invalidPdfMessage);
|
||||
result1.innerHTML = '';
|
||||
result2.innerHTML = '';
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = submitText;
|
||||
}
|
||||
}
|
||||
|
||||
// FileReader helper
|
||||
function readFileAsArrayBuffer(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = reject;
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
// Header validation (PDF.js-specific, but client-side)
|
||||
async function validatePdfHeader(data, fileName) {
|
||||
const header = new Uint8Array(data.slice(0, 8));
|
||||
const headerStr = String.fromCharCode(...header);
|
||||
console.log(`${fileName} header:`, headerStr);
|
||||
if (!headerStr.startsWith('%PDF-')) {
|
||||
throw new Error(`${fileName} is not a valid PDF (header: ${headerStr}).`);
|
||||
}
|
||||
if (data.byteLength < 100) {
|
||||
throw new Error(`${fileName} is too short.`);
|
||||
}
|
||||
}
|
||||
|
||||
// PDF loading with catch
|
||||
function loadPdfWithErrorHandling(options, fileName) {
|
||||
return pdfjsLib.getDocument(options).promise
|
||||
.then(pdf => {
|
||||
console.log(`${fileName} loaded: ${pdf.numPages} pages`);
|
||||
return pdf;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`${fileName} load failed:`, err);
|
||||
if (err.name === 'InvalidPDFException') {
|
||||
throw new Error(`${fileName}: Invalid PDF structure. Re-upload.`);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// Text extraction
|
||||
async function extractText(pdf, fileName, statusElement) {
|
||||
const pages = [];
|
||||
const totalPages = pdf.numPages;
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const content = await page.getTextContent();
|
||||
const strings = content.items.map(item => item.str).join(' ');
|
||||
pages.push(strings);
|
||||
statusElement.innerHTML = `${fileName}: ${Math.round((i / totalPages) * 100)}%`;
|
||||
}
|
||||
return pages.join(' ');
|
||||
}
|
||||
|
||||
// Worker processing
|
||||
async function processWithWorker(text1, text2, color1, color2, complexMessage, largeFilesMessage) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker('./js/compare/pdfWorker.js');
|
||||
const timeout = setTimeout(() => {
|
||||
worker.terminate();
|
||||
reject(new Error('Timeout: Files too complex.'));
|
||||
}, 30000);
|
||||
|
||||
worker.postMessage({ type: 'SET_COMPLEX_MESSAGE', message: complexMessage });
|
||||
worker.postMessage({ type: 'SET_TOO_LARGE_MESSAGE', message: largeFilesMessage });
|
||||
|
||||
worker.onerror = (error) => {
|
||||
clearTimeout(timeout);
|
||||
worker.terminate();
|
||||
reject(new Error('Worker error: ' + error.message));
|
||||
};
|
||||
|
||||
worker.onmessage = (e) => {
|
||||
clearTimeout(timeout);
|
||||
const { status, differences, message } = e.data;
|
||||
if (status === 'error') {
|
||||
worker.terminate();
|
||||
reject(new Error(message));
|
||||
return;
|
||||
}
|
||||
if (status === 'warning') {
|
||||
alert(message);
|
||||
}
|
||||
if (status === 'success' && differences) {
|
||||
displayDifferences(differences, color1, color2);
|
||||
worker.terminate();
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
worker.postMessage({ type: 'COMPARE', text1, text2, color1, color2 });
|
||||
});
|
||||
}
|
||||
|
||||
// Display differences
|
||||
function displayDifferences(differences, color1, color2) {
|
||||
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;
|
||||
} else if (color === color1) {
|
||||
span1.style.color = color;
|
||||
span2.style.color = "transparent";
|
||||
span2.style.userSelect = "none";
|
||||
} else {
|
||||
span1.style.color = color || 'black';
|
||||
span2.style.color = color || 'black';
|
||||
}
|
||||
|
||||
span1.textContent = word;
|
||||
span2.textContent = word;
|
||||
resultDiv1.appendChild(span1);
|
||||
resultDiv2.appendChild(span2);
|
||||
|
||||
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>
|
||||
@@ -1,74 +0,0 @@
|
||||
<!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 id="grayscaleCheck" name="grayscale" type="checkbox" value="true">
|
||||
<label for="grayscaleCheck" th:text="#{compress.grayscale.label}">
|
||||
Convert images to grayscale for better compression
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-3">
|
||||
<input id="linearizeCheck" name="linearize" type="checkbox" value="true">
|
||||
<label for="linearizeCheck" th:text="#{compress.linearize.label}">
|
||||
Linearize PDF for faster web viewing
|
||||
</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>
|
||||
@@ -1,52 +0,0 @@
|
||||
<!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=#{extractAttachments.title}, header=#{extractAttachments.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">folder_zip</span>
|
||||
<span class="tool-header-text"
|
||||
th:text="#{extractAttachments.header}"></span>
|
||||
</div>
|
||||
|
||||
<p class="tool-description"
|
||||
th:text="#{extractAttachments.description}"></p>
|
||||
|
||||
<form action="#"
|
||||
th:action="@{/api/v1/misc/extract-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>
|
||||
|
||||
<!-- Submit button -->
|
||||
<button type="submit"
|
||||
id="submitBtn"
|
||||
class="btn btn-primary"
|
||||
th:text="#{extractAttachments.downloadButton}">Extract Attachments</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,59 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,43 +0,0 @@
|
||||
<!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="form-check mb-3">
|
||||
<input id="allowDuplicates" name="allowDuplicates" type="checkbox">
|
||||
<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>
|
||||
@@ -1,51 +0,0 @@
|
||||
<!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 id="flattenOnlyForms" name="flattenOnlyForms" type="checkbox">
|
||||
<label for="flattenOnlyForms" th:text="#{flatten.flattenOnlyForms}" ></label>
|
||||
</div>
|
||||
<div class="form-group ms-3 mt-2">
|
||||
<label class="form-label" for="renderDpi" th:text="#{flatten.renderDpi}"></label>
|
||||
<input
|
||||
class="form-control"
|
||||
id="renderDpi"
|
||||
min="72"
|
||||
name="renderDpi"
|
||||
step="1"
|
||||
th:attr="aria-describedby=${'renderDpiHelp'}"
|
||||
type="number"
|
||||
>
|
||||
<small class="form-text text-muted" id="renderDpiHelp" th:text="#{flatten.renderDpi.help}"></small>
|
||||
</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>
|
||||
@@ -1,240 +0,0 @@
|
||||
<!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 onchange="handleLangSelection()" required th:id="${'language-' + language}" th:name="languages" th:value="${language}" type="checkbox" />
|
||||
<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 id="sidecar" name="sidecar" type="checkbox" value="true">
|
||||
<label for="sidecar">Include OCR text in sidecar text file</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="deskew" name="deskew" type="checkbox" value="true">
|
||||
<label for="deskew">Deskew input file</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="clean" name="clean" type="checkbox" value="true">
|
||||
<label for="clean">Clean input file</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="cleanFinal" name="cleanFinal" type="checkbox" value="true">
|
||||
<label for="cleanFinal">Clean final output</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="removeImagesAfter" name="removeImagesAfter" type="checkbox" value="true">
|
||||
<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>
|
||||
@@ -1,35 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,68 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,41 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,30 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,105 +0,0 @@
|
||||
<!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>
|
||||
<option th:text="#{replace-color.selectText.12}" value="COLOR_SPACE_CONVERSION"></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>
|
||||
|
||||
<div class="card mb-3" id="color-space-info" style="display: none">
|
||||
<div class="card-body">
|
||||
<h4 th:text="#{replace-color.selectText.13}"></h4>
|
||||
<p th:text="#{replace-color.selectText.14}"></p>
|
||||
<ul>
|
||||
<li th:text="#{replace-color.selectText.15}"></li>
|
||||
<li th:text="#{replace-color.selectText.16}"></li>
|
||||
<li th:text="#{replace-color.selectText.17}"></li>
|
||||
</ul>
|
||||
</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();
|
||||
$('#color-space-info').hide();
|
||||
|
||||
if (selectedOption === "HIGH_CONTRAST_COLOR") {
|
||||
$('#high-contrast-options').show();
|
||||
} else if (selectedOption === "CUSTOM_COLOR") {
|
||||
$('#custom-color-1').show();
|
||||
$('#custom-color-2').show();
|
||||
} else if (selectedOption === "COLOR_SPACE_CONVERSION") {
|
||||
$('#color-space-info').show();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,162 +0,0 @@
|
||||
<!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=#{scannerEffect.title}, header=#{scannerEffect.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="#{scannerEffect.title}"></span>
|
||||
</div>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" id="uploadForm" th:action="@{'/api/v1/misc/scanner-effect'}">
|
||||
<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="#{scannerEffect.quality}"></label>
|
||||
<select class="form-select" id="quality" name="quality">
|
||||
<option value="low" th:text="#{scannerEffect.quality.low}"></option>
|
||||
<option value="medium" th:text="#{scannerEffect.quality.medium}"></option>
|
||||
<option value="high" th:text="#{scannerEffect.quality.high}" selected></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="rotation" class="form-label" th:text="#{scannerEffect.rotation}"></label>
|
||||
<select class="form-select" id="rotation" name="rotation">
|
||||
<option value="none" th:text="#{scannerEffect.rotation.none}"></option>
|
||||
<option value="slight" th:text="#{scannerEffect.rotation.slight}" selected></option>
|
||||
<option value="moderate" th:text="#{scannerEffect.rotation.moderate}"></option>
|
||||
<option value="severe" th:text="#{scannerEffect.rotation.severe}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input id="advancedSettingsToggle" type="checkbox">
|
||||
<label for="advancedSettingsToggle" th:text="#{scannerEffect.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="#{scannerEffect.colorspace}"></label>
|
||||
<select class="form-select" id="colorspace" name="colorspace">
|
||||
<option value="grayscale" th:text="#{scannerEffect.colorspace.grayscale}" selected></option>
|
||||
<option value="color" th:text="#{scannerEffect.colorspace.color}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="border" class="form-label" th:text="#{scannerEffect.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="#{scannerEffect.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="#{scannerEffect.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="#{scannerEffect.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="#{scannerEffect.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="#{scannerEffect.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="#{scannerEffect.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="mb-3">
|
||||
<input id="yellowish" name="yellowish" type="checkbox">
|
||||
<label for="yellowish" th:text="#{scannerEffect.yellowish}"></label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="resolution" class="form-label" th:text="#{scannerEffect.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="#{scannerEffect.submit}" id="submitBtn"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
<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>
|
||||
@@ -1,96 +0,0 @@
|
||||
<!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>
|
||||
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>
|
||||
@@ -1,203 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,34 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,44 +0,0 @@
|
||||
<!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 id="addBorder" name="addBorder" type="checkbox">
|
||||
<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>
|
||||
@@ -1,231 +0,0 @@
|
||||
<!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}]]',
|
||||
duplicate: '[[#{multiTool.duplicate}]]',
|
||||
dragDropMessage: '[[#{multiTool.dragDropMessage}]]',
|
||||
undo: '[[#{multiTool.undo}]]',
|
||||
redo: '[[#{multiTool.redo}]]',
|
||||
};
|
||||
|
||||
window.multiTool = Object.assign({}, window.multiTool, {
|
||||
svgNotSupported: '[[#{multiTool.svgNotSupported}]]',
|
||||
});
|
||||
|
||||
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>
|
||||
@@ -1,102 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,77 +0,0 @@
|
||||
<!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" id="pageToOrganizeSection">
|
||||
<label for="pageOrder" th:text="#{pageOrderPrompt}"></label>
|
||||
<input type="text" class="form-control" id="pageOrder" name="pageNumbers"
|
||||
th:placeholder="#{pdfOrganiser.placeholder}">
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfOrganiser.submit}"></button>
|
||||
</form>
|
||||
<script>
|
||||
// Only display the page input field when split mode is custom.
|
||||
function togglePageInput() {
|
||||
const mode = document.getElementById('customMode').value;
|
||||
const pageInputSection = document.getElementById('pageToOrganizeSection');
|
||||
if (mode === "") {
|
||||
pageInputSection.style.display = "block";
|
||||
document.getElementById('pageOrder').setAttribute("required", "true");
|
||||
} else {
|
||||
pageInputSection.style.display = "none";
|
||||
document.getElementById('pageOrder').removeAttribute("required");
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('customMode').addEventListener('change', togglePageInput);
|
||||
window.addEventListener('DOMContentLoaded', togglePageInput);
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,30 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,203 +0,0 @@
|
||||
<!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/pipeline.js'}"></script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,511 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,35 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,48 +0,0 @@
|
||||
<!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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user