Merge pull request #184 from Frooodle/conditional
Conditional removable PDF services
This commit is contained in:
commit
b0232eb917
18 changed files with 612 additions and 159 deletions
34
.github/workflows/push-docker.yml
vendored
34
.github/workflows/push-docker.yml
vendored
|
@ -50,12 +50,13 @@ jobs:
|
|||
id: meta
|
||||
uses: docker/metadata-action@v4.4.0
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
ghcr.io/${{ github.repository_owner }}/s-pdf
|
||||
tags: |
|
||||
${{ steps.versionNumber.outputs.versionNumber }}${{ github.ref == 'refs/heads/main' && '-alpha' || '' }}
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
ghcr.io/${{ github.repository_owner }}/s-pdf
|
||||
tags: |
|
||||
${{ steps.versionNumber.outputs.versionNumber }}
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
|
@ -63,13 +64,32 @@ jobs:
|
|||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Build and push
|
||||
- name: Build and push main Dockerfile
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
|
||||
- name: Build and push Dockerfile-ultralite
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile-ultralite
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf:ultra-light-latest
|
||||
ghcr.io/${{ github.repository_owner }}/s-pdf:ultra-light-latest
|
||||
labels: |
|
||||
${{ steps.meta.outputs.labels }}
|
||||
type=raw,value=ultra-light-latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
type=raw,value=ultra-light-alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
|
||||
|
|
14
Dockerfile-ultralite
Normal file
14
Dockerfile-ultralite
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Build jbig2enc in a separate stage
|
||||
FROM openjdk:17-jdk-slim
|
||||
|
||||
# Copy the application JAR file
|
||||
COPY build/libs/*.jar app.jar
|
||||
|
||||
# Expose the application port
|
||||
EXPOSE 8080
|
||||
|
||||
# Set environment variables
|
||||
ENV GROUPS_TO_REMOVE=LibreOffice,CLI
|
||||
|
||||
# Run the application
|
||||
CMD ["java", "-jar", "/app.jar"]
|
|
@ -26,11 +26,11 @@ RUN git clone https://github.com/agl/jbig2enc && \
|
|||
FROM openjdk:17-jdk-slim AS base
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
libreoffice-core \
|
||||
libreoffice-core-nogui \
|
||||
libreoffice-common \
|
||||
libreoffice-writer \
|
||||
libreoffice-calc \
|
||||
libreoffice-impress \
|
||||
libreoffice-writer-nogui \
|
||||
libreoffice-calc-nogui \
|
||||
libreoffice-impress-nogui \
|
||||
python3-uno \
|
||||
python3-pip \
|
||||
unoconv \
|
||||
|
|
52
README.md
52
README.md
|
@ -10,6 +10,11 @@
|
|||
|
||||
This is a powerful locally hosted web based PDF manipulation tool using docker that allows you to perform various operations on PDF files, such as splitting merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application started as a 100% ChatGPT-made application and has evolved to include a wide range of features to handle all your PDF needs.
|
||||
|
||||
Stirling PDF makes no outbount calls for any record keeping or tracking.
|
||||
|
||||
All files and PDFs are either purely client side, in server memory only during the execution of the task or within a temporay file only for execution of the task.
|
||||
Any file which has been downloaded by the user will have already been deleted from the server by that time.
|
||||
|
||||
Feel free to request any features or bug fixes either in github issues or our [Discord](https://discord.gg/Cn8pWhQRxZ)
|
||||
|
||||
|
||||
|
@ -42,6 +47,7 @@ Feel free to request any features or bug fixes either in github issues or our [D
|
|||
- Parallel file processing and downloads
|
||||
- API for integration with external scripts
|
||||
|
||||
For a overview of the tasks and the technology each uses please view [groups.md](https://github.com/Frooodle/Stirling-PDF/blob/main/Groups.md)
|
||||
Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) hosted by the team at adminforge.de
|
||||
|
||||
## Technologies used
|
||||
|
@ -106,14 +112,16 @@ Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md
|
|||
|
||||
## Want to add your own language?
|
||||
Stirling PDF currently supports
|
||||
- English
|
||||
- Arabic (العربية)
|
||||
- German (Deutsch)
|
||||
- French (Français)
|
||||
- Spanish (Español)
|
||||
- Chinese (简体中文)
|
||||
- Catalan (Català)
|
||||
- Italian (Italiano)
|
||||
- English (English) (en_GB)
|
||||
- Arabic (العربية) (ar_AR)
|
||||
- German (Deutsch) (de_DE)
|
||||
- French (Français) (fr_FR)
|
||||
- Spanish (Español) (es_ES)
|
||||
- Chinese (简体中文) (it_IT)
|
||||
- Catalan (Català) (zh_CN)
|
||||
- Italian (Italiano) (ca_CA)
|
||||
- Swedish (Svenska) (sv_SE)
|
||||
- Polish (Polski) (pl_PL)
|
||||
|
||||
If you want to add your own language to Stirling-PDF please refer
|
||||
https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md
|
||||
|
@ -133,10 +141,34 @@ Simply use environment variables APP_HOME_NAME, APP_HOME_DESCRIPTION and APP_NAV
|
|||
If running Java directly, you can also pass these as properties using -D arguments.
|
||||
|
||||
Using the same method you can also change
|
||||
- The default language by providing APP_LOCALE with values like de-DE fr-FR or ar-AR to select your default language (Will always default to English on invalid locale)
|
||||
- Enable/Disable search engine visiblility with ALLOW_GOOGLE_VISIBILITY with true / false values. Default disable visibility.
|
||||
|
||||
- The default language by providing APP_LOCALE with values like de-DE fr-FR or ar-AR (Note the - character not _ ) to select your default language (Will always default to English on invalid locale) Current accepted locales can be seen above in the Want to add your own language section
|
||||
- Enable/Disable search engine visiblility with ALLOW_GOOGLE_VISIBILITY with true / false values. Default disable visiblility.
|
||||
- Change root URI for Stirling-PDF ie change server.com/ to server.com/pdf-app by running APP_ROOT_PATH as pdf-app
|
||||
- Disable and remove endpoints and functionality from Stirling-PDF. Currently the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma seperated lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image to pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Frooodle/Stirling-PDF/blob/main/Groups.md)
|
||||
|
||||
|
||||
## API
|
||||
For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation
|
||||
[here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation
|
||||
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q1: Where is the authentication for Stirling PDF
|
||||
There is no Auth within Stirling PDF and there is none planned. This feature will not be added. Instead its recommended to use trusted and secure auth software like Authentik or Authelia.
|
||||
|
||||
### Q2: What are your planned features
|
||||
- Crop
|
||||
- Progress bar/Tracking
|
||||
- Annotations (Sign/draw logic directly on page)
|
||||
- Full custom logic pipelines to combine multiple operations together.
|
||||
- Folder support with auto scanning to perform operations on
|
||||
- Redact text
|
||||
- Add page numbers
|
||||
- Auto rename (Renames file based on file title text)
|
||||
- URL to PDF
|
||||
- Change contrast
|
||||
|
||||
### Q3: Why is my application download .htm files
|
||||
This is a issue caused commonly by your NGINX congifuration. The default file upload size for NGINX is 1MB, you need to add the following in your Nginx sites-available file. client_max_body_size SIZE; Where "SIZE" is 50M for example for 50MB files.
|
|
@ -24,6 +24,11 @@ dependencies {
|
|||
|
||||
//general PDF
|
||||
implementation 'org.apache.pdfbox:pdfbox:2.0.28'
|
||||
|
||||
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
implementation 'io.micrometer:micrometer-core'
|
||||
|
||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||
|
||||
}
|
||||
|
|
32
groups.md
Normal file
32
groups.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript
|
||||
--------------------|---------|---------|----------|-------|------|--------|--------|-------------|--------- |-------- |-----------
|
||||
remove-pages | X | | | | | | | | | X |
|
||||
merge-pdfs | X | | | | | | | | | X |
|
||||
split-pdfs | X | | | | | | | | | X |
|
||||
pdf-organizer | X | | | | | | | | | X | X
|
||||
rotate-pdf | X | | | | | | | | | X |
|
||||
pdf-to-img | | X | | | | | | | | X |
|
||||
img-to-pdf | | X | | | | | | | | X |
|
||||
pdf-to-pdfa | | X | | | X | | | | X | |
|
||||
file-to-pdf | | X | | | X | | | X | | |
|
||||
xlsx-to-pdf | | X | | | X | | | X | | |
|
||||
pdf-to-word | | X | | | X | | | X | | |
|
||||
pdf-to-presentation | | X | | | X | | | X | | |
|
||||
pdf-to-text | | X | | | X | | | X | | |
|
||||
pdf-to-html | | X | | | X | | | X | | |
|
||||
pdf-to-xml | | X | | | X | | | X | | |
|
||||
add-password | | | X | | | | | | | X |
|
||||
remove-password | | | X | | | | | | | X |
|
||||
change-permissions | | | X | | | | | | | X |
|
||||
add-watermark | | | X | | | | | | | X |
|
||||
ocr-pdf | | | | X | X | | | | X | |
|
||||
add-image | | | | X | | | | | | X |
|
||||
compress-pdf | | | | X | X | | | | X
|
||||
extract-images | | | | X | | | | | | X |
|
||||
change-metadata | | | | X | | | | | | X |
|
||||
extract-image-scans | | | | X | X | X | X | | | |
|
||||
sign | | | | X | | | | | | | X
|
||||
flatten | | | | X | | | | | | |
|
||||
repair | | | | X | X | | | X | | |
|
||||
remove-blanks | | | | X | X | X | X | | | |
|
||||
compare | | | | X | | | | | | | X
|
|
@ -0,0 +1,192 @@
|
|||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.slf4j.Logger;
|
||||
@Service
|
||||
public class EndpointConfiguration {
|
||||
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
|
||||
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
||||
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
||||
|
||||
public EndpointConfiguration() {
|
||||
init();
|
||||
processEnvironmentConfigs();
|
||||
}
|
||||
|
||||
public void enableEndpoint(String endpoint) {
|
||||
endpointStatuses.put(endpoint, true);
|
||||
}
|
||||
|
||||
public void disableEndpoint(String endpoint) {
|
||||
logger.info("Disabling {}", endpoint);
|
||||
endpointStatuses.put(endpoint, false);
|
||||
}
|
||||
|
||||
public boolean isEndpointEnabled(String endpoint) {
|
||||
if (endpoint.startsWith("/")) {
|
||||
endpoint = endpoint.substring(1);
|
||||
}
|
||||
return endpointStatuses.getOrDefault(endpoint, true);
|
||||
}
|
||||
|
||||
public void addEndpointToGroup(String group, String endpoint) {
|
||||
endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
|
||||
}
|
||||
|
||||
public void enableGroup(String group) {
|
||||
Set<String> endpoints = endpointGroups.get(group);
|
||||
if (endpoints != null) {
|
||||
for (String endpoint : endpoints) {
|
||||
enableEndpoint(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void disableGroup(String group) {
|
||||
Set<String> endpoints = endpointGroups.get(group);
|
||||
if (endpoints != null) {
|
||||
for (String endpoint : endpoints) {
|
||||
disableEndpoint(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void init() {
|
||||
// Adding endpoints to "PageOps" group
|
||||
addEndpointToGroup("PageOps", "remove-pages");
|
||||
addEndpointToGroup("PageOps", "merge-pdfs");
|
||||
addEndpointToGroup("PageOps", "split-pdfs");
|
||||
addEndpointToGroup("PageOps", "pdf-organizer");
|
||||
addEndpointToGroup("PageOps", "rotate-pdf");
|
||||
|
||||
// Adding endpoints to "Convert" group
|
||||
addEndpointToGroup("Convert", "pdf-to-img");
|
||||
addEndpointToGroup("Convert", "img-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-pdfa");
|
||||
addEndpointToGroup("Convert", "file-to-pdf");
|
||||
addEndpointToGroup("Convert", "xlsx-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-word");
|
||||
addEndpointToGroup("Convert", "pdf-to-presentation");
|
||||
addEndpointToGroup("Convert", "pdf-to-text");
|
||||
addEndpointToGroup("Convert", "pdf-to-html");
|
||||
addEndpointToGroup("Convert", "pdf-to-xml");
|
||||
|
||||
// Adding endpoints to "Security" group
|
||||
addEndpointToGroup("Security", "add-password");
|
||||
addEndpointToGroup("Security", "remove-password");
|
||||
addEndpointToGroup("Security", "change-permissions");
|
||||
addEndpointToGroup("Security", "add-watermark");
|
||||
|
||||
// Adding endpoints to "Other" group
|
||||
addEndpointToGroup("Other", "ocr-pdf");
|
||||
addEndpointToGroup("Other", "add-image");
|
||||
addEndpointToGroup("Other", "compress-pdf");
|
||||
addEndpointToGroup("Other", "extract-images");
|
||||
addEndpointToGroup("Other", "change-metadata");
|
||||
addEndpointToGroup("Other", "extract-image-scans");
|
||||
addEndpointToGroup("Other", "sign");
|
||||
addEndpointToGroup("Other", "flatten");
|
||||
addEndpointToGroup("Other", "repair");
|
||||
addEndpointToGroup("Other", "remove-blanks");
|
||||
addEndpointToGroup("Other", "compare");
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//CLI
|
||||
addEndpointToGroup("CLI", "compress-pdf");
|
||||
addEndpointToGroup("CLI", "extract-image-scans");
|
||||
addEndpointToGroup("CLI", "remove-blanks");
|
||||
addEndpointToGroup("CLI", "repair");
|
||||
addEndpointToGroup("CLI", "pdf-to-pdfa");
|
||||
addEndpointToGroup("CLI", "file-to-pdf");
|
||||
addEndpointToGroup("CLI", "xlsx-to-pdf");
|
||||
addEndpointToGroup("CLI", "pdf-to-word");
|
||||
addEndpointToGroup("CLI", "pdf-to-presentation");
|
||||
addEndpointToGroup("CLI", "pdf-to-text");
|
||||
addEndpointToGroup("CLI", "pdf-to-html");
|
||||
addEndpointToGroup("CLI", "pdf-to-xml");
|
||||
addEndpointToGroup("CLI", "ocr-pdf");
|
||||
|
||||
//python
|
||||
addEndpointToGroup("Python", "extract-image-scans");
|
||||
addEndpointToGroup("Python", "remove-blanks");
|
||||
|
||||
|
||||
|
||||
//openCV
|
||||
addEndpointToGroup("OpenCV", "extract-image-scans");
|
||||
addEndpointToGroup("OpenCV", "remove-blanks");
|
||||
|
||||
//LibreOffice
|
||||
addEndpointToGroup("LibreOffice", "repair");
|
||||
addEndpointToGroup("LibreOffice", "file-to-pdf");
|
||||
addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-word");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-text");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-html");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-xml");
|
||||
|
||||
|
||||
//OCRmyPDF
|
||||
addEndpointToGroup("OCRmyPDF", "compress-pdf");
|
||||
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
|
||||
addEndpointToGroup("OCRmyPDF", "ocr-pdf");
|
||||
|
||||
//Java
|
||||
addEndpointToGroup("Java", "merge-pdfs");
|
||||
addEndpointToGroup("Java", "remove-pages");
|
||||
addEndpointToGroup("Java", "split-pdfs");
|
||||
addEndpointToGroup("Java", "pdf-organizer");
|
||||
addEndpointToGroup("Java", "rotate-pdf");
|
||||
addEndpointToGroup("Java", "pdf-to-img");
|
||||
addEndpointToGroup("Java", "img-to-pdf");
|
||||
addEndpointToGroup("Java", "add-password");
|
||||
addEndpointToGroup("Java", "remove-password");
|
||||
addEndpointToGroup("Java", "change-permissions");
|
||||
addEndpointToGroup("Java", "add-watermark");
|
||||
addEndpointToGroup("Java", "add-image");
|
||||
addEndpointToGroup("Java", "extract-images");
|
||||
addEndpointToGroup("Java", "change-metadata");
|
||||
|
||||
|
||||
//Javascript
|
||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||
addEndpointToGroup("Javascript", "sign");
|
||||
addEndpointToGroup("Javascript", "compare");
|
||||
|
||||
}
|
||||
|
||||
private void processEnvironmentConfigs() {
|
||||
String endpointsToRemove = System.getenv("ENDPOINTS_TO_REMOVE");
|
||||
String groupsToRemove = System.getenv("GROUPS_TO_REMOVE");
|
||||
|
||||
if (endpointsToRemove != null) {
|
||||
String[] endpoints = endpointsToRemove.split(",");
|
||||
for (String endpoint : endpoints) {
|
||||
disableEndpoint(endpoint.trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (groupsToRemove != null) {
|
||||
String[] groups = groupsToRemove.split(",");
|
||||
for (String group : groups) {
|
||||
disableGroup(group.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@Component
|
||||
public class EndpointInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Autowired
|
||||
private EndpointConfiguration endpointConfiguration;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
throws Exception {
|
||||
String requestURI = request.getRequestURI();
|
||||
if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
|
||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package stirling.software.SPDF.config;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Meter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.config.MeterFilter;
|
||||
import io.micrometer.core.instrument.config.MeterFilterReply;
|
||||
|
||||
@Configuration
|
||||
public class MetricsConfig {
|
||||
|
||||
@Bean
|
||||
public MeterFilter meterFilter() {
|
||||
return new MeterFilter() {
|
||||
@Override
|
||||
public MeterFilterReply accept(Meter.Id id) {
|
||||
if (id.getName().equals("http.requests")) {
|
||||
return MeterFilterReply.NEUTRAL;
|
||||
}
|
||||
return MeterFilterReply.DENY;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Meter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.config.MeterFilter;
|
||||
import io.micrometer.core.instrument.config.MeterFilterReply;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@Component
|
||||
public class MetricsFilter extends OncePerRequestFilter {
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
@Autowired
|
||||
public MetricsFilter(MeterRegistry meterRegistry) {
|
||||
this.meterRegistry = meterRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
String uri = request.getRequestURI();
|
||||
|
||||
// Ignore static resources
|
||||
if (!(uri.startsWith("/js") || uri.startsWith("/images") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) {
|
||||
Counter counter = Counter.builder("http.requests")
|
||||
.tag("uri", uri)
|
||||
.tag("method", request.getMethod())
|
||||
.register(meterRegistry);
|
||||
|
||||
counter.increment();
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private EndpointInterceptor endpointInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(endpointInterceptor);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package stirling.software.SPDF.controller.web;
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Meter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1")
|
||||
public class MetricsController {
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
public MetricsController(MeterRegistry meterRegistry) {
|
||||
this.meterRegistry = meterRegistry;
|
||||
}
|
||||
|
||||
@GetMapping("/status")
|
||||
@Operation(summary = "Application status and version",
|
||||
description = "This endpoint returns the status of the application and its version number.")
|
||||
public Map<String, String> getStatus() {
|
||||
Map<String, String> status = new HashMap<>();
|
||||
status.put("status", "UP");
|
||||
status.put("version", getClass().getPackage().getImplementationVersion());
|
||||
return status;
|
||||
}
|
||||
|
||||
@GetMapping("/loads")
|
||||
@Operation(summary = "GET request count",
|
||||
description = "This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.")
|
||||
public Double getPageLoads(@RequestParam Optional<String> endpoint) {
|
||||
try {
|
||||
double count = 0.0;
|
||||
|
||||
for (Meter meter : meterRegistry.getMeters()) {
|
||||
if (meter.getId().getName().equals("http.requests")) {
|
||||
String method = meter.getId().getTag("method");
|
||||
if (method != null && method.equals("GET")) {
|
||||
if (meter instanceof Counter) {
|
||||
count += ((Counter) meter).count();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
} catch (Exception e) {
|
||||
return -1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/requests")
|
||||
@Operation(summary = "POST request count",
|
||||
description = "This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.")
|
||||
public Double getTotalRequests(@RequestParam Optional<String> endpoint) {
|
||||
try {
|
||||
Counter counter;
|
||||
if (endpoint.isPresent()) {
|
||||
counter = meterRegistry.get("http.requests")
|
||||
.tags("method", "POST", "uri", endpoint.get()).counter();
|
||||
} else {
|
||||
counter = meterRegistry.get("http.requests")
|
||||
.tags("method", "POST").counter();
|
||||
}
|
||||
return counter.count();
|
||||
} catch (Exception e) {
|
||||
return -1.0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -23,3 +23,6 @@ spring.devtools.restart.enabled=true
|
|||
spring.devtools.livereload.enabled=true
|
||||
|
||||
spring.thymeleaf.encoding=UTF-8
|
||||
|
||||
server.connection-timeout=${CONNECTION_TIMEOUT:5m}
|
||||
spring.mvc.async.request-timeout=${ASYNC_CONNECTION_TIMEOUT:300000}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div th:fragment="card" class="feature-card" th:id="${id}">
|
||||
<div th:fragment="card" class="feature-card" th:id="${id}" th:if="${@endpointConfiguration.isEndpointEnabled(cardLink)}">
|
||||
<a th:href="${cardLink}">
|
||||
<div class="d-flex align-items-center"> <!-- Add a flex container to align the SVG and title -->
|
||||
<img th:if="${svgPath}" id="card-icon" class="home-card-icon home-card-icon-colour" th:src="${svgPath}" alt="Icon" width="30" height="30">
|
||||
|
|
|
@ -151,26 +151,12 @@ function compareVersions(version1, version2) {
|
|||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<!-- Existing menu items -->
|
||||
<a class="dropdown-item" href="#" th:href="@{merge-pdfs}" th:classappend="${currentPage}=='merge-pdfs' ? 'active' : ''" th:title="#{home.merge.desc}">
|
||||
<img class="icon" src="images/union.svg" alt="icon">
|
||||
<span class="icon-text" th:text="#{home.merge.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{split-pdfs}" th:classappend="${currentPage}=='split-pdfs' ? 'active' : ''" th:title="#{home.split.desc}">
|
||||
<img class="icon" src="images/layout-split.svg" alt="icon">
|
||||
<span class="icon-text" th:text="#{home.split.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{pdf-organizer}" th:classappend="${currentPage}=='pdf-organizer' ? 'active' : ''" th:title="#{home.pdfOrganiser.desc}">
|
||||
<img class="icon" src="images/sort-numeric-down.svg" alt="icon">
|
||||
<span class="icon-text" th:text="#{home.pdfOrganiser.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{rotate-pdf}" th:classappend="${currentPage}=='rotate-pdf' ? 'active' : ''" th:title="#{home.rotate.desc}">
|
||||
<img class="icon" src="images/arrow-clockwise.svg" alt="icon">
|
||||
<span class="icon-text" th:text="#{home.rotate.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{remove-pages}" th:classappend="${currentPage}=='remove-pages' ? 'active' : ''" th:title="#{home.removePages.desc}">
|
||||
<img class="icon" src="images/file-earmark-x.svg" alt="icon">
|
||||
<span class="icon-text" th:text="#{home.removePages.title}"></span>
|
||||
</a>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('merge-pdfs', 'images/union.svg', 'home.merge.title', 'home.merge.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('split-pdfs', 'images/layout-split.svg', 'home.split.title', 'home.split.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'pdf-organizer', 'images/sort-numeric-down.svg', 'home.pdfOrganiser.title', 'home.pdfOrganiser.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'rotate-pdf', 'images/arrow-clockwise.svg', 'home.rotate.title', 'home.rotate.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'remove-pages', 'images/file-earmark-x.svg', 'home.removePages.title', 'home.removePages.desc')}"></div>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item nav-item-separator"></li>
|
||||
|
@ -181,56 +167,18 @@ function compareVersions(version1, version2) {
|
|||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<!-- Existing menu items -->
|
||||
<a class="dropdown-item" href="#" th:href="@{img-to-pdf}" th:classappend="${currentPage}=='img-to-pdf' ? 'active' : ''" th:title="#{home.imageToPdf.desc}">
|
||||
<img class="icon" src="images/image.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;">
|
||||
<span class="icon-text" th:text="#{home.imageToPdf.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{file-to-pdf}" th:classappend="${currentPage}=='file-to-pdf' ? 'active' : ''" th:title="#{home.fileToPDF.desc}">
|
||||
<img class="icon" src="images/file.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;">
|
||||
<span class="icon-text" th:text="#{home.fileToPDF.title}"></span>
|
||||
</a>
|
||||
<hr class="dropdown-divider">
|
||||
<a class="dropdown-item" href="#" th:href="@{pdf-to-img}" th:classappend="${currentPage}=='pdf-to-img' ? 'active' : ''" th:title="#{home.pdfToImage.desc}">
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('img-to-pdf', 'images/image.svg', 'home.imageToPdf.title', 'home.imageToPdf.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('file-to-pdf', 'images/file.svg', 'home.fileToPDF.title', 'home.fileToPDF.desc')}"></div>
|
||||
<hr class="dropdown-divider">
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-img', 'images/image.svg', 'home.pdfToImage.title', 'home.pdfToImage.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-word', 'images/file-earmark-word.svg', 'home.PDFToWord.title', 'home.PDFToWord.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-presentation', 'images/file-earmark-ppt.svg', 'home.PDFToPresentation.title', 'home.PDFToPresentation.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-text', 'images/filetype-txt.svg', 'home.PDFToText.title', 'home.PDFToText.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-html', 'images/filetype-html.svg', 'home.PDFToHTML.title', 'home.PDFToHTML.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-xml', 'images/filetype-xml.svg', 'home.PDFToXML.title', 'home.PDFToXML.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-pdfa', 'images/file-earmark-pdf.svg', 'home.pdfToPDFA.title', 'home.pdfToPDFA.desc')}"></div>
|
||||
|
||||
|
||||
<img class="icon" src="images/image.svg" alt="icon">
|
||||
<span class="icon-text" th:text="#{home.pdfToImage.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{pdf-to-word}" th:classappend="${currentPage}=='pdf-to-word' ? 'active' : ''" th:title="#{home.PDFToWord.desc}">
|
||||
|
||||
|
||||
<img class="icon" src="images/file-earmark-word.svg" alt="icon">
|
||||
<span class="icon-text" th:text="#{home.PDFToWord.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{pdf-to-presentation}" th:classappend="${currentPage}=='pdf-to-presentation' ? 'active' : ''" th:title="#{home.PDFToPresentation.desc}">
|
||||
|
||||
|
||||
<img class="icon" src="images/file-earmark-ppt.svg" alt="icon">
|
||||
<span class="icon-text" th:text="#{home.PDFToPresentation.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{pdf-to-text}" th:classappend="${currentPage}=='pdf-to-text' ? 'active' : ''" th:title="#{home.PDFToText.desc}">
|
||||
|
||||
|
||||
<img class="icon" src="images/filetype-txt.svg" alt="icon">
|
||||
<span class="icon-text" th:text="#{home.PDFToText.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{pdf-to-html}" th:classappend="${currentPage}=='pdf-to-html' ? 'active' : ''" th:title="#{home.PDFToHTML.desc}">
|
||||
|
||||
|
||||
<img class="icon" src="images/filetype-html.svg" alt="icon">
|
||||
<span class="icon-text" th:text="#{home.PDFToHTML.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{pdf-to-xml}" th:classappend="${currentPage}=='pdf-to-xml' ? 'active' : ''" th:title="#{home.PDFToXML.desc}">
|
||||
|
||||
<img class="icon" src="images/filetype-xml.svg" alt="icon">
|
||||
<span class="icon-text" th:text="#{home.PDFToXML.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{pdf-to-pdfa}" th:classappend="${currentPage}=='pdf-to-pdfa' ? 'active' : ''" th:title="#{home.pdfToPDFA.desc}">
|
||||
|
||||
<img class="icon" src="images/file-earmark-pdf.svg" alt="icon">
|
||||
<span class="icon-text" th:text="#{home.pdfToPDFA.title}"></span>
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
@ -243,18 +191,11 @@ function compareVersions(version1, version2) {
|
|||
<img class="icon" src="images/shield-check.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{navbar.security}"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="#" th:href="@{add-password}" th:classappend="${currentPage}=='add-password' ? 'active' : ''" th:title="#{home.addPassword.desc}">
|
||||
<img class="icon" src="images/lock.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.addPassword.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{remove-password}" th:classappend="${currentPage}=='remove-password' ? 'active' : ''" th:title="#{home.removePassword.desc}">
|
||||
<img class="icon" src="images/unlock.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.removePassword.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{change-permissions}" th:classappend="${currentPage}=='change-permissions' ? 'active' : ''" th:title="#{home.permissions.desc}">
|
||||
<img class="icon" src="images/shield-lock.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.permissions.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{add-watermark}" th:classappend="${currentPage}=='add-watermark' ? 'active' : ''" th:title="#{home.watermark.desc}">
|
||||
<img class="icon" src="images/droplet.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.watermark.title}"></span>
|
||||
</a>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-password', 'images/lock.svg', 'home.addPassword.title', 'home.addPassword.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-password', 'images/unlock.svg', 'home.removePassword.title', 'home.removePassword.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('change-permissions', 'images/shield-lock.svg', 'home.permissions.title', 'home.permissions.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-watermark', 'images/droplet.svg', 'home.watermark.title', 'home.watermark.desc')}"></div>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
|
||||
|
@ -266,39 +207,17 @@ function compareVersions(version1, version2) {
|
|||
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="#" th:href="@{ocr-pdf}" th:classappend="${currentPage}=='ocr-pdf' ? 'active' : ''" th:title="#{home.ocr.desc}">
|
||||
<img class="icon" src="images/search.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.ocr.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{add-image}" th:classappend="${currentPage}=='add-image' ? 'active' : ''" th:title="#{home.addImage.desc}">
|
||||
<img class="icon" src="images/file-earmark-richtext.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.addImage.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{compress-pdf}" th:classappend="${currentPage}=='compress-pdf' ? 'active' : ''" th:title="#{home.compressPdfs.desc}">
|
||||
<img class="icon" src="images/file-zip.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.compressPdfs.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{extract-images}" th:classappend="${currentPage}=='extract-images' ? 'active' : ''" th:title="#{home.extractImages.desc}">
|
||||
<img class="icon" src="images/images.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.extractImages.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{change-metadata}" th:classappend="${currentPage}=='change-metadata' ? 'active' : ''" th:title="#{home.changeMetadata.desc}">
|
||||
<img class="icon" src="images/clipboard-data.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.changeMetadata.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{extract-image-scans}" th:classappend="${currentPage}=='extract-image-scans' ? 'active' : ''" th:title="#{home.ScannerImageSplit.desc}">
|
||||
<img class="icon" src="images/scanner.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.ScannerImageSplit.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{sign}" th:classappend="${currentPage}=='sign' ? 'active' : ''" th:title="#{home.sign.desc}">
|
||||
<img class="icon" src="images/sign.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.sign.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{flatten}" th:classappend="${currentPage}=='flatten' ? 'active' : ''" th:title="#{home.flatten.desc}">
|
||||
<img class="icon" src="images/flatten.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.flatten.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{repair}" th:classappend="${currentPage}=='repair' ? 'active' : ''" th:title="#{home.repair.desc}">
|
||||
<img class="icon" src="images/wrench.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.repair.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{remove-blanks}" th:classappend="${currentPage}=='remove-blanks' ? 'active' : ''" th:title="#{home.removeBlanks.desc}">
|
||||
<img class="icon" src="images/blank-file.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.removeBlanks.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{compare}" th:classappend="${currentPage}=='compare' ? 'active' : ''" th:title="#{home.compare.desc}">
|
||||
<img class="icon" src="images/scales.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.compare.title}"></span>
|
||||
</a>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('ocr-pdf', 'images/search.svg', 'home.ocr.title', 'home.ocr.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-image', 'images/file-earmark-richtext.svg', 'home.addImage.title', 'home.addImage.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('compress-pdf', 'images/file-zip.svg', 'home.compressPdfs.title', 'home.compressPdfs.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('extract-images', 'images/images.svg', 'home.extractImages.title', 'home.extractImages.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('change-metadata', 'images/clipboard-data.svg', 'home.changeMetadata.title', 'home.changeMetadata.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('extract-image-scans', 'images/scanner.svg', 'home.ScannerImageSplit.title', 'home.ScannerImageSplit.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('sign', 'images/sign.svg', 'home.sign.title', 'home.sign.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('flatten', 'images/flatten.svg', 'home.flatten.title', 'home.flatten.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('repair', 'images/wrench.svg', 'home.repair.title', 'home.repair.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-blanks', 'images/blank-file.svg', 'home.removeBlanks.title', 'home.removeBlanks.desc')}"></div>
|
||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('compare', 'images/scales.svg', 'home.compare.title', 'home.compare.desc')}"></div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
|
@ -327,42 +246,43 @@ function compareVersions(version1, version2) {
|
|||
</svg>
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="languageDropdown">
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="en_GB">
|
||||
<img src="images/flags/gb.svg" alt="icon" width="20" height="15"> English
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="ar_AR">
|
||||
<img src="images/flags/sa.svg" alt="icon" width="20" height="15"> العربية
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="de_DE">
|
||||
<img src="images/flags/de.svg" alt="icon" width="20" height="15"> Deutsch
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="fr_FR">
|
||||
<img src="images/flags/fr.svg" alt="icon" width="20" height="15"> Français
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="es_ES">
|
||||
<img src="images/flags/es.svg" alt="icon" width="20" height="15"> Español
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="it_IT">
|
||||
<img src="images/flags/it.svg" alt="icon" width="20" height="15"> Italiano
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="zh_CN">
|
||||
<img src="images/flags/cn.svg" alt="icon" width="20" height="15"> 简体中文
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="ca_CA">
|
||||
<img src="images/flags/es-ct.svg" alt="icon" width="20" height="15"> Català
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="sv_SE">
|
||||
<img src="images/flags/se.svg" alt="icon" width="20" height="15"> Svenska
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="zh_CN">
|
||||
<img src="images/flags/cn.svg" alt="icon" width="20" height="15"> 简体中文
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="de_DE">
|
||||
<img src="images/flags/de.svg" alt="icon" width="20" height="15"> Deutsch
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="en_GB">
|
||||
<img src="images/flags/gb.svg" alt="icon" width="20" height="15"> English
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="es_ES">
|
||||
<img src="images/flags/es.svg" alt="icon" width="20" height="15"> Español
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="fr_FR">
|
||||
<img src="images/flags/fr.svg" alt="icon" width="20" height="15"> Français
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="it_IT">
|
||||
<img src="images/flags/it.svg" alt="icon" width="20" height="15"> Italiano
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="pl_PL">
|
||||
<img src="images/flags/pl.svg" alt="icon" width="20" height="15"> Polski
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="sv_SE">
|
||||
<img src="images/flags/se.svg" alt="icon" width="20" height="15"> Svenska
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="ru_RU">
|
||||
<img src="images/flags/ru.svg" alt="icon" width="20" height="15"> Русский
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
|
||||
<li class="nav-item">
|
||||
<!-- Settings Button -->
|
||||
<a href="#" class="nav-link" data-toggle="modal" data-target="#settingsModal">
|
||||
|
@ -433,10 +353,14 @@ function compareVersions(version1, version2) {
|
|||
<div class="modal-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<p class="mb-0" th:utext="#{settings.appVersion} + ' ' + ${@appVersion}"></p>
|
||||
<a href="swagger-ui/index.html" target="_blank">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"> API </button>
|
||||
</a>
|
||||
<a href="https://github.com/Frooodle/Stirling-PDF/releases" target="_blank">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="update-btn" th:utext="#{settings.update}"></button>
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="downloadOption" th:utext="#{settings.downloadOption.title}"></label>
|
||||
|
@ -457,6 +381,7 @@ function compareVersions(version1, version2) {
|
|||
<label class="custom-control-label" for="boredWaiting" th:text="#{bored}"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal" th:text="#{close}"></button>
|
||||
|
@ -467,6 +392,26 @@ function compareVersions(version1, version2) {
|
|||
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$(".nav-item.dropdown").each(function() {
|
||||
var $dropdownMenu = $(this).find(".dropdown-menu");
|
||||
if ($dropdownMenu.children().length <= 2 && $dropdownMenu.children("hr.dropdown-divider").length === $dropdownMenu.children().length) {
|
||||
$(this).prev('.nav-item.nav-item-separator').remove();
|
||||
$(this).remove();
|
||||
}
|
||||
});
|
||||
|
||||
//Sort languages by alphabet
|
||||
var list = $('.dropdown-menu[aria-labelledby="languageDropdown"]').children("a");
|
||||
list.sort(function(a, b) {
|
||||
var A = $(a).text().toUpperCase();
|
||||
var B = $(b).text().toUpperCase();
|
||||
return (A < B) ? -1 : (A > B) ? 1 : 0;
|
||||
})
|
||||
.appendTo('.dropdown-menu[aria-labelledby="languageDropdown"]');
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
// Get the download option from local storage, or set it to 'sameWindow' if it doesn't exist
|
||||
|
|
6
src/main/resources/templates/fragments/navbarEntry.html
Normal file
6
src/main/resources/templates/fragments/navbarEntry.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<div th:fragment="navbarEntry (endpoint, imgSrc, titleKey, descKey)" th:if="${@endpointConfiguration.isEndpointEnabled(endpoint)}">
|
||||
<a class="dropdown-item" href="#" th:href="@{${endpoint}}" th:classappend="${endpoint.equals(currentPage)} ? 'active' : ''" th:title="#{${descKey}}">
|
||||
<img class="icon" th:src="@{${imgSrc}}" alt="icon">
|
||||
<span class="icon-text" th:text="#{${titleKey}}"></span>
|
||||
</a>
|
||||
</div>
|
|
@ -88,6 +88,8 @@ filter: invert(0.2) sepia(2) saturate(50) hue-rotate(190deg);
|
|||
|
||||
<!-- Features -->
|
||||
<div class="features-container container">
|
||||
|
||||
|
||||
<div th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', svgPath='images/tools.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', svgPath='images/union.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='split-pdfs', cardTitle=#{home.split.title}, cardText=#{home.split.desc}, cardLink='split-pdfs', svgPath='images/layout-split.svg')}"></div>
|
||||
|
|
Loading…
Reference in a new issue