mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-22 04:09:22 +00:00
merge V2
This commit is contained in:
commit
fa022c1440
@ -6,7 +6,10 @@
|
|||||||
"Bash(./gradlew:*)",
|
"Bash(./gradlew:*)",
|
||||||
"Bash(grep:*)",
|
"Bash(grep:*)",
|
||||||
"Bash(cat:*)",
|
"Bash(cat:*)",
|
||||||
"Bash(find:*)"
|
"Bash(find:*)",
|
||||||
|
"Bash(npm test)",
|
||||||
|
"Bash(npm test:*)",
|
||||||
|
"Bash(ls:*)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
|
6
.github/workflows/PR-Auto-Deploy-V2.yml
vendored
6
.github/workflows/PR-Auto-Deploy-V2.yml
vendored
@ -48,6 +48,7 @@ jobs:
|
|||||||
"DarioGii"
|
"DarioGii"
|
||||||
"ConnorYoh"
|
"ConnorYoh"
|
||||||
"EthanHealy01"
|
"EthanHealy01"
|
||||||
|
"jbrunton96"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if author is in the authorized list
|
# Check if author is in the authorized list
|
||||||
@ -317,6 +318,7 @@ jobs:
|
|||||||
SYSTEM_MAXFILESIZE: "100"
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
METRICS_ENABLED: "true"
|
METRICS_ENABLED: "true"
|
||||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||||
|
SWAGGER_SERVER_URL: "http://${{ secrets.VPS_HOST }}:${V2_PORT}"
|
||||||
restart: on-failure:5
|
restart: on-failure:5
|
||||||
|
|
||||||
stirling-pdf-v2-frontend:
|
stirling-pdf-v2-frontend:
|
||||||
@ -385,10 +387,12 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${v2Port}`;
|
const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${v2Port}`;
|
||||||
|
const httpsUrl = `https://${v2Port}.ssl.stirlingpdf.cloud`;
|
||||||
|
|
||||||
const commentBody = `## 🚀 V2 Auto-Deployment Complete!\n\n` +
|
const commentBody = `## 🚀 V2 Auto-Deployment Complete!\n\n` +
|
||||||
`Your V2 PR with the new frontend/backend split architecture has been deployed!\n\n` +
|
`Your V2 PR with the new frontend/backend split architecture has been deployed!\n\n` +
|
||||||
`🔗 **V2 Test URL:** [${deploymentUrl}](${deploymentUrl})\n\n` +
|
`🔗 **Direct Test URL (non-SSL)** [${deploymentUrl}](${deploymentUrl})\n\n` +
|
||||||
|
`🔐 **Secure HTTPS URL**: [${httpsUrl}](${httpsUrl})\n\n` +
|
||||||
`_This deployment will be automatically cleaned up when the PR is closed._\n\n` +
|
`_This deployment will be automatically cleaned up when the PR is closed._\n\n` +
|
||||||
`🔄 **Auto-deployed** because PR title or branch name contains V2/version2/React keywords.`;
|
`🔄 **Auto-deployed** because PR title or branch name contains V2/version2/React keywords.`;
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ jobs:
|
|||||||
github.event.comment.user.login == 'reecebrowne' ||
|
github.event.comment.user.login == 'reecebrowne' ||
|
||||||
github.event.comment.user.login == 'DarioGii' ||
|
github.event.comment.user.login == 'DarioGii' ||
|
||||||
github.event.comment.user.login == 'EthanHealy01' ||
|
github.event.comment.user.login == 'EthanHealy01' ||
|
||||||
|
github.event.comment.user.login == 'jbrunton96' ||
|
||||||
github.event.comment.user.login == 'ConnorYoh'
|
github.event.comment.user.login == 'ConnorYoh'
|
||||||
)
|
)
|
||||||
outputs:
|
outputs:
|
||||||
|
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -130,7 +130,7 @@ jobs:
|
|||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
run: cd frontend && npm run build
|
run: cd frontend && npm run build
|
||||||
- name: Run frontend tests
|
- name: Run frontend tests
|
||||||
run: cd frontend && npm test --passWithNoTests --watchAll=false || true
|
run: cd frontend && npm run test -- --run
|
||||||
- name: Upload frontend build artifacts
|
- name: Upload frontend build artifacts
|
||||||
uses: actions/upload-artifact@v4.6.2
|
uses: actions/upload-artifact@v4.6.2
|
||||||
with:
|
with:
|
||||||
|
188
.github/workflows/deploy-on-v2-commit.yml
vendored
Normal file
188
.github/workflows/deploy-on-v2-commit.yml
vendored
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
name: Auto V2 Deploy on Push
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- V2
|
||||||
|
- deploy-on-v2-commit
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-v2-on-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: deploy-v2-push-V2
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Get commit hashes for frontend and backend
|
||||||
|
id: commit-hashes
|
||||||
|
run: |
|
||||||
|
# Get last commit that touched the frontend folder, docker/frontend, or docker/compose
|
||||||
|
FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
|
||||||
|
if [ -z "$FRONTEND_HASH" ]; then
|
||||||
|
FRONTEND_HASH="no-frontend-changes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get last commit that touched backend code, docker/backend, or docker/compose
|
||||||
|
BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
|
||||||
|
if [ -z "$BACKEND_HASH" ]; then
|
||||||
|
BACKEND_HASH="no-backend-changes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Frontend hash: $FRONTEND_HASH"
|
||||||
|
echo "Backend hash: $BACKEND_HASH"
|
||||||
|
|
||||||
|
echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
|
||||||
|
echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Short hashes for tags
|
||||||
|
if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
|
||||||
|
echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
|
||||||
|
echo "backend_short=no-backend" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check if frontend image exists
|
||||||
|
id: check-frontend
|
||||||
|
run: |
|
||||||
|
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Frontend image already exists, skipping build"
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "Frontend image needs to be built"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check if backend image exists
|
||||||
|
id: check-backend
|
||||||
|
run: |
|
||||||
|
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Backend image already exists, skipping build"
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "Backend image needs to be built"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
|
|
||||||
|
- name: Build and push frontend image
|
||||||
|
if: steps.check-frontend.outputs.exists == 'false'
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./docker/frontend/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||||
|
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-latest
|
||||||
|
build-args: VERSION_TAG=v2-alpha
|
||||||
|
platforms: linux/amd64
|
||||||
|
|
||||||
|
- name: Build and push backend image
|
||||||
|
if: steps.check-backend.outputs.exists == 'false'
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./docker/backend/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||||
|
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-latest
|
||||||
|
build-args: VERSION_TAG=v2-alpha
|
||||||
|
platforms: linux/amd64
|
||||||
|
|
||||||
|
|
||||||
|
- name: Set up SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh/
|
||||||
|
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||||
|
chmod 600 ../private.key
|
||||||
|
|
||||||
|
|
||||||
|
- name: Deploy to VPS on port 3000
|
||||||
|
run: |
|
||||||
|
export UNIQUE_NAME=docker-compose-v2-$GITHUB_RUN_ID.yml
|
||||||
|
|
||||||
|
cat > $UNIQUE_NAME << EOF
|
||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
container_name: stirling-v2-backend
|
||||||
|
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||||
|
ports:
|
||||||
|
- "13000:8080"
|
||||||
|
volumes:
|
||||||
|
- /stirling/V2/data:/usr/share/tessdata:rw
|
||||||
|
- /stirling/V2/config:/configs:rw
|
||||||
|
- /stirling/V2/logs:/logs:rw
|
||||||
|
environment:
|
||||||
|
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||||
|
SECURITY_ENABLELOGIN: "false"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-GB
|
||||||
|
UI_APPNAME: "Stirling-PDF V2"
|
||||||
|
UI_HOMEDESCRIPTION: "V2 Frontend/Backend Split"
|
||||||
|
UI_APPNAMENAVBAR: "V2 Deployment"
|
||||||
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
|
METRICS_ENABLED: "true"
|
||||||
|
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||||
|
SWAGGER_SERVER_URL: "http://${{ secrets.VPS_HOST }}:3000"
|
||||||
|
restart: on-failure:5
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
container_name: stirling-v2-frontend
|
||||||
|
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||||
|
ports:
|
||||||
|
- "3000:80"
|
||||||
|
environment:
|
||||||
|
VITE_API_BASE_URL: "http://${{ secrets.VPS_HOST }}:13000"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
restart: on-failure:5
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Copy to remote with unique name
|
||||||
|
scp -i ../private.key -o StrictHostKeyChecking=no $UNIQUE_NAME ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/$UNIQUE_NAME
|
||||||
|
|
||||||
|
# SSH and rename/move atomically to avoid interference
|
||||||
|
ssh -i ../private.key -o StrictHostKeyChecking=no ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << ENDSSH
|
||||||
|
mkdir -p /stirling/V2/{data,config,logs}
|
||||||
|
mv /tmp/$UNIQUE_NAME /stirling/V2/docker-compose.yml
|
||||||
|
cd /stirling/V2
|
||||||
|
docker-compose down || true
|
||||||
|
docker-compose pull
|
||||||
|
docker-compose up -d
|
||||||
|
docker system prune -af --volumes
|
||||||
|
docker image prune -af --filter "until=336h" --filter "label!=keep=true"
|
||||||
|
ENDSSH
|
||||||
|
|
||||||
|
- name: Cleanup temporary files
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
rm -f ../private.key
|
||||||
|
|
12
.github/workflows/frontend-licenses-update.yml
vendored
12
.github/workflows/frontend-licenses-update.yml
vendored
@ -126,19 +126,19 @@ jobs:
|
|||||||
|
|
||||||
commentBody = `## ❌ Frontend License Check Failed
|
commentBody = `## ❌ Frontend License Check Failed
|
||||||
|
|
||||||
The frontend license check has detected compatibility warnings that require review:
|
The frontend license check has detected compatibility warnings that require review:
|
||||||
|
|
||||||
${warningDetails}
|
${warningDetails}
|
||||||
|
|
||||||
**Action Required:** Please review these licenses to ensure they are acceptable for your use case before merging.
|
**Action Required:** Please review these licenses to ensure they are acceptable for your use case before merging.
|
||||||
|
|
||||||
_This check will fail the PR until license issues are resolved._`;
|
_This check will fail the PR until license issues are resolved._`;
|
||||||
} else {
|
} else {
|
||||||
commentBody = `## ✅ Frontend License Check Passed
|
commentBody = `## ✅ Frontend License Check Passed
|
||||||
|
|
||||||
All frontend licenses have been validated and no compatibility warnings were detected.
|
All frontend licenses have been validated and no compatibility warnings were detected.
|
||||||
|
|
||||||
The frontend license report has been updated successfully.`;
|
The frontend license report has been updated successfully.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
await github.rest.issues.createComment({
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -27,7 +27,7 @@ clientWebUI/
|
|||||||
!cucumber/exampleFiles/
|
!cucumber/exampleFiles/
|
||||||
!cucumber/exampleFiles/example_html.zip
|
!cucumber/exampleFiles/example_html.zip
|
||||||
exampleYmlFiles/stirling/
|
exampleYmlFiles/stirling/
|
||||||
stirling/
|
/stirling/
|
||||||
/testing/file_snapshots
|
/testing/file_snapshots
|
||||||
SwaggerDoc.json
|
SwaggerDoc.json
|
||||||
|
|
||||||
|
@ -39,6 +39,11 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
|||||||
String queryString = request.getQueryString();
|
String queryString = request.getQueryString();
|
||||||
if (queryString != null && !queryString.isEmpty()) {
|
if (queryString != null && !queryString.isEmpty()) {
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
|
|
||||||
|
if (requestURI.contains("/api/")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, String> allowedParameters = new HashMap<>();
|
Map<String, String> allowedParameters = new HashMap<>();
|
||||||
|
|
||||||
// Keep only the allowed parameters
|
// Keep only the allowed parameters
|
||||||
|
@ -10,6 +10,7 @@ import io.swagger.v3.oas.models.info.Info;
|
|||||||
import io.swagger.v3.oas.models.info.License;
|
import io.swagger.v3.oas.models.info.License;
|
||||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||||
|
import io.swagger.v3.oas.models.servers.Server;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
@ -50,17 +51,26 @@ public class OpenApiConfig {
|
|||||||
.url("https://www.stirlingpdf.com")
|
.url("https://www.stirlingpdf.com")
|
||||||
.email("contact@stirlingpdf.com"))
|
.email("contact@stirlingpdf.com"))
|
||||||
.description(DEFAULT_DESCRIPTION);
|
.description(DEFAULT_DESCRIPTION);
|
||||||
|
|
||||||
|
OpenAPI openAPI = new OpenAPI().info(info);
|
||||||
|
|
||||||
|
// Add server configuration from environment variable
|
||||||
|
String swaggerServerUrl = System.getenv("SWAGGER_SERVER_URL");
|
||||||
|
if (swaggerServerUrl != null && !swaggerServerUrl.trim().isEmpty()) {
|
||||||
|
Server server = new Server().url(swaggerServerUrl).description("API Server");
|
||||||
|
openAPI.addServersItem(server);
|
||||||
|
}
|
||||||
|
|
||||||
if (!applicationProperties.getSecurity().getEnableLogin()) {
|
if (!applicationProperties.getSecurity().getEnableLogin()) {
|
||||||
return new OpenAPI().components(new Components()).info(info);
|
return openAPI.components(new Components());
|
||||||
} else {
|
} else {
|
||||||
SecurityScheme apiKeyScheme =
|
SecurityScheme apiKeyScheme =
|
||||||
new SecurityScheme()
|
new SecurityScheme()
|
||||||
.type(SecurityScheme.Type.APIKEY)
|
.type(SecurityScheme.Type.APIKEY)
|
||||||
.in(SecurityScheme.In.HEADER)
|
.in(SecurityScheme.In.HEADER)
|
||||||
.name("X-API-KEY");
|
.name("X-API-KEY");
|
||||||
return new OpenAPI()
|
return openAPI
|
||||||
.components(new Components().addSecuritySchemes("apiKey", apiKeyScheme))
|
.components(new Components().addSecuritySchemes("apiKey", apiKeyScheme))
|
||||||
.info(info)
|
|
||||||
.addSecurityItem(new SecurityRequirement().addList("apiKey"));
|
.addSecurityItem(new SecurityRequirement().addList("apiKey"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@ -29,7 +31,7 @@ public class AnalysisController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/page-count", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/page-count", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get PDF page count",
|
summary = "Get PDF page count",
|
||||||
description = "Returns total number of pages in PDF. Input:PDF Output:JSON Type:SISO")
|
description = "Returns total number of pages in PDF. Input:PDF Output:JSON Type:SISO")
|
||||||
@ -39,7 +41,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/basic-info", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/basic-info", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get basic PDF information",
|
summary = "Get basic PDF information",
|
||||||
description = "Returns page count, version, file size. Input:PDF Output:JSON Type:SISO")
|
description = "Returns page count, version, file size. Input:PDF Output:JSON Type:SISO")
|
||||||
@ -53,7 +55,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/document-properties", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/document-properties", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get PDF document properties",
|
summary = "Get PDF document properties",
|
||||||
description = "Returns title, author, subject, etc. Input:PDF Output:JSON Type:SISO")
|
description = "Returns title, author, subject, etc. Input:PDF Output:JSON Type:SISO")
|
||||||
@ -76,7 +78,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/page-dimensions", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/page-dimensions", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get page dimensions for all pages",
|
summary = "Get page dimensions for all pages",
|
||||||
description = "Returns width and height of each page. Input:PDF Output:JSON Type:SISO")
|
description = "Returns width and height of each page. Input:PDF Output:JSON Type:SISO")
|
||||||
@ -96,7 +98,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/form-fields", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/form-fields", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get form field information",
|
summary = "Get form field information",
|
||||||
description =
|
description =
|
||||||
@ -119,7 +121,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/annotation-info", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/annotation-info", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get annotation information",
|
summary = "Get annotation information",
|
||||||
description = "Returns count and types of annotations. Input:PDF Output:JSON Type:SISO")
|
description = "Returns count and types of annotations. Input:PDF Output:JSON Type:SISO")
|
||||||
@ -143,7 +145,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/font-info", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/font-info", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get font information",
|
summary = "Get font information",
|
||||||
description =
|
description =
|
||||||
@ -165,7 +167,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/security-info", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/security-info", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get security information",
|
summary = "Get security information",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
@ -33,7 +35,7 @@ public class CropController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/crop", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/crop", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Crops a PDF document",
|
summary = "Crops a PDF document",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -44,7 +46,7 @@ public class EditTableOfContentsController {
|
|||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
@PostMapping(value = "/extract-bookmarks", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/extract-bookmarks", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Extract PDF Bookmarks",
|
summary = "Extract PDF Bookmarks",
|
||||||
description = "Extracts bookmarks/table of contents from a PDF document as JSON.")
|
description = "Extracts bookmarks/table of contents from a PDF document as JSON.")
|
||||||
@ -152,7 +154,7 @@ public class EditTableOfContentsController {
|
|||||||
return bookmark;
|
return bookmark;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/edit-table-of-contents", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/edit-table-of-contents", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Edit Table of Contents",
|
summary = "Edit Table of Contents",
|
||||||
description = "Add or edit bookmarks/table of contents in a PDF document.")
|
description = "Add or edit bookmarks/table of contents in a PDF document.")
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -154,7 +156,7 @@ public class MergeController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Merge multiple PDF files into one",
|
summary = "Merge multiple PDF files into one",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -36,7 +38,7 @@ public class MultiPageLayoutController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Merge multiple pages of a PDF document into a single page",
|
summary = "Merge multiple pages of a PDF document into a single page",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
@ -46,7 +48,7 @@ public class PdfImageRemovalController {
|
|||||||
* content type and filename.
|
* content type and filename.
|
||||||
* @throws IOException If an error occurs while processing the PDF file.
|
* @throws IOException If an error occurs while processing the PDF file.
|
||||||
*/
|
*/
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-image-pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-image-pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Remove images from file to reduce the file size.",
|
summary = "Remove images from file to reduce the file size.",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -39,7 +41,7 @@ public class PdfOverlayController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Overlay PDF files in various modes",
|
summary = "Overlay PDF files in various modes",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -38,7 +40,7 @@ public class RearrangePagesPDFController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-pages")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-pages")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Remove pages from a PDF file",
|
summary = "Remove pages from a PDF file",
|
||||||
description =
|
description =
|
||||||
@ -237,7 +239,7 @@ public class RearrangePagesPDFController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/rearrange-pages")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Rearrange pages in a PDF file",
|
summary = "Rearrange pages in a PDF file",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
@ -31,7 +33,7 @@ public class RotationController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/rotate-pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Rotate a PDF file",
|
summary = "Rotate a PDF file",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -38,7 +40,7 @@ public class ScalePagesController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/scale-pages", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/scale-pages", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Change the size of a PDF page/document",
|
summary = "Change the size of a PDF page/document",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -31,7 +33,7 @@ public class SettingsController {
|
|||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private final EndpointConfiguration endpointConfiguration;
|
private final EndpointConfiguration endpointConfiguration;
|
||||||
|
|
||||||
@PostMapping("/update-enable-analytics")
|
@AutoJobPostMapping("/update-enable-analytics")
|
||||||
@Hidden
|
@Hidden
|
||||||
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
|
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
|
||||||
if (applicationProperties.getSystem().getEnableAnalytics() != null) {
|
if (applicationProperties.getSystem().getEnableAnalytics() != null) {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -41,7 +43,7 @@ public class SplitPDFController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/split-pages")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/split-pages")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Split a PDF file into separate documents",
|
summary = "Split a PDF file into separate documents",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@ -117,7 +119,7 @@ public class SplitPdfByChaptersController {
|
|||||||
return bookmarks;
|
return bookmarks;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/split-pdf-by-chapters", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/split-pdf-by-chapters", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Split PDFs by Chapters",
|
summary = "Split PDFs by Chapters",
|
||||||
description = "Splits a PDF into chapters and returns a ZIP file.")
|
description = "Splits a PDF into chapters and returns a ZIP file.")
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -43,7 +45,7 @@ public class SplitPdfBySectionsController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Split PDF pages into smaller sections",
|
summary = "Split PDF pages into smaller sections",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -39,7 +41,7 @@ public class SplitPdfBySizeController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Auto split PDF pages into separate documents based on size or count",
|
summary = "Auto split PDF pages into separate documents based on size or count",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.awt.geom.AffineTransform;
|
import java.awt.geom.AffineTransform;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -33,7 +35,7 @@ public class ToSinglePageController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a multi-page PDF into a single long page PDF",
|
summary = "Convert a multi-page PDF into a single long page PDF",
|
||||||
description =
|
description =
|
||||||
|
@ -0,0 +1,301 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
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.ClassPathResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.ResourceLoader;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.Dependency;
|
||||||
|
import stirling.software.SPDF.model.SignatureFile;
|
||||||
|
import stirling.software.SPDF.service.SignatureService;
|
||||||
|
import stirling.software.common.configuration.InstallationPathConfig;
|
||||||
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
import stirling.software.common.service.UserServiceInterface;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
|
import stirling.software.common.util.GeneralUtils;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/ui-data")
|
||||||
|
@Tag(name = "UI Data", description = "APIs for React UI data")
|
||||||
|
public class UIDataController {
|
||||||
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
private final SignatureService signatureService;
|
||||||
|
private final UserServiceInterface userService;
|
||||||
|
private final ResourceLoader resourceLoader;
|
||||||
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
|
|
||||||
|
public UIDataController(
|
||||||
|
ApplicationProperties applicationProperties,
|
||||||
|
SignatureService signatureService,
|
||||||
|
@Autowired(required = false) UserServiceInterface userService,
|
||||||
|
ResourceLoader resourceLoader,
|
||||||
|
RuntimePathConfig runtimePathConfig) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.signatureService = signatureService;
|
||||||
|
this.userService = userService;
|
||||||
|
this.resourceLoader = resourceLoader;
|
||||||
|
this.runtimePathConfig = runtimePathConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/home")
|
||||||
|
@Operation(summary = "Get home page data")
|
||||||
|
public ResponseEntity<HomeData> getHomeData() {
|
||||||
|
String showSurvey = System.getenv("SHOW_SURVEY");
|
||||||
|
boolean showSurveyValue = showSurvey == null || "true".equalsIgnoreCase(showSurvey);
|
||||||
|
|
||||||
|
HomeData data = new HomeData();
|
||||||
|
data.setShowSurveyFromDocker(showSurveyValue);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/licenses")
|
||||||
|
@Operation(summary = "Get third-party licenses data")
|
||||||
|
public ResponseEntity<LicensesData> getLicensesData() {
|
||||||
|
LicensesData data = new LicensesData();
|
||||||
|
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>> licenseData =
|
||||||
|
mapper.readValue(json, new TypeReference<>() {});
|
||||||
|
data.setDependencies(licenseData.get("dependencies"));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to load licenses data", e);
|
||||||
|
data.setDependencies(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/pipeline")
|
||||||
|
@Operation(summary = "Get pipeline configuration data")
|
||||||
|
public ResponseEntity<PipelineData> getPipelineData() {
|
||||||
|
PipelineData data = new PipelineData();
|
||||||
|
List<String> pipelineConfigs = new ArrayList<>();
|
||||||
|
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
|
||||||
|
|
||||||
|
if (new java.io.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.length() < 1) {
|
||||||
|
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("Failed to load pipeline configs", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pipelineConfigsWithNames.isEmpty()) {
|
||||||
|
Map<String, String> configWithName = new HashMap<>();
|
||||||
|
configWithName.put("json", "");
|
||||||
|
configWithName.put("name", "No preloaded configs found");
|
||||||
|
pipelineConfigsWithNames.add(configWithName);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.setPipelineConfigsWithNames(pipelineConfigsWithNames);
|
||||||
|
data.setPipelineConfigs(pipelineConfigs);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sign")
|
||||||
|
@Operation(summary = "Get signature form data")
|
||||||
|
public ResponseEntity<SignData> getSignData() {
|
||||||
|
String username = "";
|
||||||
|
if (userService != null) {
|
||||||
|
username = userService.getCurrentUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
|
||||||
|
List<FontResource> fonts = getFontNames();
|
||||||
|
|
||||||
|
SignData data = new SignData();
|
||||||
|
data.setSignatures(signatures);
|
||||||
|
data.setFonts(fonts);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/ocr-pdf")
|
||||||
|
@Operation(summary = "Get OCR PDF data")
|
||||||
|
public ResponseEntity<OcrData> getOcrPdfData() {
|
||||||
|
List<String> languages = getAvailableTesseractLanguages();
|
||||||
|
|
||||||
|
OcrData data = new OcrData();
|
||||||
|
data.setLanguages(languages);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getAvailableTesseractLanguages() {
|
||||||
|
String tessdataDir = applicationProperties.getSystem().getTessdataDir();
|
||||||
|
java.io.File[] files = new java.io.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FontResource> getFontNames() {
|
||||||
|
List<FontResource> fontNames = new ArrayList<>();
|
||||||
|
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
|
||||||
|
fontNames.addAll(
|
||||||
|
getFontNamesFromLocation(
|
||||||
|
"file:"
|
||||||
|
+ InstallationPathConfig.getStaticPath()
|
||||||
|
+ "fonts"
|
||||||
|
+ java.io.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data classes
|
||||||
|
@Data
|
||||||
|
public static class HomeData {
|
||||||
|
private boolean showSurveyFromDocker;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class LicensesData {
|
||||||
|
private List<Dependency> dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class PipelineData {
|
||||||
|
private List<Map<String, String>> pipelineConfigsWithNames;
|
||||||
|
private List<String> pipelineConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class SignData {
|
||||||
|
private List<SignatureFile> signatures;
|
||||||
|
private List<FontResource> fonts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class OcrData {
|
||||||
|
private List<String> languages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getFormatFromExtension(String extension) {
|
||||||
|
switch (extension) {
|
||||||
|
case "ttf":
|
||||||
|
return "truetype";
|
||||||
|
case "woff":
|
||||||
|
return "woff";
|
||||||
|
case "woff2":
|
||||||
|
return "woff2";
|
||||||
|
case "eot":
|
||||||
|
return "embedded-opentype";
|
||||||
|
case "svg":
|
||||||
|
return "svg";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
@ -38,7 +40,7 @@ public class ConvertEmlToPDF {
|
|||||||
private final RuntimePathConfig runtimePathConfig;
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
private final TempFileManager tempFileManager;
|
private final TempFileManager tempFileManager;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/eml/pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/eml/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert EML to PDF",
|
summary = "Convert EML to PDF",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@ -36,7 +38,7 @@ public class ConvertHtmlToPDF {
|
|||||||
|
|
||||||
private final TempFileManager tempFileManager;
|
private final TempFileManager tempFileManager;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
@ -51,7 +53,7 @@ public class ConvertImgPDFController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/img")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/img")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to image(s)",
|
summary = "Convert PDF to image(s)",
|
||||||
description =
|
description =
|
||||||
@ -211,7 +213,7 @@ public class ConvertImgPDFController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/img/pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/img/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert images to a PDF file",
|
summary = "Convert images to a PDF file",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -45,7 +47,7 @@ public class ConvertMarkdownToPdf {
|
|||||||
|
|
||||||
private final TempFileManager tempFileManager;
|
private final TempFileManager tempFileManager;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a Markdown file to PDF",
|
summary = "Convert a Markdown file to PDF",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -84,7 +86,7 @@ public class ConvertOfficeController {
|
|||||||
return fileExtension.matches(extensionPattern);
|
return fileExtension.matches(extensionPattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/file/pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/file/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a file to a PDF using LibreOffice",
|
summary = "Convert a file to a PDF using LibreOffice",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@ -18,7 +20,7 @@ import stirling.software.common.util.PDFToFile;
|
|||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
public class ConvertPDFToHtml {
|
public class ConvertPDFToHtml {
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/html")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/html")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to HTML",
|
summary = "Convert PDF to HTML",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
@ -34,7 +36,7 @@ public class ConvertPDFToOffice {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/presentation")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to Presentation format",
|
summary = "Convert PDF to Presentation format",
|
||||||
description =
|
description =
|
||||||
@ -49,7 +51,7 @@ public class ConvertPDFToOffice {
|
|||||||
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import");
|
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/text")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/text")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to Text or RTF format",
|
summary = "Convert PDF to Text or RTF format",
|
||||||
description =
|
description =
|
||||||
@ -77,7 +79,7 @@ public class ConvertPDFToOffice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/word")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/word")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to Word document",
|
summary = "Convert PDF to Word document",
|
||||||
description =
|
description =
|
||||||
@ -91,7 +93,7 @@ public class ConvertPDFToOffice {
|
|||||||
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
|
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/xml")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/xml")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to XML",
|
summary = "Convert PDF to XML",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -78,7 +80,7 @@ import stirling.software.common.util.WebResponseUtils;
|
|||||||
@Tag(name = "Convert", description = "Convert APIs")
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
public class ConvertPDFToPDFA {
|
public class ConvertPDFToPDFA {
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a PDF to a PDF/A",
|
summary = "Convert a PDF to a PDF/A",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@ -40,7 +42,7 @@ public class ConvertWebsiteToPDF {
|
|||||||
private final RuntimePathConfig runtimePathConfig;
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/url/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a URL to a PDF",
|
summary = "Convert a URL to a PDF",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
@ -46,7 +48,7 @@ public class ExtractCSVController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Extracts a CSV document from a PDF",
|
summary = "Extracts a CSV document from a PDF",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.filters;
|
package stirling.software.SPDF.controller.api.filters;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
@ -37,7 +39,7 @@ public class FilterController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF contains set text, returns true if does",
|
summary = "Checks if a PDF contains set text, returns true if does",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
@ -55,7 +57,7 @@ public class FilterController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-contains-image")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF contains an image",
|
summary = "Checks if a PDF contains an image",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
@ -71,7 +73,7 @@ public class FilterController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-count")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-page-count")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF is greater, less or equal to a setPageCount",
|
summary = "Checks if a PDF is greater, less or equal to a setPageCount",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
@ -104,7 +106,7 @@ public class FilterController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-size")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-page-size")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF is of a certain size",
|
summary = "Checks if a PDF is of a certain size",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
@ -147,7 +149,7 @@ public class FilterController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-file-size")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-file-size")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF is a set file size",
|
summary = "Checks if a PDF is a set file size",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
@ -180,7 +182,7 @@ public class FilterController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF is of a certain rotation",
|
summary = "Checks if a PDF is of a certain rotation",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -34,7 +36,7 @@ public class AttachmentController {
|
|||||||
|
|
||||||
private final AttachmentServiceInterface pdfAttachmentService;
|
private final AttachmentServiceInterface pdfAttachmentService;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/add-attachments")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-attachments")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Add attachments to PDF",
|
summary = "Add attachments to PDF",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@ -38,7 +40,7 @@ public class AutoRenameController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/auto-rename")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Extract header from PDF file",
|
summary = "Extract header from PDF file",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.DataBufferByte;
|
import java.awt.image.DataBufferByte;
|
||||||
import java.awt.image.DataBufferInt;
|
import java.awt.image.DataBufferInt;
|
||||||
@ -102,7 +104,7 @@ public class AutoSplitPdfController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Auto split PDF pages into separate documents",
|
summary = "Auto split PDF pages into separate documents",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -69,7 +71,7 @@ public class BlankPageController {
|
|||||||
return whitePixelPercentage >= whitePercent;
|
return whitePixelPercentage >= whitePercent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Remove blank pages from a PDF file",
|
summary = "Remove blank pages from a PDF file",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
@ -658,7 +660,7 @@ public class CompressController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Optimize PDF file",
|
summary = "Optimize PDF file",
|
||||||
description =
|
description =
|
||||||
|
@ -110,14 +110,14 @@ public class ConfigController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/endpoint-enabled")
|
@GetMapping("/endpoint-enabled")
|
||||||
public ResponseEntity<Boolean> isEndpointEnabled(@RequestParam String endpoint) {
|
public ResponseEntity<Boolean> isEndpointEnabled(@RequestParam(name = "endpoint") String endpoint) {
|
||||||
boolean enabled = endpointConfiguration.isEndpointEnabled(endpoint);
|
boolean enabled = endpointConfiguration.isEndpointEnabled(endpoint);
|
||||||
return ResponseEntity.ok(enabled);
|
return ResponseEntity.ok(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/endpoints-enabled")
|
@GetMapping("/endpoints-enabled")
|
||||||
public ResponseEntity<Map<String, Boolean>> areEndpointsEnabled(
|
public ResponseEntity<Map<String, Boolean>> areEndpointsEnabled(
|
||||||
@RequestParam String endpoints) {
|
@RequestParam(name = "endpoints") String endpoints) {
|
||||||
Map<String, Boolean> result = new HashMap<>();
|
Map<String, Boolean> result = new HashMap<>();
|
||||||
String[] endpointArray = endpoints.split(",");
|
String[] endpointArray = endpoints.split(",");
|
||||||
for (String endpoint : endpointArray) {
|
for (String endpoint : endpointArray) {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
@ -38,7 +40,7 @@ public class DecompressPdfController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/decompress-pdf", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/decompress-pdf", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Decompress PDF streams",
|
summary = "Decompress PDF streams",
|
||||||
description = "Fully decompresses all PDF streams including text content")
|
description = "Fully decompresses all PDF streams including text content")
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -50,7 +52,7 @@ public class ExtractImageScansController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Extract image scans from an input file",
|
summary = "Extract image scans from an input file",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.RenderedImage;
|
import java.awt.image.RenderedImage;
|
||||||
@ -54,7 +56,7 @@ public class ExtractImagesController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/extract-images")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/extract-images")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Extract images from a PDF file",
|
summary = "Extract images from a PDF file",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
@ -38,7 +40,7 @@ public class FlattenController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/flatten")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/flatten")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Flatten PDF form fields or full page",
|
summary = "Flatten PDF form fields or full page",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
@ -51,7 +53,7 @@ public class MetadataController {
|
|||||||
binder.registerCustomEditor(Map.class, "allRequestParams", new StringToMapPropertyEditor());
|
binder.registerCustomEditor(Map.class, "allRequestParams", new StringToMapPropertyEditor());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/update-metadata")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Update metadata of a PDF file",
|
summary = "Update metadata of a PDF file",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -76,7 +78,7 @@ public class OCRController {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Process a PDF file with OCR",
|
summary = "Process a PDF file with OCR",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@ -31,7 +33,7 @@ public class OverlayImageController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/add-image")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-image")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Overlay image onto a PDF file",
|
summary = "Overlay image onto a PDF file",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -37,7 +39,7 @@ public class PageNumbersController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/add-page-numbers", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Add page numbers to a PDF document",
|
summary = "Add page numbers to a PDF document",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.print.PageFormat;
|
import java.awt.print.PageFormat;
|
||||||
@ -37,7 +39,7 @@ import stirling.software.SPDF.model.api.misc.PrintFileRequest;
|
|||||||
public class PrintFileController {
|
public class PrintFileController {
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
// @PostMapping(value = "/print-file", consumes = "multipart/form-data")
|
// @AutoJobPostMapping(value = "/print-file", consumes = "multipart/form-data")
|
||||||
// @Operation(
|
// @Operation(
|
||||||
// summary = "Prints PDF/Image file to a set printer",
|
// summary = "Prints PDF/Image file to a set printer",
|
||||||
// description =
|
// description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -46,7 +48,7 @@ public class RepairController {
|
|||||||
return endpointConfiguration.isGroupEnabled("qpdf");
|
return endpointConfiguration.isGroupEnabled("qpdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/repair")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/repair")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Repair a PDF file",
|
summary = "Repair a PDF file",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.InputStreamResource;
|
||||||
@ -27,7 +29,7 @@ public class ReplaceAndInvertColorController {
|
|||||||
|
|
||||||
private final ReplaceAndInvertColorService replaceAndInvertColorService;
|
private final ReplaceAndInvertColorService replaceAndInvertColorService;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/replace-invert-pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/replace-invert-pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Replace-Invert Color PDF",
|
summary = "Replace-Invert Color PDF",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.awt.Graphics2D;
|
import java.awt.Graphics2D;
|
||||||
import java.awt.RenderingHints;
|
import java.awt.RenderingHints;
|
||||||
@ -52,7 +54,7 @@ public class ScannerEffectController {
|
|||||||
private static final int MAX_IMAGE_HEIGHT = 8192;
|
private static final int MAX_IMAGE_HEIGHT = 8192;
|
||||||
private static final long MAX_IMAGE_PIXELS = 16_777_216; // 4096x4096
|
private static final long MAX_IMAGE_PIXELS = 16_777_216; // 4096x4096
|
||||||
|
|
||||||
@PostMapping(value = "/scanner-effect", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/scanner-effect", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Apply scanner effect to PDF",
|
summary = "Apply scanner effect to PDF",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -32,7 +34,7 @@ public class ShowJavascript {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/show-javascript")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Grabs all JS from a PDF and returns a single JS file with all code",
|
summary = "Grabs all JS from a PDF and returns a single JS file with all code",
|
||||||
description = "desc. Input:PDF Output:JS Type:SISO")
|
description = "desc. Input:PDF Output:JS Type:SISO")
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -52,7 +54,7 @@ public class StampController {
|
|||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
private final TempFileManager tempFileManager;
|
private final TempFileManager tempFileManager;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/add-stamp")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-stamp")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Add stamp to a PDF file",
|
summary = "Add stamp to a PDF file",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@ -37,7 +39,7 @@ public class UnlockPDFFormsController {
|
|||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/unlock-pdf-forms")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/unlock-pdf-forms")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Remove read-only property from form fields",
|
summary = "Remove read-only property from form fields",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.pipeline;
|
package stirling.software.SPDF.controller.api.pipeline;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -46,7 +48,7 @@ public class PipelineController {
|
|||||||
|
|
||||||
private final PostHogService postHogService;
|
private final PostHogService postHogService;
|
||||||
|
|
||||||
@PostMapping(value = "/handleData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
@AutoJobPostMapping(value = "/handleData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
|
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
|
||||||
throws JsonMappingException, JsonProcessingException {
|
throws JsonMappingException, JsonProcessingException {
|
||||||
MultipartFile[] files = request.getFileInput();
|
MultipartFile[] files = request.getFileInput();
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.security;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.beans.PropertyEditorSupport;
|
import java.beans.PropertyEditorSupport;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
@ -138,7 +140,7 @@ public class CertSignController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(
|
@AutoJobPostMapping(
|
||||||
consumes = {
|
consumes = {
|
||||||
MediaType.MULTIPART_FORM_DATA_VALUE,
|
MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||||
MediaType.APPLICATION_FORM_URLENCODED_VALUE
|
MediaType.APPLICATION_FORM_URLENCODED_VALUE
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.security;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@ -188,7 +190,7 @@ public class GetInfoOnPDF {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
|
||||||
@Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO")
|
@Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO")
|
||||||
public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) throws IOException {
|
public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) throws IOException {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.security;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
@ -32,7 +34,7 @@ public class PasswordController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-password")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-password")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Remove password from a PDF file",
|
summary = "Remove password from a PDF file",
|
||||||
description =
|
description =
|
||||||
@ -58,7 +60,7 @@ public class PasswordController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/add-password")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-password")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Add password to a PDF file",
|
summary = "Add password to a PDF file",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.security;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -56,7 +58,7 @@ public class RedactController {
|
|||||||
List.class, "redactions", new StringToArrayListPropertyEditor());
|
List.class, "redactions", new StringToArrayListPropertyEditor());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/redact", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/redact", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Redacts areas and pages in a PDF document",
|
summary = "Redacts areas and pages in a PDF document",
|
||||||
description =
|
description =
|
||||||
@ -190,7 +192,7 @@ public class RedactController {
|
|||||||
return pageNumbers;
|
return pageNumbers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/auto-redact", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/auto-redact", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Redacts listOfText in a PDF document",
|
summary = "Redacts listOfText in a PDF document",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.security;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
@ -32,7 +34,7 @@ public class RemoveCertSignController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-cert-sign")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-cert-sign")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Remove digital signature from PDF",
|
summary = "Remove digital signature from PDF",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.security;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.apache.pdfbox.cos.COSDictionary;
|
import org.apache.pdfbox.cos.COSDictionary;
|
||||||
@ -46,7 +48,7 @@ public class SanitizeController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Sanitize a PDF file",
|
summary = "Sanitize a PDF file",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.security;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.beans.PropertyEditorSupport;
|
import java.beans.PropertyEditorSupport;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -69,7 +71,7 @@ public class ValidateSignatureController {
|
|||||||
description =
|
description =
|
||||||
"Validates the digital signatures in a PDF file against default or custom"
|
"Validates the digital signatures in a PDF file against default or custom"
|
||||||
+ " certificates. Input:PDF Output:JSON Type:SISO")
|
+ " certificates. Input:PDF Output:JSON Type:SISO")
|
||||||
@PostMapping(value = "/validate-signature", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
@AutoJobPostMapping(value = "/validate-signature", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
public ResponseEntity<List<SignatureValidationResult>> validateSignature(
|
public ResponseEntity<List<SignatureValidationResult>> validateSignature(
|
||||||
@ModelAttribute SignatureValidationRequest request) throws IOException {
|
@ModelAttribute SignatureValidationRequest request) throws IOException {
|
||||||
List<SignatureValidationResult> results = new ArrayList<>();
|
List<SignatureValidationResult> results = new ArrayList<>();
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.security;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.beans.PropertyEditorSupport;
|
import java.beans.PropertyEditorSupport;
|
||||||
@ -64,7 +66,7 @@ public class WatermarkController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/add-watermark")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-watermark")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Add watermark to a PDF file",
|
summary = "Add watermark to a PDF file",
|
||||||
description =
|
description =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.model.api.converters;
|
package stirling.software.SPDF.model.api.converters;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@ -18,7 +20,7 @@ import stirling.software.common.util.PDFToFile;
|
|||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
public class ConvertPDFToMarkdown {
|
public class ConvertPDFToMarkdown {
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/markdown")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/markdown")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to Markdown",
|
summary = "Convert PDF to Markdown",
|
||||||
description =
|
description =
|
||||||
|
@ -0,0 +1,484 @@
|
|||||||
|
package stirling.software.proprietary.controller.api;
|
||||||
|
|
||||||
|
import static stirling.software.common.util.ProviderUtils.validateProvider;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
import stirling.software.common.model.ApplicationProperties.Security;
|
||||||
|
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2;
|
||||||
|
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||||
|
import stirling.software.common.model.ApplicationProperties.Security.SAML2;
|
||||||
|
import stirling.software.common.model.FileInfo;
|
||||||
|
import stirling.software.common.model.enumeration.Role;
|
||||||
|
import stirling.software.common.model.oauth2.GitHubProvider;
|
||||||
|
import stirling.software.common.model.oauth2.GoogleProvider;
|
||||||
|
import stirling.software.common.model.oauth2.KeycloakProvider;
|
||||||
|
import stirling.software.proprietary.audit.AuditEventType;
|
||||||
|
import stirling.software.proprietary.audit.AuditLevel;
|
||||||
|
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||||
|
import stirling.software.proprietary.model.Team;
|
||||||
|
import stirling.software.proprietary.model.dto.TeamWithUserCountDTO;
|
||||||
|
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||||
|
import stirling.software.proprietary.security.database.repository.SessionRepository;
|
||||||
|
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||||
|
import stirling.software.proprietary.security.model.Authority;
|
||||||
|
import stirling.software.proprietary.security.model.SessionEntity;
|
||||||
|
import stirling.software.proprietary.security.model.User;
|
||||||
|
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||||
|
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||||
|
import stirling.software.proprietary.security.service.DatabaseService;
|
||||||
|
import stirling.software.proprietary.security.service.TeamService;
|
||||||
|
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/proprietary/ui-data")
|
||||||
|
@Tag(name = "Proprietary UI Data", description = "APIs for React UI data (Proprietary features)")
|
||||||
|
@EnterpriseEndpoint
|
||||||
|
public class ProprietaryUIDataController {
|
||||||
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
private final AuditConfigurationProperties auditConfig;
|
||||||
|
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final TeamRepository teamRepository;
|
||||||
|
private final SessionRepository sessionRepository;
|
||||||
|
private final DatabaseService databaseService;
|
||||||
|
private final boolean runningEE;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public ProprietaryUIDataController(
|
||||||
|
ApplicationProperties applicationProperties,
|
||||||
|
AuditConfigurationProperties auditConfig,
|
||||||
|
SessionPersistentRegistry sessionPersistentRegistry,
|
||||||
|
UserRepository userRepository,
|
||||||
|
TeamRepository teamRepository,
|
||||||
|
SessionRepository sessionRepository,
|
||||||
|
DatabaseService databaseService,
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
@Qualifier("runningEE") boolean runningEE) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.auditConfig = auditConfig;
|
||||||
|
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.teamRepository = teamRepository;
|
||||||
|
this.sessionRepository = sessionRepository;
|
||||||
|
this.databaseService = databaseService;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.runningEE = runningEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/audit-dashboard")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
@Operation(summary = "Get audit dashboard data")
|
||||||
|
public ResponseEntity<AuditDashboardData> getAuditDashboardData() {
|
||||||
|
AuditDashboardData data = new AuditDashboardData();
|
||||||
|
data.setAuditEnabled(auditConfig.isEnabled());
|
||||||
|
data.setAuditLevel(auditConfig.getAuditLevel());
|
||||||
|
data.setAuditLevelInt(auditConfig.getLevel());
|
||||||
|
data.setRetentionDays(auditConfig.getRetentionDays());
|
||||||
|
data.setAuditLevels(AuditLevel.values());
|
||||||
|
data.setAuditEventTypes(AuditEventType.values());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/login")
|
||||||
|
@Operation(summary = "Get login page data")
|
||||||
|
public ResponseEntity<LoginData> getLoginData() {
|
||||||
|
LoginData data = new LoginData();
|
||||||
|
Map<String, String> providerList = new HashMap<>();
|
||||||
|
Security securityProps = applicationProperties.getSecurity();
|
||||||
|
OAUTH2 oauth = securityProps.getOauth2();
|
||||||
|
|
||||||
|
if (oauth != null && oauth.getEnabled()) {
|
||||||
|
if (oauth.isSettingsValid()) {
|
||||||
|
String firstChar = String.valueOf(oauth.getProvider().charAt(0));
|
||||||
|
String clientName =
|
||||||
|
oauth.getProvider().replaceFirst(firstChar, firstChar.toUpperCase());
|
||||||
|
providerList.put("/oauth2/authorization/" + oauth.getProvider(), clientName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Client client = oauth.getClient();
|
||||||
|
if (client != null) {
|
||||||
|
GoogleProvider google = client.getGoogle();
|
||||||
|
if (validateProvider(google)) {
|
||||||
|
providerList.put(
|
||||||
|
"/oauth2/authorization/" + google.getName(), google.getClientName());
|
||||||
|
}
|
||||||
|
|
||||||
|
GitHubProvider github = client.getGithub();
|
||||||
|
if (validateProvider(github)) {
|
||||||
|
providerList.put(
|
||||||
|
"/oauth2/authorization/" + github.getName(), github.getClientName());
|
||||||
|
}
|
||||||
|
|
||||||
|
KeycloakProvider keycloak = client.getKeycloak();
|
||||||
|
if (validateProvider(keycloak)) {
|
||||||
|
providerList.put(
|
||||||
|
"/oauth2/authorization/" + keycloak.getName(),
|
||||||
|
keycloak.getClientName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SAML2 saml2 = securityProps.getSaml2();
|
||||||
|
if (securityProps.isSaml2Active()
|
||||||
|
&& applicationProperties.getSystem().getEnableAlphaFunctionality()
|
||||||
|
&& applicationProperties.getPremium().isEnabled()) {
|
||||||
|
String samlIdp = saml2.getProvider();
|
||||||
|
String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId();
|
||||||
|
|
||||||
|
if (!applicationProperties.getPremium().getProFeatures().isSsoAutoLogin()) {
|
||||||
|
providerList.put(saml2AuthenticationPath, samlIdp + " (SAML 2)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove null entries
|
||||||
|
providerList
|
||||||
|
.entrySet()
|
||||||
|
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
|
||||||
|
|
||||||
|
data.setProviderList(providerList);
|
||||||
|
data.setLoginMethod(securityProps.getLoginMethod());
|
||||||
|
data.setAltLogin(!providerList.isEmpty() && securityProps.isAltLogin());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/admin-settings")
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@Operation(summary = "Get admin settings data")
|
||||||
|
public ResponseEntity<AdminSettingsData> getAdminSettingsData(Authentication authentication) {
|
||||||
|
List<User> allUsers = userRepository.findAllWithTeam();
|
||||||
|
Iterator<User> iterator = allUsers.iterator();
|
||||||
|
Map<String, String> roleDetails = Role.getAllRoleDetails();
|
||||||
|
|
||||||
|
Map<String, Boolean> userSessions = new HashMap<>();
|
||||||
|
Map<String, Date> userLastRequest = new HashMap<>();
|
||||||
|
int activeUsers = 0;
|
||||||
|
int disabledUsers = 0;
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
User user = iterator.next();
|
||||||
|
if (user != null) {
|
||||||
|
boolean shouldRemove = false;
|
||||||
|
|
||||||
|
// Check if user is an INTERNAL_API_USER
|
||||||
|
for (Authority authority : user.getAuthorities()) {
|
||||||
|
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
||||||
|
shouldRemove = true;
|
||||||
|
roleDetails.remove(Role.INTERNAL_API_USER.getRoleId());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is part of the Internal team
|
||||||
|
if (user.getTeam() != null
|
||||||
|
&& user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||||
|
shouldRemove = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRemove) {
|
||||||
|
iterator.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session status and last request time
|
||||||
|
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
|
||||||
|
boolean hasActiveSession = false;
|
||||||
|
Date lastRequest = null;
|
||||||
|
Optional<SessionEntity> latestSession =
|
||||||
|
sessionPersistentRegistry.findLatestSession(user.getUsername());
|
||||||
|
|
||||||
|
if (latestSession.isPresent()) {
|
||||||
|
SessionEntity sessionEntity = latestSession.get();
|
||||||
|
Date lastAccessedTime = sessionEntity.getLastRequest();
|
||||||
|
Instant now = Instant.now();
|
||||||
|
Instant expirationTime =
|
||||||
|
lastAccessedTime
|
||||||
|
.toInstant()
|
||||||
|
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||||
|
|
||||||
|
if (now.isAfter(expirationTime)) {
|
||||||
|
sessionPersistentRegistry.expireSession(sessionEntity.getSessionId());
|
||||||
|
} else {
|
||||||
|
hasActiveSession = !sessionEntity.isExpired();
|
||||||
|
}
|
||||||
|
lastRequest = sessionEntity.getLastRequest();
|
||||||
|
} else {
|
||||||
|
lastRequest = new Date(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
userSessions.put(user.getUsername(), hasActiveSession);
|
||||||
|
userLastRequest.put(user.getUsername(), lastRequest);
|
||||||
|
|
||||||
|
if (hasActiveSession) activeUsers++;
|
||||||
|
if (!user.isEnabled()) disabledUsers++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort users by active status and last request date
|
||||||
|
List<User> sortedUsers =
|
||||||
|
allUsers.stream()
|
||||||
|
.sorted(
|
||||||
|
(u1, u2) -> {
|
||||||
|
boolean u1Active = userSessions.get(u1.getUsername());
|
||||||
|
boolean u2Active = userSessions.get(u2.getUsername());
|
||||||
|
if (u1Active && !u2Active) return -1;
|
||||||
|
if (!u1Active && u2Active) return 1;
|
||||||
|
|
||||||
|
Date u1LastRequest =
|
||||||
|
userLastRequest.getOrDefault(
|
||||||
|
u1.getUsername(), new Date(0));
|
||||||
|
Date u2LastRequest =
|
||||||
|
userLastRequest.getOrDefault(
|
||||||
|
u2.getUsername(), new Date(0));
|
||||||
|
return u2LastRequest.compareTo(u1LastRequest);
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
List<Team> allTeams =
|
||||||
|
teamRepository.findAll().stream()
|
||||||
|
.filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
AdminSettingsData data = new AdminSettingsData();
|
||||||
|
data.setUsers(sortedUsers);
|
||||||
|
data.setCurrentUsername(authentication.getName());
|
||||||
|
data.setRoleDetails(roleDetails);
|
||||||
|
data.setUserSessions(userSessions);
|
||||||
|
data.setUserLastRequest(userLastRequest);
|
||||||
|
data.setTotalUsers(allUsers.size());
|
||||||
|
data.setActiveUsers(activeUsers);
|
||||||
|
data.setDisabledUsers(disabledUsers);
|
||||||
|
data.setTeams(allTeams);
|
||||||
|
data.setMaxPaidUsers(applicationProperties.getPremium().getMaxUsers());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/account")
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
|
@Operation(summary = "Get account page data")
|
||||||
|
public ResponseEntity<AccountData> getAccountData(Authentication authentication) {
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
return ResponseEntity.status(401).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Object principal = authentication.getPrincipal();
|
||||||
|
String username = null;
|
||||||
|
boolean isOAuth2Login = false;
|
||||||
|
boolean isSaml2Login = false;
|
||||||
|
|
||||||
|
if (principal instanceof UserDetails detailsUser) {
|
||||||
|
username = detailsUser.getUsername();
|
||||||
|
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||||
|
username = oAuth2User.getName();
|
||||||
|
isOAuth2Login = true;
|
||||||
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||||
|
username = saml2User.name();
|
||||||
|
isSaml2Login = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username == null) {
|
||||||
|
return ResponseEntity.status(401).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<User> user = userRepository.findByUsernameIgnoreCaseWithSettings(username);
|
||||||
|
if (user.isEmpty()) {
|
||||||
|
return ResponseEntity.status(404).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String settingsJson;
|
||||||
|
try {
|
||||||
|
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("Error converting settings map", e);
|
||||||
|
return ResponseEntity.status(500).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountData data = new AccountData();
|
||||||
|
data.setUsername(username);
|
||||||
|
data.setRole(user.get().getRolesAsString());
|
||||||
|
data.setSettings(settingsJson);
|
||||||
|
data.setChangeCredsFlag(user.get().isFirstLogin());
|
||||||
|
data.setOAuth2Login(isOAuth2Login);
|
||||||
|
data.setSaml2Login(isSaml2Login);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/teams")
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@Operation(summary = "Get teams list data")
|
||||||
|
public ResponseEntity<TeamsData> getTeamsData() {
|
||||||
|
List<TeamWithUserCountDTO> allTeamsWithCounts = teamRepository.findAllTeamsWithUserCount();
|
||||||
|
List<TeamWithUserCountDTO> teamsWithCounts =
|
||||||
|
allTeamsWithCounts.stream()
|
||||||
|
.filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
List<Object[]> teamActivities = sessionRepository.findLatestActivityByTeam();
|
||||||
|
Map<Long, Date> teamLastRequest = new HashMap<>();
|
||||||
|
for (Object[] result : teamActivities) {
|
||||||
|
Long teamId = (Long) result[0];
|
||||||
|
Date lastActivity = (Date) result[1];
|
||||||
|
teamLastRequest.put(teamId, lastActivity);
|
||||||
|
}
|
||||||
|
|
||||||
|
TeamsData data = new TeamsData();
|
||||||
|
data.setTeamsWithCounts(teamsWithCounts);
|
||||||
|
data.setTeamLastRequest(teamLastRequest);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/teams/{id}")
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@Operation(summary = "Get team details data")
|
||||||
|
public ResponseEntity<TeamDetailsData> getTeamDetailsData(@PathVariable("id") Long id) {
|
||||||
|
Team team =
|
||||||
|
teamRepository
|
||||||
|
.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Team not found"));
|
||||||
|
|
||||||
|
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||||
|
return ResponseEntity.status(403).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<User> teamUsers = userRepository.findAllByTeamId(id);
|
||||||
|
List<User> allUsers = userRepository.findAllWithTeam();
|
||||||
|
List<User> availableUsers =
|
||||||
|
allUsers.stream()
|
||||||
|
.filter(
|
||||||
|
user ->
|
||||||
|
(user.getTeam() == null
|
||||||
|
|| !user.getTeam().getId().equals(id))
|
||||||
|
&& (user.getTeam() == null
|
||||||
|
|| !user.getTeam()
|
||||||
|
.getName()
|
||||||
|
.equals(
|
||||||
|
TeamService
|
||||||
|
.INTERNAL_TEAM_NAME)))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
List<Object[]> userSessions = sessionRepository.findLatestSessionByTeamId(id);
|
||||||
|
Map<String, Date> userLastRequest = new HashMap<>();
|
||||||
|
for (Object[] result : userSessions) {
|
||||||
|
String username = (String) result[0];
|
||||||
|
Date lastRequest = (Date) result[1];
|
||||||
|
userLastRequest.put(username, lastRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
TeamDetailsData data = new TeamDetailsData();
|
||||||
|
data.setTeam(team);
|
||||||
|
data.setTeamUsers(teamUsers);
|
||||||
|
data.setAvailableUsers(availableUsers);
|
||||||
|
data.setUserLastRequest(userLastRequest);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/database")
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@Operation(summary = "Get database management data")
|
||||||
|
public ResponseEntity<DatabaseData> getDatabaseData() {
|
||||||
|
List<FileInfo> backupList = databaseService.getBackupList();
|
||||||
|
String dbVersion = databaseService.getH2Version();
|
||||||
|
boolean isVersionUnknown = "Unknown".equalsIgnoreCase(dbVersion);
|
||||||
|
|
||||||
|
DatabaseData data = new DatabaseData();
|
||||||
|
data.setBackupFiles(backupList);
|
||||||
|
data.setDatabaseVersion(dbVersion);
|
||||||
|
data.setVersionUnknown(isVersionUnknown);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data classes
|
||||||
|
@Data
|
||||||
|
public static class AuditDashboardData {
|
||||||
|
private boolean auditEnabled;
|
||||||
|
private AuditLevel auditLevel;
|
||||||
|
private int auditLevelInt;
|
||||||
|
private int retentionDays;
|
||||||
|
private AuditLevel[] auditLevels;
|
||||||
|
private AuditEventType[] auditEventTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class LoginData {
|
||||||
|
private Map<String, String> providerList;
|
||||||
|
private String loginMethod;
|
||||||
|
private boolean altLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class AdminSettingsData {
|
||||||
|
private List<User> users;
|
||||||
|
private String currentUsername;
|
||||||
|
private Map<String, String> roleDetails;
|
||||||
|
private Map<String, Boolean> userSessions;
|
||||||
|
private Map<String, Date> userLastRequest;
|
||||||
|
private int totalUsers;
|
||||||
|
private int activeUsers;
|
||||||
|
private int disabledUsers;
|
||||||
|
private List<Team> teams;
|
||||||
|
private int maxPaidUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class AccountData {
|
||||||
|
private String username;
|
||||||
|
private String role;
|
||||||
|
private String settings;
|
||||||
|
private boolean changeCredsFlag;
|
||||||
|
private boolean oAuth2Login;
|
||||||
|
private boolean saml2Login;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class TeamsData {
|
||||||
|
private List<TeamWithUserCountDTO> teamsWithCounts;
|
||||||
|
private Map<Long, Date> teamLastRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class TeamDetailsData {
|
||||||
|
private Team team;
|
||||||
|
private List<User> teamUsers;
|
||||||
|
private List<User> availableUsers;
|
||||||
|
private Map<String, Date> userLastRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class DatabaseData {
|
||||||
|
private List<FileInfo> backupFiles;
|
||||||
|
private String databaseVersion;
|
||||||
|
private boolean versionUnknown;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.proprietary.security.controller.api;
|
package stirling.software.proprietary.security.controller.api;
|
||||||
|
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@ -42,7 +44,7 @@ public class EmailController {
|
|||||||
* attachment.
|
* attachment.
|
||||||
* @return ResponseEntity with success or error message.
|
* @return ResponseEntity with success or error message.
|
||||||
*/
|
*/
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/send-email")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/send-email")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Send an email with an attachment",
|
summary = "Send an email with an attachment",
|
||||||
description =
|
description =
|
||||||
|
@ -26,8 +26,6 @@ services:
|
|||||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||||
SECURITY_ENABLELOGIN: "false"
|
SECURITY_ENABLELOGIN: "false"
|
||||||
FAT_DOCKER: "true"
|
FAT_DOCKER: "true"
|
||||||
INSTALL_BOOK_AND_ADVANCED_HTML_OPS: "false"
|
|
||||||
LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID"
|
|
||||||
SYSTEM_DEFAULTLOCALE: en-US
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
UI_APPNAME: Stirling-PDF
|
UI_APPNAME: Stirling-PDF
|
||||||
UI_HOMEDESCRIPTION: Full-featured Stirling-PDF with all capabilities
|
UI_HOMEDESCRIPTION: Full-featured Stirling-PDF with all capabilities
|
||||||
|
@ -25,7 +25,6 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||||
SECURITY_ENABLELOGIN: "false"
|
SECURITY_ENABLELOGIN: "false"
|
||||||
LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID"
|
|
||||||
SYSTEM_DEFAULTLOCALE: en-US
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
UI_APPNAME: Stirling-PDF
|
UI_APPNAME: Stirling-PDF
|
||||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest
|
||||||
|
@ -52,6 +52,44 @@ http {
|
|||||||
proxy_request_buffering off;
|
proxy_request_buffering off;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Proxy Swagger UI to backend (including versioned paths)
|
||||||
|
location ~ ^/swagger-ui(.*)$ {
|
||||||
|
proxy_pass ${VITE_API_BASE_URL}/swagger-ui$1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
|
||||||
|
proxy_set_header Connection '';
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_cache off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy API docs to backend (with query parameters and sub-paths)
|
||||||
|
location ~ ^/v3/api-docs(.*)$ {
|
||||||
|
proxy_pass ${VITE_API_BASE_URL}/v3/api-docs$1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy v1 API docs to backend (with query parameters and sub-paths)
|
||||||
|
location ~ ^/v1/api-docs(.*)$ {
|
||||||
|
proxy_pass ${VITE_API_BASE_URL}/v1/api-docs$1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
}
|
||||||
|
|
||||||
# Cache static assets
|
# Cache static assets
|
||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
expires 1y;
|
expires 1y;
|
||||||
|
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@ -22,3 +22,6 @@
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
|
playwright-report
|
||||||
|
test-results
|
2616
frontend/package-lock.json
generated
2616
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -37,7 +37,13 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"generate-licenses": "node scripts/generate-licenses.js"
|
"generate-licenses": "node scripts/generate-licenses.js",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:watch": "vitest --watch",
|
||||||
|
"test:coverage": "vitest --coverage",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
"test:e2e:install": "playwright install"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@ -58,15 +64,19 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.40.0",
|
||||||
"@types/react": "^19.1.4",
|
"@types/react": "^19.1.4",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.5",
|
||||||
"@vitejs/plugin-react": "^4.5.0",
|
"@vitejs/plugin-react": "^4.5.0",
|
||||||
|
"@vitest/coverage-v8": "^1.0.0",
|
||||||
|
"jsdom": "^23.0.0",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"postcss-cli": "^11.0.1",
|
"postcss-cli": "^11.0.1",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5",
|
||||||
|
"vitest": "^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
75
frontend/playwright.config.ts
Normal file
75
frontend/playwright.config.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://playwright.dev/docs/test-configuration
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './src/tests',
|
||||||
|
testMatch: '**/*.spec.ts',
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: 'html',
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
baseURL: 'http://localhost:5173',
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: {
|
||||||
|
...devices['Desktop Chrome'],
|
||||||
|
viewport: { width: 1920, height: 1080 }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: { ...devices['Desktop Firefox'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'webkit',
|
||||||
|
use: { ...devices['Desktop Safari'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run dev',
|
||||||
|
url: 'http://localhost:5173',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
@ -347,6 +347,10 @@
|
|||||||
"title": "Rotate",
|
"title": "Rotate",
|
||||||
"desc": "Easily rotate your PDFs."
|
"desc": "Easily rotate your PDFs."
|
||||||
},
|
},
|
||||||
|
"convert": {
|
||||||
|
"title": "Convert",
|
||||||
|
"desc": "Convert files between different formats"
|
||||||
|
},
|
||||||
"imageToPdf": {
|
"imageToPdf": {
|
||||||
"title": "Image to PDF",
|
"title": "Image to PDF",
|
||||||
"desc": "Convert a image (PNG, JPEG, GIF) to PDF."
|
"desc": "Convert a image (PNG, JPEG, GIF) to PDF."
|
||||||
@ -579,6 +583,10 @@
|
|||||||
"title": "Validate PDF Signature",
|
"title": "Validate PDF Signature",
|
||||||
"desc": "Verify digital signatures and certificates in PDF documents"
|
"desc": "Verify digital signatures and certificates in PDF documents"
|
||||||
},
|
},
|
||||||
|
"swagger": {
|
||||||
|
"title": "API Documentation",
|
||||||
|
"desc": "View API documentation and test endpoints"
|
||||||
|
},
|
||||||
"replaceColorPdf": {
|
"replaceColorPdf": {
|
||||||
"title": "Advanced Colour options",
|
"title": "Advanced Colour options",
|
||||||
"desc": "Replace colour for text and background in PDF and invert full colour of pdf to reduce file size"
|
"desc": "Replace colour for text and background in PDF and invert full colour of pdf to reduce file size"
|
||||||
@ -675,6 +683,73 @@
|
|||||||
"selectAngle": "Select rotation angle (in multiples of 90 degrees):",
|
"selectAngle": "Select rotation angle (in multiples of 90 degrees):",
|
||||||
"submit": "Rotate"
|
"submit": "Rotate"
|
||||||
},
|
},
|
||||||
|
"convert": {
|
||||||
|
"title": "Convert",
|
||||||
|
"desc": "Convert files between different formats",
|
||||||
|
"files": "Files",
|
||||||
|
"selectFilesPlaceholder": "Select files in the main view to get started",
|
||||||
|
"settings": "Settings",
|
||||||
|
"conversionCompleted": "Conversion completed",
|
||||||
|
"results": "Results",
|
||||||
|
"defaultFilename": "converted_file",
|
||||||
|
"conversionResults": "Conversion Results",
|
||||||
|
"convertFrom": "Convert from",
|
||||||
|
"convertTo": "Convert to",
|
||||||
|
"sourceFormatPlaceholder": "Source format",
|
||||||
|
"targetFormatPlaceholder": "Target format",
|
||||||
|
"selectSourceFormatFirst": "Select a source format first",
|
||||||
|
"outputOptions": "Output Options",
|
||||||
|
"pdfOptions": "PDF Options",
|
||||||
|
"imageOptions": "Image Options",
|
||||||
|
"colorType": "Colour Type",
|
||||||
|
"color": "Colour",
|
||||||
|
"greyscale": "Greyscale",
|
||||||
|
"blackwhite": "Black & White",
|
||||||
|
"dpi": "DPI",
|
||||||
|
"output": "Output",
|
||||||
|
"single": "Single",
|
||||||
|
"multiple": "Multiple",
|
||||||
|
"fitOption": "Fit Option",
|
||||||
|
"maintainAspectRatio": "Maintain Aspect Ratio",
|
||||||
|
"fitDocumentToPage": "Fit Document to Page",
|
||||||
|
"fillPage": "Fill Page",
|
||||||
|
"autoRotate": "Auto Rotate",
|
||||||
|
"autoRotateDescription": "Automatically rotate images to better fit the PDF page",
|
||||||
|
"combineImages": "Combine Images",
|
||||||
|
"combineImagesDescription": "Combine all images into one PDF, or create separate PDFs for each image",
|
||||||
|
"webOptions": "Web to PDF Options",
|
||||||
|
"zoomLevel": "Zoom Level",
|
||||||
|
"emailOptions": "Email to PDF Options",
|
||||||
|
"includeAttachments": "Include email attachments",
|
||||||
|
"maxAttachmentSize": "Maximum attachment size (MB)",
|
||||||
|
"includeAllRecipients": "Include CC and BCC recipients in header",
|
||||||
|
"downloadHtml": "Download HTML intermediate file instead of PDF",
|
||||||
|
"pdfaOptions": "PDF/A Options",
|
||||||
|
"outputFormat": "Output Format",
|
||||||
|
"pdfaNote": "PDF/A-1b is more compatible, PDF/A-2b supports more features.",
|
||||||
|
"pdfaDigitalSignatureWarning": "The PDF contains a digital signature. This will be removed in the next step.",
|
||||||
|
"fileFormat": "File Format",
|
||||||
|
"wordDoc": "Word Document",
|
||||||
|
"wordDocExt": "Word Document (.docx)",
|
||||||
|
"odtExt": "OpenDocument Text (.odt)",
|
||||||
|
"pptExt": "PowerPoint (.pptx)",
|
||||||
|
"odpExt": "OpenDocument Presentation (.odp)",
|
||||||
|
"txtExt": "Plain Text (.txt)",
|
||||||
|
"rtfExt": "Rich Text Format (.rtf)",
|
||||||
|
"selectedFiles": "Selected files",
|
||||||
|
"noFileSelected": "No file selected. Use the file panel to add files.",
|
||||||
|
"convertFiles": "Convert Files",
|
||||||
|
"converting": "Converting...",
|
||||||
|
"downloadConverted": "Download Converted File",
|
||||||
|
"errorNoFiles": "Please select at least one file to convert.",
|
||||||
|
"errorNoFormat": "Please select both source and target formats.",
|
||||||
|
"errorNotSupported": "Conversion from {{from}} to {{to}} is not supported.",
|
||||||
|
"images": "Images",
|
||||||
|
"officeDocs": "Office Documents (Word, Excel, PowerPoint)",
|
||||||
|
"imagesExt": "Images (JPG, PNG, etc.)",
|
||||||
|
"markdown": "Markdown",
|
||||||
|
"textRtf": "Text/RTF"
|
||||||
|
},
|
||||||
"imageToPdf": {
|
"imageToPdf": {
|
||||||
"tags": "conversion,img,jpg,picture,photo"
|
"tags": "conversion,img,jpg,picture,photo"
|
||||||
},
|
},
|
||||||
@ -1562,6 +1637,12 @@
|
|||||||
},
|
},
|
||||||
"note": "Release notes are only available in English"
|
"note": "Release notes are only available in English"
|
||||||
},
|
},
|
||||||
|
"swagger": {
|
||||||
|
"title": "API Documentation",
|
||||||
|
"header": "API Documentation",
|
||||||
|
"desc": "View and test the Stirling PDF API endpoints",
|
||||||
|
"tags": "api,documentation,swagger,endpoints,development"
|
||||||
|
},
|
||||||
"cookieBanner": {
|
"cookieBanner": {
|
||||||
"popUp": {
|
"popUp": {
|
||||||
"title": "How we use Cookies",
|
"title": "How we use Cookies",
|
||||||
@ -1613,18 +1694,6 @@
|
|||||||
"pageEditor": "Page Editor",
|
"pageEditor": "Page Editor",
|
||||||
"fileManager": "File Manager"
|
"fileManager": "File Manager"
|
||||||
},
|
},
|
||||||
"fileManager": {
|
|
||||||
"dragDrop": "Drag & Drop files here",
|
|
||||||
"clickToUpload": "Click to upload files",
|
|
||||||
"selectedFiles": "Selected Files",
|
|
||||||
"clearAll": "Clear All",
|
|
||||||
"storage": "Storage",
|
|
||||||
"filesStored": "files stored",
|
|
||||||
"storageError": "Storage error occurred",
|
|
||||||
"storageLow": "Storage is running low. Consider removing old files.",
|
|
||||||
"uploadError": "Failed to upload some files.",
|
|
||||||
"supportMessage": "Powered by browser database storage for unlimited capacity"
|
|
||||||
},
|
|
||||||
"pageEditor": {
|
"pageEditor": {
|
||||||
"title": "Page Editor",
|
"title": "Page Editor",
|
||||||
"save": "Save Changes",
|
"save": "Save Changes",
|
||||||
@ -1696,7 +1765,16 @@
|
|||||||
"failedToLoad": "Failed to load file to active set.",
|
"failedToLoad": "Failed to load file to active set.",
|
||||||
"storageCleared": "Browser cleared storage. Files have been removed. Please re-upload.",
|
"storageCleared": "Browser cleared storage. Files have been removed. Please re-upload.",
|
||||||
"clearAll": "Clear All",
|
"clearAll": "Clear All",
|
||||||
"reloadFiles": "Reload Files"
|
"reloadFiles": "Reload Files",
|
||||||
|
"dragDrop": "Drag & Drop files here",
|
||||||
|
"clickToUpload": "Click to upload files",
|
||||||
|
"selectedFiles": "Selected Files",
|
||||||
|
"storage": "Storage",
|
||||||
|
"filesStored": "files stored",
|
||||||
|
"storageError": "Storage error occurred",
|
||||||
|
"storageLow": "Storage is running low. Consider removing old files.",
|
||||||
|
"supportMessage": "Powered by browser database storage for unlimited capacity",
|
||||||
|
"noFileSelected": "No files selected"
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"temporaryNotice": "Files are stored temporarily in your browser and may be cleared automatically",
|
"temporaryNotice": "Files are stored temporarily in your browser and may be cleared automatically",
|
||||||
|
@ -575,6 +575,10 @@
|
|||||||
"title": "Validate PDF Signature",
|
"title": "Validate PDF Signature",
|
||||||
"desc": "Verify digital signatures and certificates in PDF documents"
|
"desc": "Verify digital signatures and certificates in PDF documents"
|
||||||
},
|
},
|
||||||
|
"swagger": {
|
||||||
|
"title": "API Documentation",
|
||||||
|
"desc": "View API documentation and test endpoints"
|
||||||
|
},
|
||||||
"replaceColorPdf": {
|
"replaceColorPdf": {
|
||||||
"title": "Replace and Invert Color",
|
"title": "Replace and Invert Color",
|
||||||
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
|
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
|
||||||
@ -1521,6 +1525,12 @@
|
|||||||
},
|
},
|
||||||
"note": "Release notes are only available in English"
|
"note": "Release notes are only available in English"
|
||||||
},
|
},
|
||||||
|
"swagger": {
|
||||||
|
"title": "API Documentation",
|
||||||
|
"header": "API Documentation",
|
||||||
|
"desc": "View and test the Stirling PDF API endpoints",
|
||||||
|
"tags": "api,documentation,swagger,endpoints,development"
|
||||||
|
},
|
||||||
"cookieBanner": {
|
"cookieBanner": {
|
||||||
"popUp": {
|
"popUp": {
|
||||||
"title": "How we use Cookies",
|
"title": "How we use Cookies",
|
||||||
@ -1557,5 +1567,51 @@
|
|||||||
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
|
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"convert": {
|
||||||
|
"files": "Files",
|
||||||
|
"selectFilesPlaceholder": "Select files in the main view to get started",
|
||||||
|
"settings": "Settings",
|
||||||
|
"conversionCompleted": "Conversion completed",
|
||||||
|
"results": "Results",
|
||||||
|
"defaultFilename": "converted_file",
|
||||||
|
"conversionResults": "Conversion Results",
|
||||||
|
"converting": "Converting...",
|
||||||
|
"convertFiles": "Convert Files",
|
||||||
|
"downloadConverted": "Download Converted File",
|
||||||
|
"convertFrom": "Convert from",
|
||||||
|
"convertTo": "Convert to",
|
||||||
|
"sourceFormatPlaceholder": "Source format",
|
||||||
|
"targetFormatPlaceholder": "Target format",
|
||||||
|
"selectSourceFormatFirst": "Select a source format first",
|
||||||
|
"imageOptions": "Image Options",
|
||||||
|
"colorType": "Color Type",
|
||||||
|
"color": "Color",
|
||||||
|
"greyscale": "Greyscale",
|
||||||
|
"blackwhite": "Black & White",
|
||||||
|
"dpi": "DPI",
|
||||||
|
"output": "Output",
|
||||||
|
"single": "Single",
|
||||||
|
"multiple": "Multiple",
|
||||||
|
"pdfOptions": "PDF Options",
|
||||||
|
"fitOption": "Fit Option",
|
||||||
|
"maintainAspectRatio": "Maintain Aspect Ratio",
|
||||||
|
"fitDocumentToPage": "Fit Document to Page",
|
||||||
|
"fillPage": "Fill Page",
|
||||||
|
"autoRotate": "Auto Rotate",
|
||||||
|
"autoRotateDescription": "Automatically rotate images to better fit the PDF page",
|
||||||
|
"combineImages": "Combine Images",
|
||||||
|
"combineImagesDescription": "Combine all images into one PDF, or create separate PDFs for each image",
|
||||||
|
"webOptions": "Web to PDF Options",
|
||||||
|
"zoomLevel": "Zoom Level",
|
||||||
|
"emailOptions": "Email to PDF Options",
|
||||||
|
"includeAttachments": "Include email attachments",
|
||||||
|
"maxAttachmentSize": "Maximum attachment size (MB)",
|
||||||
|
"includeAllRecipients": "Include CC and BCC recipients in header",
|
||||||
|
"downloadHtml": "Download HTML intermediate file instead of PDF",
|
||||||
|
"pdfaOptions": "PDF/A Options",
|
||||||
|
"outputFormat": "Output Format",
|
||||||
|
"pdfaNote": "PDF/A-1b is more compatible, PDF/A-2b supports more features.",
|
||||||
|
"pdfaDigitalSignatureWarning": "The PDF contains a digital signature. This will be removed in the next step."
|
||||||
}
|
}
|
||||||
}
|
}
|
29
frontend/public/locales/en/translation.json
Normal file
29
frontend/public/locales/en/translation.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"convert": {
|
||||||
|
"selectSourceFormat": "Select source file format",
|
||||||
|
"selectTargetFormat": "Select target file format",
|
||||||
|
"selectFirst": "Select a source format first",
|
||||||
|
"imageOptions": "Image Options:",
|
||||||
|
"emailOptions": "Email Options:",
|
||||||
|
"colorType": "Color Type",
|
||||||
|
"dpi": "DPI",
|
||||||
|
"singleOrMultiple": "Output",
|
||||||
|
"emailNote": "Email attachments and embedded images will be included"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"color": "Color",
|
||||||
|
"grayscale": "Grayscale",
|
||||||
|
"blackWhite": "Black & White",
|
||||||
|
"single": "Single Image",
|
||||||
|
"multiple": "Multiple Images"
|
||||||
|
},
|
||||||
|
"groups": {
|
||||||
|
"document": "Document",
|
||||||
|
"spreadsheet": "Spreadsheet",
|
||||||
|
"presentation": "Presentation",
|
||||||
|
"image": "Image",
|
||||||
|
"web": "Web",
|
||||||
|
"text": "Text",
|
||||||
|
"email": "Email"
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +0,0 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
|
||||||
render(<App />);
|
|
||||||
const linkElement = screen.getByText(/learn react/i);
|
|
||||||
expect(linkElement).toBeInTheDocument();
|
|
||||||
});
|
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider';
|
import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider';
|
||||||
import { FileContextProvider } from './contexts/FileContext';
|
import { FileContextProvider } from './contexts/FileContext';
|
||||||
|
import { FilesModalProvider } from './contexts/FilesModalContext';
|
||||||
import HomePage from './pages/HomePage';
|
import HomePage from './pages/HomePage';
|
||||||
|
|
||||||
// Import global styles
|
// Import global styles
|
||||||
@ -11,7 +12,9 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<RainbowThemeProvider>
|
<RainbowThemeProvider>
|
||||||
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
||||||
<HomePage />
|
<FilesModalProvider>
|
||||||
|
<HomePage />
|
||||||
|
</FilesModalProvider>
|
||||||
</FileContextProvider>
|
</FileContextProvider>
|
||||||
</RainbowThemeProvider>
|
</RainbowThemeProvider>
|
||||||
);
|
);
|
||||||
|
2006
frontend/src/assets/3rdPartyLicenses.json
Normal file
2006
frontend/src/assets/3rdPartyLicenses.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@ import { FileOperation } from '../../types/fileContext';
|
|||||||
import { fileStorage } from '../../services/fileStorage';
|
import { fileStorage } from '../../services/fileStorage';
|
||||||
import { generateThumbnailForFile } from '../../utils/thumbnailUtils';
|
import { generateThumbnailForFile } from '../../utils/thumbnailUtils';
|
||||||
import { zipFileService } from '../../services/zipFileService';
|
import { zipFileService } from '../../services/zipFileService';
|
||||||
|
import { detectFileExtension } from '../../utils/fileUtils';
|
||||||
import styles from '../pageEditor/PageEditor.module.css';
|
import styles from '../pageEditor/PageEditor.module.css';
|
||||||
import FileThumbnail from '../pageEditor/FileThumbnail';
|
import FileThumbnail from '../pageEditor/FileThumbnail';
|
||||||
import DragDropGrid from '../pageEditor/DragDropGrid';
|
import DragDropGrid from '../pageEditor/DragDropGrid';
|
||||||
@ -34,6 +35,7 @@ interface FileEditorProps {
|
|||||||
toolMode?: boolean;
|
toolMode?: boolean;
|
||||||
showUpload?: boolean;
|
showUpload?: boolean;
|
||||||
showBulkActions?: boolean;
|
showBulkActions?: boolean;
|
||||||
|
supportedExtensions?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileEditor = ({
|
const FileEditor = ({
|
||||||
@ -41,10 +43,17 @@ const FileEditor = ({
|
|||||||
onMergeFiles,
|
onMergeFiles,
|
||||||
toolMode = false,
|
toolMode = false,
|
||||||
showUpload = true,
|
showUpload = true,
|
||||||
showBulkActions = true
|
showBulkActions = true,
|
||||||
|
supportedExtensions = ["pdf"]
|
||||||
}: FileEditorProps) => {
|
}: FileEditorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// Utility function to check if a file extension is supported
|
||||||
|
const isFileSupported = useCallback((fileName: string): boolean => {
|
||||||
|
const extension = detectFileExtension(fileName);
|
||||||
|
return extension ? supportedExtensions.includes(extension) : false;
|
||||||
|
}, [supportedExtensions]);
|
||||||
|
|
||||||
// Get file context
|
// Get file context
|
||||||
const fileContext = useFileContext();
|
const fileContext = useFileContext();
|
||||||
const {
|
const {
|
||||||
@ -224,49 +233,46 @@ const FileEditor = ({
|
|||||||
// Handle PDF files normally
|
// Handle PDF files normally
|
||||||
allExtractedFiles.push(file);
|
allExtractedFiles.push(file);
|
||||||
} else if (file.type === 'application/zip' || file.type === 'application/x-zip-compressed' || file.name.toLowerCase().endsWith('.zip')) {
|
} else if (file.type === 'application/zip' || file.type === 'application/x-zip-compressed' || file.name.toLowerCase().endsWith('.zip')) {
|
||||||
// Handle ZIP files
|
// Handle ZIP files - only expand if they contain PDFs
|
||||||
try {
|
try {
|
||||||
// Validate ZIP file first
|
// Validate ZIP file first
|
||||||
const validation = await zipFileService.validateZipFile(file);
|
const validation = await zipFileService.validateZipFile(file);
|
||||||
if (!validation.isValid) {
|
|
||||||
errors.push(`ZIP file "${file.name}": ${validation.errors.join(', ')}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract PDF files from ZIP
|
if (validation.isValid && validation.containsPDFs) {
|
||||||
setZipExtractionProgress({
|
// ZIP contains PDFs - extract them
|
||||||
isExtracting: true,
|
|
||||||
currentFile: file.name,
|
|
||||||
progress: 0,
|
|
||||||
extractedCount: 0,
|
|
||||||
totalFiles: validation.fileCount
|
|
||||||
});
|
|
||||||
|
|
||||||
const extractionResult = await zipFileService.extractPdfFiles(file, (progress) => {
|
|
||||||
setZipExtractionProgress({
|
setZipExtractionProgress({
|
||||||
isExtracting: true,
|
isExtracting: true,
|
||||||
currentFile: progress.currentFile,
|
currentFile: file.name,
|
||||||
progress: progress.progress,
|
progress: 0,
|
||||||
extractedCount: progress.extractedCount,
|
extractedCount: 0,
|
||||||
totalFiles: progress.totalFiles
|
totalFiles: validation.fileCount
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Reset extraction progress
|
const extractionResult = await zipFileService.extractPdfFiles(file, (progress) => {
|
||||||
setZipExtractionProgress({
|
setZipExtractionProgress({
|
||||||
isExtracting: false,
|
isExtracting: true,
|
||||||
currentFile: '',
|
currentFile: progress.currentFile,
|
||||||
progress: 0,
|
progress: progress.progress,
|
||||||
extractedCount: 0,
|
extractedCount: progress.extractedCount,
|
||||||
totalFiles: 0
|
totalFiles: progress.totalFiles
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (extractionResult.success) {
|
// Reset extraction progress
|
||||||
allExtractedFiles.push(...extractionResult.extractedFiles);
|
setZipExtractionProgress({
|
||||||
|
isExtracting: false,
|
||||||
|
currentFile: '',
|
||||||
|
progress: 0,
|
||||||
|
extractedCount: 0,
|
||||||
|
totalFiles: 0
|
||||||
|
});
|
||||||
|
|
||||||
// Record ZIP extraction operation
|
if (extractionResult.success) {
|
||||||
const operationId = `zip-extract-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
allExtractedFiles.push(...extractionResult.extractedFiles);
|
||||||
const operation: FileOperation = {
|
|
||||||
|
// Record ZIP extraction operation
|
||||||
|
const operationId = `zip-extract-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
const operation: FileOperation = {
|
||||||
id: operationId,
|
id: operationId,
|
||||||
type: 'convert',
|
type: 'convert',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@ -290,8 +296,13 @@ const FileEditor = ({
|
|||||||
if (extractionResult.errors.length > 0) {
|
if (extractionResult.errors.length > 0) {
|
||||||
errors.push(...extractionResult.errors);
|
errors.push(...extractionResult.errors);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
errors.push(`Failed to extract ZIP file "${file.name}": ${extractionResult.errors.join(', ')}`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
errors.push(`Failed to extract ZIP file "${file.name}": ${extractionResult.errors.join(', ')}`);
|
// ZIP doesn't contain PDFs or is invalid - treat as regular file
|
||||||
|
console.log(`Adding ZIP file as regular file: ${file.name} (no PDFs found)`);
|
||||||
|
allExtractedFiles.push(file);
|
||||||
}
|
}
|
||||||
} catch (zipError) {
|
} catch (zipError) {
|
||||||
errors.push(`Failed to process ZIP file "${file.name}": ${zipError instanceof Error ? zipError.message : 'Unknown error'}`);
|
errors.push(`Failed to process ZIP file "${file.name}": ${zipError instanceof Error ? zipError.message : 'Unknown error'}`);
|
||||||
@ -304,7 +315,8 @@ const FileEditor = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errors.push(`Unsupported file type: ${file.name} (${file.type})`);
|
console.log(`Adding none PDF file: ${file.name} (${file.type})`);
|
||||||
|
allExtractedFiles.push(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -653,46 +665,35 @@ const FileEditor = ({
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box pos="relative" h="100vh" style={{ overflow: 'auto' }}>
|
<Dropzone
|
||||||
<LoadingOverlay visible={false} />
|
onDrop={handleFileUpload}
|
||||||
|
accept={["*/*"]}
|
||||||
|
multiple={true}
|
||||||
|
maxSize={2 * 1024 * 1024 * 1024}
|
||||||
|
style={{
|
||||||
|
height: '100vh',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 0,
|
||||||
|
backgroundColor: 'transparent'
|
||||||
|
}}
|
||||||
|
activateOnClick={false}
|
||||||
|
activateOnDrag={true}
|
||||||
|
>
|
||||||
|
<Box pos="relative" h="100vh" style={{ overflow: 'auto' }}>
|
||||||
|
<LoadingOverlay visible={false} />
|
||||||
|
|
||||||
<Box p="md" pt="xl">
|
<Box p="md" pt="xl">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
{showBulkActions && !toolMode && (
|
{showBulkActions && !toolMode && (
|
||||||
<>
|
<>
|
||||||
<Button onClick={selectAll} variant="light">Select All</Button>
|
<Button onClick={selectAll} variant="light">Select All</Button>
|
||||||
<Button onClick={deselectAll} variant="light">Deselect All</Button>
|
<Button onClick={deselectAll} variant="light">Deselect All</Button>
|
||||||
<Button onClick={closeAllFiles} variant="light" color="orange">
|
<Button onClick={closeAllFiles} variant="light" color="orange">
|
||||||
Close All
|
Close All
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Load from storage and upload buttons */}
|
|
||||||
{showUpload && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
color="blue"
|
|
||||||
onClick={() => setShowFilePickerModal(true)}
|
|
||||||
>
|
|
||||||
Load from Storage
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Dropzone
|
|
||||||
onDrop={handleFileUpload}
|
|
||||||
accept={["application/pdf", "application/zip", "application/x-zip-compressed"]}
|
|
||||||
multiple={true}
|
|
||||||
maxSize={2 * 1024 * 1024 * 1024}
|
|
||||||
style={{ display: 'contents' }}
|
|
||||||
>
|
|
||||||
<Button variant="outline" color="green">
|
|
||||||
Upload Files
|
|
||||||
</Button>
|
</Button>
|
||||||
</Dropzone>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</Group>
|
||||||
</Group>
|
|
||||||
|
|
||||||
|
|
||||||
{files.length === 0 && !localLoading && !zipExtractionProgress.isExtracting ? (
|
{files.length === 0 && !localLoading && !zipExtractionProgress.isExtracting ? (
|
||||||
@ -804,6 +805,7 @@ const FileEditor = ({
|
|||||||
onSplitFile={handleSplitFile}
|
onSplitFile={handleSplitFile}
|
||||||
onSetStatus={setStatus}
|
onSetStatus={setStatus}
|
||||||
toolMode={toolMode}
|
toolMode={toolMode}
|
||||||
|
isSupported={isFileSupported(file.name)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderSplitMarker={(file, index) => (
|
renderSplitMarker={(file, index) => (
|
||||||
@ -853,7 +855,8 @@ const FileEditor = ({
|
|||||||
{error}
|
{error}
|
||||||
</Notification>
|
</Notification>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
</Dropzone>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,9 +18,10 @@ interface FileCardProps {
|
|||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
onSelect?: () => void;
|
onSelect?: () => void;
|
||||||
|
isSupported?: boolean; // Whether the file format is supported by the current tool
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, onSelect }: FileCardProps) => {
|
const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, onSelect, isSupported = true }: FileCardProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { thumbnail: thumb, isGenerating } = useIndexedDBThumbnail(file);
|
const { thumbnail: thumb, isGenerating } = useIndexedDBThumbnail(file);
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
@ -35,15 +36,18 @@ const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, o
|
|||||||
width: 225,
|
width: 225,
|
||||||
minWidth: 180,
|
minWidth: 180,
|
||||||
maxWidth: 260,
|
maxWidth: 260,
|
||||||
cursor: onDoubleClick ? "pointer" : undefined,
|
cursor: onDoubleClick && isSupported ? "pointer" : undefined,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
border: isSelected ? '2px solid var(--mantine-color-blue-6)' : undefined,
|
border: isSelected ? '2px solid var(--mantine-color-blue-6)' : undefined,
|
||||||
backgroundColor: isSelected ? 'var(--mantine-color-blue-0)' : undefined
|
backgroundColor: isSelected ? 'var(--mantine-color-blue-0)' : undefined,
|
||||||
|
opacity: isSupported ? 1 : 0.5,
|
||||||
|
filter: isSupported ? 'none' : 'grayscale(50%)'
|
||||||
}}
|
}}
|
||||||
onDoubleClick={onDoubleClick}
|
onDoubleClick={onDoubleClick}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
|
data-testid="file-card"
|
||||||
>
|
>
|
||||||
<Stack gap={6} align="center">
|
<Stack gap={6} align="center">
|
||||||
<Box
|
<Box
|
||||||
@ -179,6 +183,11 @@ const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, o
|
|||||||
DB
|
DB
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
{!isSupported && (
|
||||||
|
<Badge color="orange" variant="filled" size="sm">
|
||||||
|
{t("fileManager.unsupported", "Unsupported")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Text, Checkbox, Tooltip, ActionIcon, Badge, Modal } from '@mantine/core';
|
import { Text, Checkbox, Tooltip, ActionIcon, Badge, Modal } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
import HistoryIcon from '@mui/icons-material/History';
|
import HistoryIcon from '@mui/icons-material/History';
|
||||||
@ -37,6 +38,7 @@ interface FileThumbnailProps {
|
|||||||
onViewFile: (fileId: string) => void;
|
onViewFile: (fileId: string) => void;
|
||||||
onSetStatus: (status: string) => void;
|
onSetStatus: (status: string) => void;
|
||||||
toolMode?: boolean;
|
toolMode?: boolean;
|
||||||
|
isSupported?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileThumbnail = ({
|
const FileThumbnail = ({
|
||||||
@ -60,7 +62,9 @@ const FileThumbnail = ({
|
|||||||
onViewFile,
|
onViewFile,
|
||||||
onSetStatus,
|
onSetStatus,
|
||||||
toolMode = false,
|
toolMode = false,
|
||||||
|
isSupported = true,
|
||||||
}: FileThumbnailProps) => {
|
}: FileThumbnailProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [showHistory, setShowHistory] = useState(false);
|
const [showHistory, setShowHistory] = useState(false);
|
||||||
|
|
||||||
const formatFileSize = (bytes: number) => {
|
const formatFileSize = (bytes: number) => {
|
||||||
@ -81,6 +85,7 @@ const FileThumbnail = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
data-file-id={file.id}
|
data-file-id={file.id}
|
||||||
|
data-testid="file-thumbnail"
|
||||||
className={`
|
className={`
|
||||||
${styles.pageContainer}
|
${styles.pageContainer}
|
||||||
!rounded-lg
|
!rounded-lg
|
||||||
@ -106,7 +111,9 @@ const FileThumbnail = ({
|
|||||||
}
|
}
|
||||||
return 'translateX(0)';
|
return 'translateX(0)';
|
||||||
})(),
|
})(),
|
||||||
transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out'
|
transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out',
|
||||||
|
opacity: isSupported ? 1 : 0.5,
|
||||||
|
filter: isSupported ? 'none' : 'grayscale(50%)'
|
||||||
}}
|
}}
|
||||||
draggable
|
draggable
|
||||||
onDragStart={() => onDragStart(file.id)}
|
onDragStart={() => onDragStart(file.id)}
|
||||||
@ -119,6 +126,7 @@ const FileThumbnail = ({
|
|||||||
{selectionMode && (
|
{selectionMode && (
|
||||||
<div
|
<div
|
||||||
className={styles.checkboxContainer}
|
className={styles.checkboxContainer}
|
||||||
|
data-testid="file-thumbnail-checkbox"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
top: 8,
|
||||||
@ -140,9 +148,12 @@ const FileThumbnail = ({
|
|||||||
checked={selectedFiles.includes(file.id)}
|
checked={selectedFiles.includes(file.id)}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onToggleFile(file.id);
|
if (isSupported) {
|
||||||
|
onToggleFile(file.id);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
disabled={!isSupported}
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -193,6 +204,23 @@ const FileThumbnail = ({
|
|||||||
{file.pageCount} pages
|
{file.pageCount} pages
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
|
{/* Unsupported badge */}
|
||||||
|
{!isSupported && (
|
||||||
|
<Badge
|
||||||
|
size="sm"
|
||||||
|
variant="filled"
|
||||||
|
color="orange"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 8,
|
||||||
|
right: selectionMode ? 48 : 8, // Avoid overlap with checkbox
|
||||||
|
zIndex: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("fileManager.unsupported", "Unsupported")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* File name overlay */}
|
{/* File name overlay */}
|
||||||
<Text
|
<Text
|
||||||
className={styles.pageNumber}
|
className={styles.pageNumber}
|
||||||
@ -238,7 +266,7 @@ const FileThumbnail = ({
|
|||||||
whiteSpace: 'nowrap'
|
whiteSpace: 'nowrap'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!toolMode && (
|
{!toolMode && isSupported && (
|
||||||
<>
|
<>
|
||||||
<Tooltip label="View File">
|
<Tooltip label="View File">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
|
237
frontend/src/components/shared/DropdownListWithFooter.tsx
Normal file
237
frontend/src/components/shared/DropdownListWithFooter.tsx
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import React, { ReactNode, useState, useMemo } from 'react';
|
||||||
|
import { Stack, Text, Popover, Box, Checkbox, Group, TextInput } from '@mantine/core';
|
||||||
|
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
|
||||||
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
|
|
||||||
|
export interface DropdownItem {
|
||||||
|
value: string;
|
||||||
|
name: string;
|
||||||
|
leftIcon?: ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DropdownListWithFooterProps {
|
||||||
|
// Value and onChange - support both single and multi-select
|
||||||
|
value: string | string[];
|
||||||
|
onChange: (value: string | string[]) => void;
|
||||||
|
|
||||||
|
// Items and display
|
||||||
|
items: DropdownItem[];
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
|
||||||
|
// Labels and headers
|
||||||
|
label?: string;
|
||||||
|
header?: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
|
||||||
|
// Behavior
|
||||||
|
multiSelect?: boolean;
|
||||||
|
searchable?: boolean;
|
||||||
|
maxHeight?: number;
|
||||||
|
|
||||||
|
// Styling
|
||||||
|
className?: string;
|
||||||
|
dropdownClassName?: string;
|
||||||
|
|
||||||
|
// Popover props
|
||||||
|
position?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
withArrow?: boolean;
|
||||||
|
width?: 'target' | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
items,
|
||||||
|
placeholder = 'Select option',
|
||||||
|
disabled = false,
|
||||||
|
label,
|
||||||
|
header,
|
||||||
|
footer,
|
||||||
|
multiSelect = false,
|
||||||
|
searchable = false,
|
||||||
|
maxHeight = 300,
|
||||||
|
className = '',
|
||||||
|
dropdownClassName = '',
|
||||||
|
position = 'bottom',
|
||||||
|
withArrow = false,
|
||||||
|
width = 'target'
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
const isMultiValue = Array.isArray(value);
|
||||||
|
const selectedValues = isMultiValue ? value : (value ? [value] : []);
|
||||||
|
|
||||||
|
// Filter items based on search term
|
||||||
|
const filteredItems = useMemo(() => {
|
||||||
|
if (!searchable || !searchTerm.trim()) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
return items.filter(item =>
|
||||||
|
item.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
}, [items, searchTerm, searchable]);
|
||||||
|
|
||||||
|
const handleItemClick = (itemValue: string) => {
|
||||||
|
if (multiSelect) {
|
||||||
|
const newSelection = selectedValues.includes(itemValue)
|
||||||
|
? selectedValues.filter(v => v !== itemValue)
|
||||||
|
: [...selectedValues, itemValue];
|
||||||
|
onChange(newSelection);
|
||||||
|
} else {
|
||||||
|
onChange(itemValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDisplayText = () => {
|
||||||
|
if (selectedValues.length === 0) {
|
||||||
|
return placeholder;
|
||||||
|
} else if (selectedValues.length === 1) {
|
||||||
|
const selectedItem = items.find(item => item.value === selectedValues[0]);
|
||||||
|
return selectedItem?.name || selectedValues[0];
|
||||||
|
} else {
|
||||||
|
return `${selectedValues.length} selected`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearchTerm(event.currentTarget.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className={className}>
|
||||||
|
{label && (
|
||||||
|
<Text size="sm" fw={500} mb={4}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
width={width}
|
||||||
|
position={position}
|
||||||
|
withArrow={withArrow}
|
||||||
|
shadow="md"
|
||||||
|
onClose={() => searchable && setSearchTerm('')}
|
||||||
|
>
|
||||||
|
<Popover.Target>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
border: 'light-dark(1px solid var(--mantine-color-gray-3), 1px solid var(--mantine-color-dark-4))',
|
||||||
|
borderRadius: 'var(--mantine-radius-sm)',
|
||||||
|
padding: '8px 12px',
|
||||||
|
backgroundColor: 'light-dark(var(--mantine-color-white), var(--mantine-color-dark-6))',
|
||||||
|
opacity: disabled ? 0.6 : 1,
|
||||||
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
|
minHeight: '36px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="sm" style={{ flex: 1 }}>
|
||||||
|
{getDisplayText()}
|
||||||
|
</Text>
|
||||||
|
<UnfoldMoreIcon style={{
|
||||||
|
fontSize: '1rem',
|
||||||
|
color: 'light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-2))'
|
||||||
|
}} />
|
||||||
|
</Box>
|
||||||
|
</Popover.Target>
|
||||||
|
|
||||||
|
<Popover.Dropdown className={dropdownClassName}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
{header && (
|
||||||
|
<Box style={{
|
||||||
|
borderBottom: 'light-dark(1px solid var(--mantine-color-gray-2), 1px solid var(--mantine-color-dark-4))',
|
||||||
|
paddingBottom: '8px'
|
||||||
|
}}>
|
||||||
|
{header}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{searchable && (
|
||||||
|
<Box style={{
|
||||||
|
borderBottom: 'light-dark(1px solid var(--mantine-color-gray-2), 1px solid var(--mantine-color-dark-4))',
|
||||||
|
paddingBottom: '8px'
|
||||||
|
}}>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Search..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
leftSection={<SearchIcon style={{ fontSize: '1rem' }} />}
|
||||||
|
size="sm"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box style={{ maxHeight, overflowY: 'auto' }}>
|
||||||
|
{filteredItems.length === 0 ? (
|
||||||
|
<Box style={{ padding: '12px', textAlign: 'center' }}>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{searchable && searchTerm ? 'No results found' : 'No items available'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
filteredItems.map((item) => (
|
||||||
|
<Box
|
||||||
|
key={item.value}
|
||||||
|
onClick={() => !item.disabled && handleItemClick(item.value)}
|
||||||
|
style={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
cursor: item.disabled ? 'not-allowed' : 'pointer',
|
||||||
|
borderRadius: 'var(--mantine-radius-sm)',
|
||||||
|
opacity: item.disabled ? 0.5 : 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (!item.disabled) {
|
||||||
|
e.currentTarget.style.backgroundColor = 'light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5))';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = 'transparent';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group gap="sm" style={{ flex: 1 }}>
|
||||||
|
{item.leftIcon && (
|
||||||
|
<Box style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
{item.leftIcon}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Text size="sm">{item.name}</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{multiSelect && (
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedValues.includes(item.value)}
|
||||||
|
onChange={() => {}} // Handled by parent onClick
|
||||||
|
size="sm"
|
||||||
|
disabled={item.disabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{footer && (
|
||||||
|
<Box style={{
|
||||||
|
borderTop: 'light-dark(1px solid var(--mantine-color-gray-2), 1px solid var(--mantine-color-dark-4))',
|
||||||
|
paddingTop: '8px'
|
||||||
|
}}>
|
||||||
|
{footer}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropdownListWithFooter;
|
@ -20,6 +20,7 @@ interface FileGridProps {
|
|||||||
onShowAll?: () => void;
|
onShowAll?: () => void;
|
||||||
showingAll?: boolean;
|
showingAll?: boolean;
|
||||||
onDeleteAll?: () => void;
|
onDeleteAll?: () => void;
|
||||||
|
isFileSupported?: (fileName: string) => boolean; // Function to check if file is supported
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortOption = 'date' | 'name' | 'size';
|
type SortOption = 'date' | 'name' | 'size';
|
||||||
@ -37,7 +38,8 @@ const FileGrid = ({
|
|||||||
maxDisplay,
|
maxDisplay,
|
||||||
onShowAll,
|
onShowAll,
|
||||||
showingAll = false,
|
showingAll = false,
|
||||||
onDeleteAll
|
onDeleteAll,
|
||||||
|
isFileSupported
|
||||||
}: FileGridProps) => {
|
}: FileGridProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
@ -123,16 +125,18 @@ const FileGrid = ({
|
|||||||
{displayFiles.map((file, idx) => {
|
{displayFiles.map((file, idx) => {
|
||||||
const fileId = file.id || file.name;
|
const fileId = file.id || file.name;
|
||||||
const originalIdx = files.findIndex(f => (f.id || f.name) === fileId);
|
const originalIdx = files.findIndex(f => (f.id || f.name) === fileId);
|
||||||
|
const supported = isFileSupported ? isFileSupported(file.name) : true;
|
||||||
return (
|
return (
|
||||||
<FileCard
|
<FileCard
|
||||||
key={fileId + idx}
|
key={fileId + idx}
|
||||||
file={file}
|
file={file}
|
||||||
onRemove={onRemove ? () => onRemove(originalIdx) : undefined}
|
onRemove={onRemove ? () => onRemove(originalIdx) : undefined}
|
||||||
onDoubleClick={onDoubleClick ? () => onDoubleClick(file) : undefined}
|
onDoubleClick={onDoubleClick && supported ? () => onDoubleClick(file) : undefined}
|
||||||
onView={onView ? () => onView(file) : undefined}
|
onView={onView && supported ? () => onView(file) : undefined}
|
||||||
onEdit={onEdit ? () => onEdit(file) : undefined}
|
onEdit={onEdit && supported ? () => onEdit(file) : undefined}
|
||||||
isSelected={selectedFiles.includes(fileId)}
|
isSelected={selectedFiles.includes(fileId)}
|
||||||
onSelect={onSelect ? () => onSelect(fileId) : undefined}
|
onSelect={onSelect && supported ? () => onSelect(fileId) : undefined}
|
||||||
|
isSupported={supported}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
36
frontend/src/components/shared/FileUploadModal.tsx
Normal file
36
frontend/src/components/shared/FileUploadModal.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Modal } from '@mantine/core';
|
||||||
|
import FileUploadSelector from './FileUploadSelector';
|
||||||
|
import { useFilesModalContext } from '../../contexts/FilesModalContext';
|
||||||
|
import { Tool } from '../../types/tool';
|
||||||
|
|
||||||
|
interface FileUploadModalProps {
|
||||||
|
selectedTool?: Tool | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileUploadModal: React.FC<FileUploadModalProps> = ({ selectedTool }) => {
|
||||||
|
const { isFilesModalOpen, closeFilesModal, onFileSelect, onFilesSelect } = useFilesModalContext();
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={isFilesModalOpen}
|
||||||
|
onClose={closeFilesModal}
|
||||||
|
title="Upload Files"
|
||||||
|
size="xl"
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<FileUploadSelector
|
||||||
|
title="Upload Files"
|
||||||
|
subtitle="Choose files from storage or upload new files"
|
||||||
|
onFileSelect={onFileSelect}
|
||||||
|
onFilesSelect={onFilesSelect}
|
||||||
|
accept={["*/*"]}
|
||||||
|
supportedExtensions={selectedTool?.supportedFormats || ["pdf"]}
|
||||||
|
data-testid="file-upload-modal"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileUploadModal;
|
@ -5,6 +5,7 @@ import UploadFileIcon from '@mui/icons-material/UploadFile';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { fileStorage } from '../../services/fileStorage';
|
import { fileStorage } from '../../services/fileStorage';
|
||||||
import { FileWithUrl } from '../../types/file';
|
import { FileWithUrl } from '../../types/file';
|
||||||
|
import { detectFileExtension } from '../../utils/fileUtils';
|
||||||
import FileGrid from './FileGrid';
|
import FileGrid from './FileGrid';
|
||||||
import MultiSelectControls from './MultiSelectControls';
|
import MultiSelectControls from './MultiSelectControls';
|
||||||
import { useFileManager } from '../../hooks/useFileManager';
|
import { useFileManager } from '../../hooks/useFileManager';
|
||||||
@ -20,6 +21,7 @@ interface FileUploadSelectorProps {
|
|||||||
onFileSelect?: (file: File) => void;
|
onFileSelect?: (file: File) => void;
|
||||||
onFilesSelect: (files: File[]) => void;
|
onFilesSelect: (files: File[]) => void;
|
||||||
accept?: string[];
|
accept?: string[];
|
||||||
|
supportedExtensions?: string[]; // Extensions this tool supports (e.g., ['pdf', 'jpg', 'png'])
|
||||||
|
|
||||||
// Loading state
|
// Loading state
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
@ -38,6 +40,7 @@ const FileUploadSelector = ({
|
|||||||
onFileSelect,
|
onFileSelect,
|
||||||
onFilesSelect,
|
onFilesSelect,
|
||||||
accept = ["application/pdf", "application/zip", "application/x-zip-compressed"],
|
accept = ["application/pdf", "application/zip", "application/x-zip-compressed"],
|
||||||
|
supportedExtensions = ["pdf"], // Default to PDF only for most tools
|
||||||
loading = false,
|
loading = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
showRecentFiles = true,
|
showRecentFiles = true,
|
||||||
@ -51,6 +54,12 @@ const FileUploadSelector = ({
|
|||||||
|
|
||||||
const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile, createFileSelectionHandlers } = useFileManager();
|
const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile, createFileSelectionHandlers } = useFileManager();
|
||||||
|
|
||||||
|
// Utility function to check if a file extension is supported
|
||||||
|
const isFileSupported = useCallback((fileName: string): boolean => {
|
||||||
|
const extension = detectFileExtension(fileName);
|
||||||
|
return extension ? supportedExtensions.includes(extension) : false;
|
||||||
|
}, [supportedExtensions]);
|
||||||
|
|
||||||
const refreshRecentFiles = useCallback(async () => {
|
const refreshRecentFiles = useCallback(async () => {
|
||||||
const files = await loadRecentFiles();
|
const files = await loadRecentFiles();
|
||||||
setRecentFiles(files);
|
setRecentFiles(files);
|
||||||
@ -155,6 +164,7 @@ const FileUploadSelector = ({
|
|||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
style={{ width: '100%', height: "5rem" }}
|
style={{ width: '100%', height: "5rem" }}
|
||||||
activateOnClick={true}
|
activateOnClick={true}
|
||||||
|
data-testid="file-dropzone"
|
||||||
>
|
>
|
||||||
<Center>
|
<Center>
|
||||||
<Stack align="center" gap="sm">
|
<Stack align="center" gap="sm">
|
||||||
@ -192,6 +202,7 @@ const FileUploadSelector = ({
|
|||||||
accept={accept.join(',')}
|
accept={accept.join(',')}
|
||||||
onChange={handleFileInputChange}
|
onChange={handleFileInputChange}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
|
data-testid="file-input"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
@ -225,6 +236,7 @@ const FileUploadSelector = ({
|
|||||||
selectedFiles={selectedFiles}
|
selectedFiles={selectedFiles}
|
||||||
showSearch={true}
|
showSearch={true}
|
||||||
showSort={true}
|
showSort={true}
|
||||||
|
isFileSupported={isFileSupported}
|
||||||
onDeleteAll={async () => {
|
onDeleteAll={async () => {
|
||||||
await Promise.all(recentFiles.map(async (file) => {
|
await Promise.all(recentFiles.map(async (file) => {
|
||||||
await fileStorage.deleteFile(file.id || file.name);
|
await fileStorage.deleteFile(file.id || file.name);
|
||||||
|
30
frontend/src/components/shared/LandingPage.tsx
Normal file
30
frontend/src/components/shared/LandingPage.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Container, Stack, Text, Button } from '@mantine/core';
|
||||||
|
import FolderIcon from '@mui/icons-material/FolderRounded';
|
||||||
|
import { useFilesModalContext } from '../../contexts/FilesModalContext';
|
||||||
|
|
||||||
|
interface LandingPageProps {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LandingPage = ({ title }: LandingPageProps) => {
|
||||||
|
const { openFilesModal } = useFilesModalContext();
|
||||||
|
return (
|
||||||
|
<Container size="lg" p="xl" h="100%" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<Stack align="center" gap="lg">
|
||||||
|
<Text size="xl" fw={500} c="dimmed">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
leftSection={<FolderIcon />}
|
||||||
|
size="lg"
|
||||||
|
onClick={openFilesModal}
|
||||||
|
>
|
||||||
|
Open Files
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LandingPage;
|
@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
/* Dark theme support */
|
/* Dark theme support */
|
||||||
[data-mantine-color-scheme="dark"] .languageItem {
|
[data-mantine-color-scheme="dark"] .languageItem {
|
||||||
border-right-color: var(--mantine-color-dark-4);
|
border-right-color: var(--mantine-color-dark-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-mantine-color-scheme="dark"] .languageItem:nth-child(4n) {
|
[data-mantine-color-scheme="dark"] .languageItem:nth-child(4n) {
|
||||||
@ -52,11 +52,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-mantine-color-scheme="dark"] .languageItem:nth-child(2n) {
|
[data-mantine-color-scheme="dark"] .languageItem:nth-child(2n) {
|
||||||
border-right-color: var(--mantine-color-dark-4);
|
border-right-color: var(--mantine-color-dark-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-mantine-color-scheme="dark"] .languageItem:nth-child(3n) {
|
[data-mantine-color-scheme="dark"] .languageItem:nth-child(3n) {
|
||||||
border-right-color: var(--mantine-color-dark-4);
|
border-right-color: var(--mantine-color-dark-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive text visibility */
|
/* Responsive text visibility */
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Menu, Button, ScrollArea, useMantineTheme, useMantineColorScheme } from '@mantine/core';
|
import { Menu, Button, ScrollArea } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { supportedLanguages } from '../../i18n';
|
import { supportedLanguages } from '../../i18n';
|
||||||
import LanguageIcon from '@mui/icons-material/Language';
|
import LanguageIcon from '@mui/icons-material/Language';
|
||||||
@ -7,8 +7,6 @@ import styles from './LanguageSelector.module.css';
|
|||||||
|
|
||||||
const LanguageSelector = () => {
|
const LanguageSelector = () => {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const theme = useMantineTheme();
|
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
const [animationTriggered, setAnimationTriggered] = useState(false);
|
const [animationTriggered, setAnimationTriggered] = useState(false);
|
||||||
const [isChanging, setIsChanging] = useState(false);
|
const [isChanging, setIsChanging] = useState(false);
|
||||||
@ -102,10 +100,10 @@ const LanguageSelector = () => {
|
|||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
border: 'none',
|
border: 'none',
|
||||||
color: colorScheme === 'dark' ? theme.colors.gray[3] : theme.colors.gray[7],
|
color: 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-1))',
|
||||||
transition: 'background-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
transition: 'background-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
@ -125,7 +123,8 @@ const LanguageSelector = () => {
|
|||||||
padding: '12px',
|
padding: '12px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||||||
border: colorScheme === 'dark' ? `1px solid ${theme.colors.dark[4]}` : `1px solid ${theme.colors.gray[3]}`,
|
backgroundColor: 'light-dark(var(--mantine-color-white), var(--mantine-color-dark-6))',
|
||||||
|
border: 'light-dark(1px solid var(--mantine-color-gray-3), 1px solid var(--mantine-color-dark-4))',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ScrollArea h={190} type="scroll">
|
<ScrollArea h={190} type="scroll">
|
||||||
@ -145,6 +144,7 @@ const LanguageSelector = () => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={(event) => handleLanguageChange(option.value, event)}
|
onClick={(event) => handleLanguageChange(option.value, event)}
|
||||||
|
data-selected={option.value === i18n.language}
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
@ -153,21 +153,17 @@ const LanguageSelector = () => {
|
|||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: option.value === i18n.language ? (
|
backgroundColor: option.value === i18n.language
|
||||||
colorScheme === 'dark' ? theme.colors.blue[8] : theme.colors.blue[1]
|
? 'light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))'
|
||||||
) : 'transparent',
|
: 'transparent',
|
||||||
color: option.value === i18n.language ? (
|
color: option.value === i18n.language
|
||||||
colorScheme === 'dark' ? theme.colors.blue[2] : theme.colors.blue[7]
|
? 'light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))'
|
||||||
) : (
|
: 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-white))',
|
||||||
colorScheme === 'dark' ? theme.colors.gray[3] : theme.colors.gray[7]
|
|
||||||
),
|
|
||||||
transition: 'all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
transition: 'all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: option.value === i18n.language ? (
|
backgroundColor: option.value === i18n.language
|
||||||
colorScheme === 'dark' ? theme.colors.blue[7] : theme.colors.blue[2]
|
? 'light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-7))'
|
||||||
) : (
|
: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
|
||||||
colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1]
|
|
||||||
),
|
|
||||||
transform: 'translateY(-1px)',
|
transform: 'translateY(-1px)',
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
}
|
}
|
||||||
@ -197,7 +193,7 @@ const LanguageSelector = () => {
|
|||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
backgroundColor: theme.colors.blue[4],
|
backgroundColor: 'var(--mantine-color-blue-4)',
|
||||||
opacity: 0.6,
|
opacity: 0.6,
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
animation: 'ripple-expand 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
animation: 'ripple-expand 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
|
@ -11,6 +11,7 @@ import { useRainbowThemeContext } from "./RainbowThemeProvider";
|
|||||||
import rainbowStyles from '../../styles/rainbow.module.css';
|
import rainbowStyles from '../../styles/rainbow.module.css';
|
||||||
import AppConfigModal from './AppConfigModal';
|
import AppConfigModal from './AppConfigModal';
|
||||||
import { useIsOverflowing } from '../../hooks/useIsOverflowing';
|
import { useIsOverflowing } from '../../hooks/useIsOverflowing';
|
||||||
|
import { useFilesModalContext } from '../../contexts/FilesModalContext';
|
||||||
import './QuickAccessBar.css';
|
import './QuickAccessBar.css';
|
||||||
|
|
||||||
interface QuickAccessBarProps {
|
interface QuickAccessBarProps {
|
||||||
@ -30,6 +31,7 @@ interface ButtonConfig {
|
|||||||
isRound?: boolean;
|
isRound?: boolean;
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
type?: 'navigation' | 'modal' | 'action'; // navigation = main nav, modal = triggers modal, action = other actions
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavHeader({
|
function NavHeader({
|
||||||
@ -111,11 +113,16 @@ const QuickAccessBar = ({
|
|||||||
readerMode,
|
readerMode,
|
||||||
}: QuickAccessBarProps) => {
|
}: QuickAccessBarProps) => {
|
||||||
const { isRainbowMode } = useRainbowThemeContext();
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
|
const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
|
||||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||||
const [activeButton, setActiveButton] = useState<string>('tools');
|
const [activeButton, setActiveButton] = useState<string>('tools');
|
||||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||||
const isOverflow = useIsOverflowing(scrollableRef);
|
const isOverflow = useIsOverflowing(scrollableRef);
|
||||||
|
|
||||||
|
const handleFilesButtonClick = () => {
|
||||||
|
openFilesModal();
|
||||||
|
};
|
||||||
|
|
||||||
const buttonConfigs: ButtonConfig[] = [
|
const buttonConfigs: ButtonConfig[] = [
|
||||||
{
|
{
|
||||||
id: 'read',
|
id: 'read',
|
||||||
@ -124,6 +131,7 @@ const QuickAccessBar = ({
|
|||||||
tooltip: 'Read documents',
|
tooltip: 'Read documents',
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
isRound: false,
|
isRound: false,
|
||||||
|
type: 'navigation',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setActiveButton('read');
|
setActiveButton('read');
|
||||||
onReaderToggle();
|
onReaderToggle();
|
||||||
@ -139,6 +147,7 @@ const QuickAccessBar = ({
|
|||||||
tooltip: 'Sign your document',
|
tooltip: 'Sign your document',
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
isRound: false,
|
isRound: false,
|
||||||
|
type: 'navigation',
|
||||||
onClick: () => setActiveButton('sign')
|
onClick: () => setActiveButton('sign')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -151,6 +160,7 @@ const QuickAccessBar = ({
|
|||||||
tooltip: 'Automate workflows',
|
tooltip: 'Automate workflows',
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
isRound: false,
|
isRound: false,
|
||||||
|
type: 'navigation',
|
||||||
onClick: () => setActiveButton('automate')
|
onClick: () => setActiveButton('automate')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -160,7 +170,8 @@ const QuickAccessBar = ({
|
|||||||
tooltip: 'Manage files',
|
tooltip: 'Manage files',
|
||||||
isRound: true,
|
isRound: true,
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
onClick: () => setActiveButton('files')
|
type: 'modal',
|
||||||
|
onClick: handleFilesButtonClick
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'activity',
|
id: 'activity',
|
||||||
@ -172,6 +183,7 @@ const QuickAccessBar = ({
|
|||||||
tooltip: 'View activity and analytics',
|
tooltip: 'View activity and analytics',
|
||||||
isRound: true,
|
isRound: true,
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
|
type: 'navigation',
|
||||||
onClick: () => setActiveButton('activity')
|
onClick: () => setActiveButton('activity')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -180,6 +192,7 @@ const QuickAccessBar = ({
|
|||||||
icon: <SettingsIcon sx={{ fontSize: "1rem" }} />,
|
icon: <SettingsIcon sx={{ fontSize: "1rem" }} />,
|
||||||
tooltip: 'Configure settings',
|
tooltip: 'Configure settings',
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
|
type: 'modal',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setConfigModalOpen(true);
|
setConfigModalOpen(true);
|
||||||
}
|
}
|
||||||
@ -193,8 +206,16 @@ const QuickAccessBar = ({
|
|||||||
return config.isRound ? CIRCULAR_BORDER_RADIUS : ROUND_BORDER_RADIUS;
|
return config.isRound ? CIRCULAR_BORDER_RADIUS : ROUND_BORDER_RADIUS;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isButtonActive = (config: ButtonConfig): boolean => {
|
||||||
|
return (
|
||||||
|
(config.type === 'navigation' && activeButton === config.id) ||
|
||||||
|
(config.type === 'modal' && config.id === 'files' && isFilesModalOpen) ||
|
||||||
|
(config.type === 'modal' && config.id === 'config' && configModalOpen)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const getButtonStyle = (config: ButtonConfig) => {
|
const getButtonStyle = (config: ButtonConfig) => {
|
||||||
const isActive = activeButton === config.id;
|
const isActive = isButtonActive(config);
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
return {
|
return {
|
||||||
@ -205,7 +226,7 @@ const QuickAccessBar = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inactive state - use consistent inactive colors
|
// Inactive state for all buttons
|
||||||
return {
|
return {
|
||||||
backgroundColor: 'var(--icon-inactive-bg)',
|
backgroundColor: 'var(--icon-inactive-bg)',
|
||||||
color: 'var(--icon-inactive-color)',
|
color: 'var(--icon-inactive-color)',
|
||||||
@ -260,13 +281,14 @@ const QuickAccessBar = ({
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={config.onClick}
|
onClick={config.onClick}
|
||||||
style={getButtonStyle(config)}
|
style={getButtonStyle(config)}
|
||||||
className={activeButton === config.id ? 'activeIconScale' : ''}
|
className={isButtonActive(config) ? 'activeIconScale' : ''}
|
||||||
|
data-testid={`${config.id}-button`}
|
||||||
>
|
>
|
||||||
<span className="iconContainer">
|
<span className="iconContainer">
|
||||||
{config.icon}
|
{config.icon}
|
||||||
</span>
|
</span>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<span className={`button-text ${activeButton === config.id ? 'active' : 'inactive'}`}>
|
<span className={`button-text ${isButtonActive(config) ? 'active' : 'inactive'}`}>
|
||||||
{config.name}
|
{config.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -287,30 +309,29 @@ const QuickAccessBar = ({
|
|||||||
<div className="spacer" />
|
<div className="spacer" />
|
||||||
|
|
||||||
{/* Config button at the bottom */}
|
{/* Config button at the bottom */}
|
||||||
<Tooltip label="Configure settings" position="right">
|
{buttonConfigs
|
||||||
<div className="flex flex-col items-center gap-1">
|
.filter(config => config.id === 'config')
|
||||||
<ActionIcon
|
.map(config => (
|
||||||
size="lg"
|
<Tooltip key={config.id} label={config.tooltip} position="right">
|
||||||
variant="subtle"
|
<div className="flex flex-col items-center gap-1">
|
||||||
onClick={() => {
|
<ActionIcon
|
||||||
setConfigModalOpen(true);
|
size={config.size || 'lg'}
|
||||||
}}
|
variant="subtle"
|
||||||
style={{
|
onClick={config.onClick}
|
||||||
backgroundColor: 'var(--icon-inactive-bg)',
|
style={getButtonStyle(config)}
|
||||||
color: 'var(--icon-inactive-color)',
|
className={isButtonActive(config) ? 'activeIconScale' : ''}
|
||||||
border: 'none',
|
data-testid={`${config.id}-button`}
|
||||||
borderRadius: '8px',
|
>
|
||||||
}}
|
<span className="iconContainer">
|
||||||
>
|
{config.icon}
|
||||||
<span className="iconContainer">
|
</span>
|
||||||
<SettingsIcon sx={{ fontSize: "1rem" }} />
|
</ActionIcon>
|
||||||
</span>
|
<span className={`button-text ${isButtonActive(config) ? 'active' : 'inactive'}`}>
|
||||||
</ActionIcon>
|
{config.name}
|
||||||
<span className="config-button-text">
|
</span>
|
||||||
Config
|
</div>
|
||||||
</span>
|
</Tooltip>
|
||||||
</div>
|
))}
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stack, Text, NumberInput, Checkbox } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
|
||||||
|
|
||||||
|
interface ConvertFromEmailSettingsProps {
|
||||||
|
parameters: ConvertParameters;
|
||||||
|
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConvertFromEmailSettings = ({
|
||||||
|
parameters,
|
||||||
|
onParameterChange,
|
||||||
|
disabled = false
|
||||||
|
}: ConvertFromEmailSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm" data-testid="email-settings">
|
||||||
|
<Text size="sm" fw={500}>{t("convert.emailOptions", "Email to PDF Options")}:</Text>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label={t("convert.includeAttachments", "Include email attachments")}
|
||||||
|
checked={parameters.emailOptions.includeAttachments}
|
||||||
|
onChange={(event) => onParameterChange('emailOptions', {
|
||||||
|
...parameters.emailOptions,
|
||||||
|
includeAttachments: event.currentTarget.checked
|
||||||
|
})}
|
||||||
|
disabled={disabled}
|
||||||
|
data-testid="include-attachments-checkbox"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{parameters.emailOptions.includeAttachments && (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text size="xs" fw={500}>{t("convert.maxAttachmentSize", "Maximum attachment size (MB)")}:</Text>
|
||||||
|
<NumberInput
|
||||||
|
value={parameters.emailOptions.maxAttachmentSizeMB}
|
||||||
|
onChange={(value) => onParameterChange('emailOptions', {
|
||||||
|
...parameters.emailOptions,
|
||||||
|
maxAttachmentSizeMB: Number(value) || 10
|
||||||
|
})}
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
disabled={disabled}
|
||||||
|
data-testid="max-attachment-size-input"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label={t("convert.includeAllRecipients", "Include CC and BCC recipients in header")}
|
||||||
|
checked={parameters.emailOptions.includeAllRecipients}
|
||||||
|
onChange={(event) => onParameterChange('emailOptions', {
|
||||||
|
...parameters.emailOptions,
|
||||||
|
includeAllRecipients: event.currentTarget.checked
|
||||||
|
})}
|
||||||
|
disabled={disabled}
|
||||||
|
data-testid="include-all-recipients-checkbox"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label={t("convert.downloadHtml", "Download HTML intermediate file instead of PDF")}
|
||||||
|
checked={parameters.emailOptions.downloadHtml}
|
||||||
|
onChange={(event) => onParameterChange('emailOptions', {
|
||||||
|
...parameters.emailOptions,
|
||||||
|
downloadHtml: event.currentTarget.checked
|
||||||
|
})}
|
||||||
|
disabled={disabled}
|
||||||
|
data-testid="download-html-checkbox"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConvertFromEmailSettings;
|
@ -0,0 +1,82 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Stack, Text, Select, Switch } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { COLOR_TYPES, FIT_OPTIONS } from "../../../constants/convertConstants";
|
||||||
|
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
||||||
|
|
||||||
|
interface ConvertFromImageSettingsProps {
|
||||||
|
parameters: ConvertParameters;
|
||||||
|
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConvertFromImageSettings = ({
|
||||||
|
parameters,
|
||||||
|
onParameterChange,
|
||||||
|
disabled = false
|
||||||
|
}: ConvertFromImageSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm" data-testid="pdf-options-section">
|
||||||
|
<Text size="sm" fw={500}>{t("convert.pdfOptions", "PDF Options")}:</Text>
|
||||||
|
<Select
|
||||||
|
data-testid="color-type-select"
|
||||||
|
label={t("convert.colorType", "Color Type")}
|
||||||
|
value={parameters.imageOptions.colorType}
|
||||||
|
onChange={(val) => val && onParameterChange('imageOptions', {
|
||||||
|
...parameters.imageOptions,
|
||||||
|
colorType: val as typeof COLOR_TYPES[keyof typeof COLOR_TYPES]
|
||||||
|
})}
|
||||||
|
data={[
|
||||||
|
{ value: COLOR_TYPES.COLOR, label: t("convert.color", "Color") },
|
||||||
|
{ value: COLOR_TYPES.GREYSCALE, label: t("convert.greyscale", "Greyscale") },
|
||||||
|
{ value: COLOR_TYPES.BLACK_WHITE, label: t("convert.blackwhite", "Black & White") },
|
||||||
|
]}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
data-testid="fit-option-select"
|
||||||
|
label={t("convert.fitOption", "Fit Option")}
|
||||||
|
value={parameters.imageOptions.fitOption}
|
||||||
|
onChange={(val) => val && onParameterChange('imageOptions', {
|
||||||
|
...parameters.imageOptions,
|
||||||
|
fitOption: val as typeof FIT_OPTIONS[keyof typeof FIT_OPTIONS]
|
||||||
|
})}
|
||||||
|
data={[
|
||||||
|
{ value: FIT_OPTIONS.MAINTAIN_ASPECT, label: t("convert.maintainAspectRatio", "Maintain Aspect Ratio") },
|
||||||
|
{ value: FIT_OPTIONS.FIT_PAGE, label: t("convert.fitDocumentToPage", "Fit Document to Page") },
|
||||||
|
{ value: FIT_OPTIONS.FILL_PAGE, label: t("convert.fillPage", "Fill Page") },
|
||||||
|
]}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
data-testid="auto-rotate-switch"
|
||||||
|
label={t("convert.autoRotate", "Auto Rotate")}
|
||||||
|
description={t("convert.autoRotateDescription", "Automatically rotate images to better fit the PDF page")}
|
||||||
|
checked={parameters.imageOptions.autoRotate}
|
||||||
|
onChange={(event) => onParameterChange('imageOptions', {
|
||||||
|
...parameters.imageOptions,
|
||||||
|
autoRotate: event.currentTarget.checked
|
||||||
|
})}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
data-testid="combine-images-switch"
|
||||||
|
label={t("convert.combineImages", "Combine Images")}
|
||||||
|
description={t("convert.combineImagesDescription", "Combine all images into one PDF, or create separate PDFs for each image")}
|
||||||
|
checked={parameters.imageOptions.combineImages}
|
||||||
|
onChange={(event) => onParameterChange('imageOptions', {
|
||||||
|
...parameters.imageOptions,
|
||||||
|
combineImages: event.currentTarget.checked
|
||||||
|
})}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConvertFromImageSettings;
|
@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stack, Text, NumberInput, Slider } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
|
||||||
|
|
||||||
|
interface ConvertFromWebSettingsProps {
|
||||||
|
parameters: ConvertParameters;
|
||||||
|
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConvertFromWebSettings = ({
|
||||||
|
parameters,
|
||||||
|
onParameterChange,
|
||||||
|
disabled = false
|
||||||
|
}: ConvertFromWebSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm" data-testid="web-settings">
|
||||||
|
<Text size="sm" fw={500}>{t("convert.webOptions", "Web to PDF Options")}:</Text>
|
||||||
|
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text size="xs" fw={500}>{t("convert.zoomLevel", "Zoom Level")}:</Text>
|
||||||
|
<NumberInput
|
||||||
|
value={parameters.htmlOptions.zoomLevel}
|
||||||
|
onChange={(value) => onParameterChange('htmlOptions', {
|
||||||
|
...parameters.htmlOptions,
|
||||||
|
zoomLevel: Number(value) || 1.0
|
||||||
|
})}
|
||||||
|
min={0.1}
|
||||||
|
max={3.0}
|
||||||
|
step={0.1}
|
||||||
|
precision={1}
|
||||||
|
disabled={disabled}
|
||||||
|
data-testid="zoom-level-input"
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
value={parameters.htmlOptions.zoomLevel}
|
||||||
|
onChange={(value) => onParameterChange('htmlOptions', {
|
||||||
|
...parameters.htmlOptions,
|
||||||
|
zoomLevel: value
|
||||||
|
})}
|
||||||
|
min={0.1}
|
||||||
|
max={3.0}
|
||||||
|
step={0.1}
|
||||||
|
disabled={disabled}
|
||||||
|
data-testid="zoom-level-slider"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConvertFromWebSettings;
|
318
frontend/src/components/tools/convert/ConvertSettings.tsx
Normal file
318
frontend/src/components/tools/convert/ConvertSettings.tsx
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { Stack, Text, Group, Divider, UnstyledButton, useMantineTheme, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useMultipleEndpointsEnabled } from "../../../hooks/useEndpointConfig";
|
||||||
|
import { isImageFormat, isWebFormat } from "../../../utils/convertUtils";
|
||||||
|
import { useFileSelectionActions } from "../../../contexts/FileSelectionContext";
|
||||||
|
import { useFileContext } from "../../../contexts/FileContext";
|
||||||
|
import { detectFileExtension } from "../../../utils/fileUtils";
|
||||||
|
import GroupedFormatDropdown from "./GroupedFormatDropdown";
|
||||||
|
import ConvertToImageSettings from "./ConvertToImageSettings";
|
||||||
|
import ConvertFromImageSettings from "./ConvertFromImageSettings";
|
||||||
|
import ConvertFromWebSettings from "./ConvertFromWebSettings";
|
||||||
|
import ConvertFromEmailSettings from "./ConvertFromEmailSettings";
|
||||||
|
import ConvertToPdfaSettings from "./ConvertToPdfaSettings";
|
||||||
|
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
||||||
|
import {
|
||||||
|
FROM_FORMAT_OPTIONS,
|
||||||
|
EXTENSION_TO_ENDPOINT,
|
||||||
|
COLOR_TYPES,
|
||||||
|
OUTPUT_OPTIONS,
|
||||||
|
FIT_OPTIONS
|
||||||
|
} from "../../../constants/convertConstants";
|
||||||
|
|
||||||
|
interface ConvertSettingsProps {
|
||||||
|
parameters: ConvertParameters;
|
||||||
|
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||||
|
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
|
||||||
|
selectedFiles: File[];
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConvertSettings = ({
|
||||||
|
parameters,
|
||||||
|
onParameterChange,
|
||||||
|
getAvailableToExtensions,
|
||||||
|
selectedFiles,
|
||||||
|
disabled = false
|
||||||
|
}: ConvertSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const { setSelectedFiles } = useFileSelectionActions();
|
||||||
|
const { activeFiles, setSelectedFiles: setContextSelectedFiles } = useFileContext();
|
||||||
|
|
||||||
|
const allEndpoints = useMemo(() => {
|
||||||
|
const endpoints = new Set<string>();
|
||||||
|
Object.values(EXTENSION_TO_ENDPOINT).forEach(toEndpoints => {
|
||||||
|
Object.values(toEndpoints).forEach(endpoint => {
|
||||||
|
endpoints.add(endpoint);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Array.from(endpoints);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { endpointStatus } = useMultipleEndpointsEnabled(allEndpoints);
|
||||||
|
|
||||||
|
const isConversionAvailable = (fromExt: string, toExt: string): boolean => {
|
||||||
|
const endpointKey = EXTENSION_TO_ENDPOINT[fromExt]?.[toExt];
|
||||||
|
if (!endpointKey) return false;
|
||||||
|
|
||||||
|
return endpointStatus[endpointKey] === true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Enhanced FROM options with endpoint availability
|
||||||
|
const enhancedFromOptions = useMemo(() => {
|
||||||
|
const baseOptions = FROM_FORMAT_OPTIONS.map(option => {
|
||||||
|
// Check if this source format has any available conversions
|
||||||
|
const availableConversions = getAvailableToExtensions(option.value) || [];
|
||||||
|
const hasAvailableConversions = availableConversions.some(targetOption =>
|
||||||
|
isConversionAvailable(option.value, targetOption.value)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...option,
|
||||||
|
enabled: hasAvailableConversions
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add dynamic format option if current selection is a file-<extension> format
|
||||||
|
if (parameters.fromExtension && parameters.fromExtension.startsWith('file-')) {
|
||||||
|
const extension = parameters.fromExtension.replace('file-', '');
|
||||||
|
const dynamicOption = {
|
||||||
|
value: parameters.fromExtension,
|
||||||
|
label: extension.toUpperCase(),
|
||||||
|
group: 'File',
|
||||||
|
enabled: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the dynamic option at the beginning
|
||||||
|
return [dynamicOption, ...baseOptions];
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseOptions;
|
||||||
|
}, [getAvailableToExtensions, endpointStatus, parameters.fromExtension]);
|
||||||
|
|
||||||
|
// Enhanced TO options with endpoint availability
|
||||||
|
const enhancedToOptions = useMemo(() => {
|
||||||
|
if (!parameters.fromExtension) return [];
|
||||||
|
|
||||||
|
const availableOptions = getAvailableToExtensions(parameters.fromExtension) || [];
|
||||||
|
return availableOptions.map(option => ({
|
||||||
|
...option,
|
||||||
|
enabled: isConversionAvailable(parameters.fromExtension, option.value)
|
||||||
|
}));
|
||||||
|
}, [parameters.fromExtension, getAvailableToExtensions, endpointStatus]);
|
||||||
|
|
||||||
|
const resetParametersToDefaults = () => {
|
||||||
|
onParameterChange('imageOptions', {
|
||||||
|
colorType: COLOR_TYPES.COLOR,
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
|
||||||
|
fitOption: FIT_OPTIONS.MAINTAIN_ASPECT,
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true,
|
||||||
|
});
|
||||||
|
onParameterChange('emailOptions', {
|
||||||
|
includeAttachments: true,
|
||||||
|
maxAttachmentSizeMB: 10,
|
||||||
|
downloadHtml: false,
|
||||||
|
includeAllRecipients: false,
|
||||||
|
});
|
||||||
|
onParameterChange('pdfaOptions', {
|
||||||
|
outputFormat: 'pdfa-1',
|
||||||
|
});
|
||||||
|
onParameterChange('isSmartDetection', false);
|
||||||
|
onParameterChange('smartDetectionType', 'none');
|
||||||
|
};
|
||||||
|
|
||||||
|
const setAutoTargetExtension = (fromExtension: string) => {
|
||||||
|
const availableToOptions = getAvailableToExtensions(fromExtension);
|
||||||
|
const autoTarget = availableToOptions.length === 1 ? availableToOptions[0].value : '';
|
||||||
|
onParameterChange('toExtension', autoTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterFilesByExtension = (extension: string) => {
|
||||||
|
return activeFiles.filter(file => {
|
||||||
|
const fileExtension = detectFileExtension(file.name);
|
||||||
|
|
||||||
|
if (extension === 'any') {
|
||||||
|
return true;
|
||||||
|
} else if (extension === 'image') {
|
||||||
|
return isImageFormat(fileExtension);
|
||||||
|
} else {
|
||||||
|
return fileExtension === extension;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFileSelection = (files: File[]) => {
|
||||||
|
setSelectedFiles(files);
|
||||||
|
const fileIds = files.map(file => (file as any).id || file.name);
|
||||||
|
setContextSelectedFiles(fileIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFromExtensionChange = (value: string) => {
|
||||||
|
onParameterChange('fromExtension', value);
|
||||||
|
setAutoTargetExtension(value);
|
||||||
|
resetParametersToDefaults();
|
||||||
|
|
||||||
|
if (activeFiles.length > 0) {
|
||||||
|
const matchingFiles = filterFilesByExtension(value);
|
||||||
|
updateFileSelection(matchingFiles);
|
||||||
|
} else {
|
||||||
|
updateFileSelection([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToExtensionChange = (value: string) => {
|
||||||
|
onParameterChange('toExtension', value);
|
||||||
|
onParameterChange('imageOptions', {
|
||||||
|
colorType: COLOR_TYPES.COLOR,
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
|
||||||
|
fitOption: FIT_OPTIONS.MAINTAIN_ASPECT,
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true,
|
||||||
|
});
|
||||||
|
onParameterChange('emailOptions', {
|
||||||
|
includeAttachments: true,
|
||||||
|
maxAttachmentSizeMB: 10,
|
||||||
|
downloadHtml: false,
|
||||||
|
includeAllRecipients: false,
|
||||||
|
});
|
||||||
|
onParameterChange('pdfaOptions', {
|
||||||
|
outputFormat: 'pdfa-1',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
|
||||||
|
{/* Format Selection */}
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{t("convert.convertFrom", "Convert from")}:
|
||||||
|
</Text>
|
||||||
|
<GroupedFormatDropdown
|
||||||
|
name="convert-from-dropdown"
|
||||||
|
data-testid="convert-from-dropdown"
|
||||||
|
value={parameters.fromExtension}
|
||||||
|
placeholder={t("convert.sourceFormatPlaceholder", "Source format")}
|
||||||
|
options={enhancedFromOptions}
|
||||||
|
onChange={handleFromExtensionChange}
|
||||||
|
disabled={disabled}
|
||||||
|
minWidth="18rem"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{t("convert.convertTo", "Convert to")}:
|
||||||
|
</Text>
|
||||||
|
{!parameters.fromExtension ? (
|
||||||
|
<UnstyledButton
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 0.75rem',
|
||||||
|
border: `0.0625rem solid ${theme.colors.gray[4]}`,
|
||||||
|
borderRadius: theme.radius.sm,
|
||||||
|
backgroundColor: colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
||||||
|
color: colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
|
||||||
|
cursor: 'not-allowed'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text size="sm">{t("convert.selectSourceFormatFirst", "Select a source format first")}</Text>
|
||||||
|
<KeyboardArrowDownIcon
|
||||||
|
style={{
|
||||||
|
fontSize: '1rem',
|
||||||
|
color: colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</UnstyledButton>
|
||||||
|
) : (
|
||||||
|
<GroupedFormatDropdown
|
||||||
|
name="convert-to-dropdown"
|
||||||
|
data-testid="convert-to-dropdown"
|
||||||
|
value={parameters.toExtension}
|
||||||
|
placeholder={t("convert.targetFormatPlaceholder", "Target format")}
|
||||||
|
options={enhancedToOptions}
|
||||||
|
onChange={handleToExtensionChange}
|
||||||
|
disabled={disabled}
|
||||||
|
minWidth="18rem"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Format-specific options */}
|
||||||
|
{isImageFormat(parameters.toExtension) && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<ConvertToImageSettings
|
||||||
|
parameters={parameters}
|
||||||
|
onParameterChange={onParameterChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
{/* Color options for image to PDF conversion */}
|
||||||
|
{(isImageFormat(parameters.fromExtension) && parameters.toExtension === 'pdf') ||
|
||||||
|
(parameters.isSmartDetection && parameters.smartDetectionType === 'images') ? (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<ConvertFromImageSettings
|
||||||
|
parameters={parameters}
|
||||||
|
onParameterChange={onParameterChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* Web to PDF options */}
|
||||||
|
{((isWebFormat(parameters.fromExtension) && parameters.toExtension === 'pdf') ||
|
||||||
|
(parameters.isSmartDetection && parameters.smartDetectionType === 'web')) ? (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<ConvertFromWebSettings
|
||||||
|
parameters={parameters}
|
||||||
|
onParameterChange={onParameterChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* Email to PDF options */}
|
||||||
|
{parameters.fromExtension === 'eml' && parameters.toExtension === 'pdf' && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<ConvertFromEmailSettings
|
||||||
|
parameters={parameters}
|
||||||
|
onParameterChange={onParameterChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* PDF to PDF/A options */}
|
||||||
|
{parameters.fromExtension === 'pdf' && parameters.toExtension === 'pdfa' && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<ConvertToPdfaSettings
|
||||||
|
parameters={parameters}
|
||||||
|
onParameterChange={onParameterChange}
|
||||||
|
selectedFiles={selectedFiles}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConvertSettings;
|
@ -0,0 +1,71 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Stack, Text, Select, NumberInput, Group } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { COLOR_TYPES, OUTPUT_OPTIONS } from "../../../constants/convertConstants";
|
||||||
|
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
||||||
|
|
||||||
|
interface ConvertToImageSettingsProps {
|
||||||
|
parameters: ConvertParameters;
|
||||||
|
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConvertToImageSettings = ({
|
||||||
|
parameters,
|
||||||
|
onParameterChange,
|
||||||
|
disabled = false
|
||||||
|
}: ConvertToImageSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm" data-testid="image-options-section">
|
||||||
|
<Text size="sm" fw={500} data-testid="image-options-title">{t("convert.imageOptions", "Image Options")}:</Text>
|
||||||
|
<Group grow>
|
||||||
|
<Select
|
||||||
|
data-testid="color-type-select"
|
||||||
|
label={t("convert.colorType", "Color Type")}
|
||||||
|
value={parameters.imageOptions.colorType}
|
||||||
|
onChange={(val) => val && onParameterChange('imageOptions', {
|
||||||
|
...parameters.imageOptions,
|
||||||
|
colorType: val as typeof COLOR_TYPES[keyof typeof COLOR_TYPES]
|
||||||
|
})}
|
||||||
|
data={[
|
||||||
|
{ value: COLOR_TYPES.COLOR, label: t("convert.color", "Color") },
|
||||||
|
{ value: COLOR_TYPES.GREYSCALE, label: t("convert.greyscale", "Greyscale") },
|
||||||
|
{ value: COLOR_TYPES.BLACK_WHITE, label: t("convert.blackwhite", "Black & White") },
|
||||||
|
]}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
data-testid="dpi-input"
|
||||||
|
label={t("convert.dpi", "DPI")}
|
||||||
|
value={parameters.imageOptions.dpi}
|
||||||
|
onChange={(val) => typeof val === 'number' && onParameterChange('imageOptions', {
|
||||||
|
...parameters.imageOptions,
|
||||||
|
dpi: val
|
||||||
|
})}
|
||||||
|
min={72}
|
||||||
|
max={600}
|
||||||
|
step={1}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Select
|
||||||
|
data-testid="output-type-select"
|
||||||
|
label={t("convert.output", "Output")}
|
||||||
|
value={parameters.imageOptions.singleOrMultiple}
|
||||||
|
onChange={(val) => val && onParameterChange('imageOptions', {
|
||||||
|
...parameters.imageOptions,
|
||||||
|
singleOrMultiple: val as typeof OUTPUT_OPTIONS[keyof typeof OUTPUT_OPTIONS]
|
||||||
|
})}
|
||||||
|
data={[
|
||||||
|
{ value: OUTPUT_OPTIONS.SINGLE, label: t("convert.single", "Single") },
|
||||||
|
{ value: OUTPUT_OPTIONS.MULTIPLE, label: t("convert.multiple", "Multiple") },
|
||||||
|
]}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConvertToImageSettings;
|
@ -0,0 +1,60 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stack, Text, Select, Alert } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
|
||||||
|
import { usePdfSignatureDetection } from '../../../hooks/usePdfSignatureDetection';
|
||||||
|
|
||||||
|
interface ConvertToPdfaSettingsProps {
|
||||||
|
parameters: ConvertParameters;
|
||||||
|
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||||
|
selectedFiles: File[];
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConvertToPdfaSettings = ({
|
||||||
|
parameters,
|
||||||
|
onParameterChange,
|
||||||
|
selectedFiles,
|
||||||
|
disabled = false
|
||||||
|
}: ConvertToPdfaSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { hasDigitalSignatures, isChecking } = usePdfSignatureDetection(selectedFiles);
|
||||||
|
|
||||||
|
const pdfaFormatOptions = [
|
||||||
|
{ value: 'pdfa-1', label: 'PDF/A-1b' },
|
||||||
|
{ value: 'pdfa', label: 'PDF/A-2b' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm" data-testid="pdfa-settings">
|
||||||
|
<Text size="sm" fw={500}>{t("convert.pdfaOptions", "PDF/A Options")}:</Text>
|
||||||
|
|
||||||
|
{hasDigitalSignatures && (
|
||||||
|
<Alert color="yellow" size="sm">
|
||||||
|
<Text size="sm">
|
||||||
|
{t("convert.pdfaDigitalSignatureWarning", "The PDF contains a digital signature. This will be removed in the next step.")}
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text size="xs" fw={500}>{t("convert.outputFormat", "Output Format")}:</Text>
|
||||||
|
<Select
|
||||||
|
value={parameters.pdfaOptions.outputFormat}
|
||||||
|
onChange={(value) => onParameterChange('pdfaOptions', {
|
||||||
|
...parameters.pdfaOptions,
|
||||||
|
outputFormat: value || 'pdfa-1'
|
||||||
|
})}
|
||||||
|
data={pdfaFormatOptions}
|
||||||
|
disabled={disabled || isChecking}
|
||||||
|
data-testid="pdfa-output-format-select"
|
||||||
|
/>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{t("convert.pdfaNote", "PDF/A-1b is more compatible, PDF/A-2b supports more features.")}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConvertToPdfaSettings;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user