mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 13:20:08 +03:00
Compare commits
20
Commits
v1.0.0
...
multiFileAsync
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f096297af | ||
|
|
624e04a783 | ||
|
|
bb9f1d4f8b | ||
|
|
0d967aa5ea | ||
|
|
f19ff6691d | ||
|
|
98b4763c96 | ||
|
|
2c9537f786 | ||
|
|
d17d10b240 | ||
|
|
a959e6eb2e | ||
|
|
74d3d7d2c7 | ||
|
|
6f982c4db4 | ||
|
|
ad52ef641f | ||
|
|
69bbb12ece | ||
|
|
2218f0bffa | ||
|
|
05c10d3a9f | ||
|
|
e0746b468e | ||
|
|
721cb8e0a3 | ||
|
|
6496015af7 | ||
|
|
df4539dc35 | ||
|
|
baeb1acb8c |
@@ -30,6 +30,7 @@ jobs:
|
||||
github.event.comment.user.login == 'sbplat' ||
|
||||
github.event.comment.user.login == 'reecebrowne' ||
|
||||
github.event.comment.user.login == 'DarioGii' ||
|
||||
github.event.comment.user.login == 'EthanHealy01' ||
|
||||
github.event.comment.user.login == 'ConnorYoh'
|
||||
)
|
||||
outputs:
|
||||
|
||||
@@ -27,21 +27,25 @@ jobs:
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
# Get version number
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
|
||||
# ✅ Get version from Gradle
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: |
|
||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
||||
VERSION=$(./gradlew printVersion --quiet | tail -1)
|
||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
# ✅ Get Mac-specific version from Gradle
|
||||
- name: Get version number mac
|
||||
id: versionNumberMac
|
||||
run: |
|
||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
||||
CURRENT_YEAR=$(date +'%Y')
|
||||
IFS='.' read -r -a VERSION_PARTS <<< "$VERSION"
|
||||
MAC_VERSION="$CURRENT_YEAR.${VERSION_PARTS[1]:-0}.${VERSION_PARTS[2]:-0}"
|
||||
echo "versionNumberMac=$MAC_VERSION" >> $GITHUB_OUTPUT
|
||||
VERSION_MAC=$(./gradlew printMacVersion --quiet | tail -1)
|
||||
echo "versionNumberMac=$VERSION_MAC" >> $GITHUB_OUTPUT
|
||||
|
||||
build-portable:
|
||||
needs: read_versions
|
||||
@@ -82,7 +86,7 @@ jobs:
|
||||
run: |
|
||||
mkdir ./binaries
|
||||
mv ./build/launch4j/Stirling-PDF.exe ./binaries/win-Stirling-PDF-portable-Server${{ matrix.file_suffix }}.exe
|
||||
mv ./build/libs/Stirling-PDF-${{ needs.read_versions.outputs.version }}.jar ./binaries/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||
mv ./stirling-pdf/build/libs/stirling-pdf-${{ needs.read_versions.outputs.version }}.jar ./binaries/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
|
||||
@@ -128,7 +128,7 @@ Stirling-PDF currently supports 40 languages!
|
||||
| English (English) (en_GB) |  |
|
||||
| English (US) (en_US) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
|
||||
+43
-9
@@ -15,6 +15,8 @@ plugins {
|
||||
|
||||
import com.github.jk1.license.render.*
|
||||
import org.gradle.internal.os.OperatingSystem
|
||||
import org.panteleyev.jpackage.ImageType
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.time.Year
|
||||
|
||||
@@ -43,9 +45,19 @@ bootJar {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
// Configure main class for the root project
|
||||
springBoot {
|
||||
mainClass = 'stirling.software.SPDF.SPDFApplication'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = 'https://build.shibboleth.net/maven/releases' }
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group = 'stirling.software'
|
||||
version = '1.0.0'
|
||||
version = '1.0.2'
|
||||
|
||||
configurations.configureEach {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
@@ -53,7 +65,6 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tasks.register('writeVersion') {
|
||||
def propsFile = file("$projectDir/common/src/main/resources/version.properties")
|
||||
def propsDir = propsFile.parentFile
|
||||
@@ -78,6 +89,10 @@ tasks.register('writeVersion') {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('createExe') {
|
||||
dependsOn(":stirling-pdf:bootJar")
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'java-library'
|
||||
@@ -90,9 +105,11 @@ subprojects {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
bootJar {
|
||||
enabled = false
|
||||
}
|
||||
if (project.name != "stirling-pdf") {
|
||||
bootJar {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@@ -194,6 +211,14 @@ openApi {
|
||||
waitTimeInSeconds = 60 // Increase the wait time to 60 seconds
|
||||
}
|
||||
|
||||
// Configure the forked spring boot run task to properly delegate to the stirling-pdf module
|
||||
tasks.named('forkedSpringBootRun') {
|
||||
dependsOn ':stirling-pdf:bootRun'
|
||||
doFirst {
|
||||
println "Delegating forkedSpringBootRun to :stirling-pdf:bootRun"
|
||||
}
|
||||
}
|
||||
|
||||
//0.11.5 to 2024.11.5
|
||||
static def getMacVersion(String version) {
|
||||
def currentYear = Year.now().getValue()
|
||||
@@ -202,6 +227,7 @@ static def getMacVersion(String version) {
|
||||
}
|
||||
|
||||
jpackage {
|
||||
dependsOn(":stirling-pdf:bootJar")
|
||||
input = layout.projectDirectory.dir("stirling-pdf/build/libs")
|
||||
destination = layout.projectDirectory.dir("build/jpackage")
|
||||
mainJar = "Stirling-PDF-${project.version}.jar"
|
||||
@@ -244,7 +270,7 @@ jpackage {
|
||||
winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates
|
||||
winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF"
|
||||
winUpdateUrl = "https://github.com/Stirling-Tools/Stirling-PDF/releases"
|
||||
type = "exe"
|
||||
type = ImageType.EXE
|
||||
installDir = "C:/Program Files/Stirling-PDF"
|
||||
}
|
||||
|
||||
@@ -252,7 +278,7 @@ jpackage {
|
||||
mac {
|
||||
appVersion = getMacVersion(project.version.toString())
|
||||
icon = layout.projectDirectory.file("stirling-pdf/src/main/resources/static/favicon.icns")
|
||||
type = "dmg"
|
||||
type = ImageType.DMG
|
||||
macPackageIdentifier = "Stirling PDF"
|
||||
macPackageName = "Stirling PDF"
|
||||
macAppCategory = "public.app-category.productivity"
|
||||
@@ -274,7 +300,7 @@ jpackage {
|
||||
linux {
|
||||
appVersion = project.version
|
||||
icon = layout.projectDirectory.file("stirling-pdf/src/main/resources/static/favicon.png")
|
||||
type = "deb" // Can also use "rpm" for Red Hat-based systems
|
||||
type = ImageType.DEB // Can also use "rpm" for Red Hat-based systems
|
||||
|
||||
// Debian package configuration
|
||||
//linuxPackageName = "stirlingpdf"
|
||||
@@ -445,7 +471,7 @@ launch4j {
|
||||
} else {
|
||||
headerType = "console"
|
||||
}
|
||||
jarTask = tasks.bootJar
|
||||
jarTask = project(":stirling-pdf").tasks.bootJar
|
||||
|
||||
errTitle="Encountered error, do you have Java 21?"
|
||||
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
|
||||
@@ -507,6 +533,14 @@ swaggerhubUpload {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':stirling-pdf')
|
||||
implementation project(':common')
|
||||
if (System.getenv('DISABLE_ADDITIONAL_FEATURES') != 'true'
|
||||
|| (project.hasProperty('DISABLE_ADDITIONAL_FEATURES')
|
||||
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') != 'true')) {
|
||||
implementation project(':proprietary')
|
||||
}
|
||||
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2'
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import java.util.Properties;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -21,6 +20,7 @@ import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
|
||||
import lombok.Getter;
|
||||
@@ -148,23 +148,10 @@ public class AppConfig {
|
||||
}
|
||||
|
||||
@Bean(name = "activeSecurity")
|
||||
public boolean activeSecurity() {
|
||||
String disableAdditionalFeatures = env.getProperty("DISABLE_ADDITIONAL_FEATURES");
|
||||
|
||||
if (disableAdditionalFeatures != null) {
|
||||
// DISABLE_ADDITIONAL_FEATURES=true means security OFF, so return false
|
||||
// DISABLE_ADDITIONAL_FEATURES=false means security ON, so return true
|
||||
return !Boolean.parseBoolean(disableAdditionalFeatures);
|
||||
}
|
||||
|
||||
return env.getProperty("DOCKER_ENABLE_SECURITY", Boolean.class, true);
|
||||
}
|
||||
|
||||
@Bean(name = "missingActiveSecurity")
|
||||
@ConditionalOnMissingClass(
|
||||
"stirling.software.proprietary.security.configuration.SecurityConfiguration")
|
||||
public boolean missingActiveSecurity() {
|
||||
return true;
|
||||
return ClassUtils.isPresent(
|
||||
"stirling.software.proprietary.security.configuration.SecurityConfiguration",
|
||||
this.getClass().getClassLoader());
|
||||
}
|
||||
|
||||
@Bean(name = "directoryFilter")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package stirling.software.common.model.job;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
@@ -26,14 +27,8 @@ public class JobResult {
|
||||
/** Error message if the job failed */
|
||||
private String error;
|
||||
|
||||
/** The file ID of the result file, if applicable */
|
||||
private String fileId;
|
||||
|
||||
/** Original file name, if applicable */
|
||||
private String originalFileName;
|
||||
|
||||
/** MIME type of the result, if applicable */
|
||||
private String contentType;
|
||||
/** List of result files for jobs that produce files */
|
||||
private List<ResultFile> resultFiles;
|
||||
|
||||
/** Time when the job was created */
|
||||
private LocalDateTime createdAt;
|
||||
@@ -64,21 +59,6 @@ public class JobResult {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this job as complete with a file result
|
||||
*
|
||||
* @param fileId The file ID of the result
|
||||
* @param originalFileName The original file name
|
||||
* @param contentType The content type of the file
|
||||
*/
|
||||
public void completeWithFile(String fileId, String originalFileName, String contentType) {
|
||||
this.complete = true;
|
||||
this.fileId = fileId;
|
||||
this.originalFileName = originalFileName;
|
||||
this.contentType = contentType;
|
||||
this.completedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this job as complete with a general result
|
||||
*
|
||||
@@ -101,6 +81,67 @@ public class JobResult {
|
||||
this.completedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this job as complete with multiple file results
|
||||
*
|
||||
* @param resultFiles The list of result files
|
||||
*/
|
||||
public void completeWithFiles(List<ResultFile> resultFiles) {
|
||||
this.complete = true;
|
||||
this.resultFiles = new ArrayList<>(resultFiles);
|
||||
this.completedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this job as complete with a single file result (convenience method)
|
||||
*
|
||||
* @param fileId The file ID of the result
|
||||
* @param fileName The file name
|
||||
* @param contentType The content type of the file
|
||||
* @param fileSize The size of the file in bytes
|
||||
*/
|
||||
public void completeWithSingleFile(
|
||||
String fileId, String fileName, String contentType, long fileSize) {
|
||||
ResultFile resultFile =
|
||||
ResultFile.builder()
|
||||
.fileId(fileId)
|
||||
.fileName(fileName)
|
||||
.contentType(contentType)
|
||||
.fileSize(fileSize)
|
||||
.build();
|
||||
completeWithFiles(List.of(resultFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this job has file results
|
||||
*
|
||||
* @return true if this job has file results, false otherwise
|
||||
*/
|
||||
public boolean hasFiles() {
|
||||
return resultFiles != null && !resultFiles.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this job has multiple file results
|
||||
*
|
||||
* @return true if this job has multiple file results, false otherwise
|
||||
*/
|
||||
public boolean hasMultipleFiles() {
|
||||
return resultFiles != null && resultFiles.size() > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all result files
|
||||
*
|
||||
* @return List of result files
|
||||
*/
|
||||
public List<ResultFile> getAllResultFiles() {
|
||||
if (resultFiles != null && !resultFiles.isEmpty()) {
|
||||
return Collections.unmodifiableList(resultFiles);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a note to this job
|
||||
*
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package stirling.software.common.model.job;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/** Represents a single file result from a job execution */
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ResultFile {
|
||||
|
||||
/** The file ID for accessing the file */
|
||||
private String fileId;
|
||||
|
||||
/** The original file name */
|
||||
private String fileName;
|
||||
|
||||
/** MIME type of the file */
|
||||
private String contentType;
|
||||
|
||||
/** Size of the file in bytes */
|
||||
private long fileSize;
|
||||
}
|
||||
@@ -131,14 +131,46 @@ public class FileStorage {
|
||||
return Files.exists(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of a file by its ID without loading the content into memory
|
||||
*
|
||||
* @param fileId The ID of the file
|
||||
* @return The size of the file in bytes
|
||||
* @throws IOException If the file doesn't exist or can't be read
|
||||
*/
|
||||
public long getFileSize(String fileId) throws IOException {
|
||||
Path filePath = getFilePath(fileId);
|
||||
|
||||
if (!Files.exists(filePath)) {
|
||||
throw new IOException("File not found with ID: " + fileId);
|
||||
}
|
||||
|
||||
return Files.size(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path for a file ID
|
||||
*
|
||||
* @param fileId The ID of the file
|
||||
* @return The path to the file
|
||||
* @throws IllegalArgumentException if fileId contains path traversal characters or resolves
|
||||
* outside base directory
|
||||
*/
|
||||
private Path getFilePath(String fileId) {
|
||||
return Path.of(tempDirPath).resolve(fileId);
|
||||
// Validate fileId to prevent path traversal
|
||||
if (fileId.contains("..") || fileId.contains("/") || fileId.contains("\\")) {
|
||||
throw new IllegalArgumentException("Invalid file ID");
|
||||
}
|
||||
|
||||
Path basePath = Path.of(tempDirPath).normalize().toAbsolutePath();
|
||||
Path resolvedPath = basePath.resolve(fileId).normalize();
|
||||
|
||||
// Ensure resolved path is within the base directory
|
||||
if (!resolvedPath.startsWith(basePath)) {
|
||||
throw new IllegalArgumentException("File ID resolves to an invalid path");
|
||||
}
|
||||
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import io.github.pixee.security.ZipSecurity;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
@@ -17,6 +27,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.job.JobResult;
|
||||
import stirling.software.common.model.job.JobStats;
|
||||
import stirling.software.common.model.job.ResultFile;
|
||||
|
||||
/** Manages async tasks and their results */
|
||||
@Service
|
||||
@@ -80,8 +91,53 @@ public class TaskManager {
|
||||
public void setFileResult(
|
||||
String jobId, String fileId, String originalFileName, String contentType) {
|
||||
JobResult jobResult = getOrCreateJobResult(jobId);
|
||||
jobResult.completeWithFile(fileId, originalFileName, contentType);
|
||||
log.debug("Set file result for job ID: {} with file ID: {}", jobId, fileId);
|
||||
|
||||
// Check if this is a ZIP file that should be extracted
|
||||
if (isZipFile(contentType, originalFileName)) {
|
||||
try {
|
||||
List<ResultFile> extractedFiles =
|
||||
extractZipToIndividualFiles(fileId, originalFileName);
|
||||
if (!extractedFiles.isEmpty()) {
|
||||
jobResult.completeWithFiles(extractedFiles);
|
||||
log.debug(
|
||||
"Set multiple file results for job ID: {} with {} files extracted from ZIP",
|
||||
jobId,
|
||||
extractedFiles.size());
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to extract ZIP file for job {}: {}. Falling back to single file result.",
|
||||
jobId,
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Handle as single file using new ResultFile approach
|
||||
try {
|
||||
long fileSize = fileStorage.getFileSize(fileId);
|
||||
jobResult.completeWithSingleFile(fileId, originalFileName, contentType, fileSize);
|
||||
log.debug("Set single file result for job ID: {} with file ID: {}", jobId, fileId);
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to get file size for job {}: {}. Using size 0.", jobId, e.getMessage());
|
||||
jobResult.completeWithSingleFile(fileId, originalFileName, contentType, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the result of a task as multiple files
|
||||
*
|
||||
* @param jobId The job ID
|
||||
* @param resultFiles The list of result files
|
||||
*/
|
||||
public void setMultipleFileResults(String jobId, List<ResultFile> resultFiles) {
|
||||
JobResult jobResult = getOrCreateJobResult(jobId);
|
||||
jobResult.completeWithFiles(resultFiles);
|
||||
log.debug(
|
||||
"Set multiple file results for job ID: {} with {} files",
|
||||
jobId,
|
||||
resultFiles.size());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,7 +160,7 @@ public class TaskManager {
|
||||
public void setComplete(String jobId) {
|
||||
JobResult jobResult = getOrCreateJobResult(jobId);
|
||||
if (jobResult.getResult() == null
|
||||
&& jobResult.getFileId() == null
|
||||
&& !jobResult.hasFiles()
|
||||
&& jobResult.getError() == null) {
|
||||
// If no result or error has been set, mark it as complete with an empty result
|
||||
jobResult.completeWithResult("Task completed successfully");
|
||||
@@ -186,7 +242,7 @@ public class TaskManager {
|
||||
failedJobs++;
|
||||
} else {
|
||||
successfulJobs++;
|
||||
if (result.getFileId() != null) {
|
||||
if (result.hasFiles()) {
|
||||
fileResultJobs++;
|
||||
}
|
||||
}
|
||||
@@ -250,17 +306,8 @@ public class TaskManager {
|
||||
&& result.getCompletedAt() != null
|
||||
&& result.getCompletedAt().isBefore(expiryThreshold)) {
|
||||
|
||||
// If the job has a file result, delete the file
|
||||
if (result.getFileId() != null) {
|
||||
try {
|
||||
fileStorage.deleteFile(result.getFileId());
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to delete file for job {}: {}",
|
||||
entry.getKey(),
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
// Clean up file results
|
||||
cleanupJobFiles(result, entry.getKey());
|
||||
|
||||
// Remove the job result
|
||||
jobResults.remove(entry.getKey());
|
||||
@@ -290,4 +337,128 @@ public class TaskManager {
|
||||
cleanupExecutor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if a file is a ZIP file based on content type and filename */
|
||||
private boolean isZipFile(String contentType, String fileName) {
|
||||
if (contentType != null
|
||||
&& (contentType.equals("application/zip")
|
||||
|| contentType.equals("application/x-zip-compressed"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fileName != null && fileName.toLowerCase().endsWith(".zip")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Extract a ZIP file into individual files and store them */
|
||||
private List<ResultFile> extractZipToIndividualFiles(
|
||||
String zipFileId, String originalZipFileName) throws IOException {
|
||||
List<ResultFile> extractedFiles = new ArrayList<>();
|
||||
|
||||
MultipartFile zipFile = fileStorage.retrieveFile(zipFileId);
|
||||
|
||||
try (ZipInputStream zipIn =
|
||||
ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(zipFile.getBytes()))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zipIn.getNextEntry()) != null) {
|
||||
if (!entry.isDirectory()) {
|
||||
// Use buffered reading for memory safety
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = zipIn.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, bytesRead);
|
||||
}
|
||||
byte[] fileContent = out.toByteArray();
|
||||
|
||||
String contentType = determineContentType(entry.getName());
|
||||
String individualFileId = fileStorage.storeBytes(fileContent, entry.getName());
|
||||
|
||||
ResultFile resultFile =
|
||||
ResultFile.builder()
|
||||
.fileId(individualFileId)
|
||||
.fileName(entry.getName())
|
||||
.contentType(contentType)
|
||||
.fileSize(fileContent.length)
|
||||
.build();
|
||||
|
||||
extractedFiles.add(resultFile);
|
||||
log.debug(
|
||||
"Extracted file: {} (size: {} bytes)",
|
||||
entry.getName(),
|
||||
fileContent.length);
|
||||
}
|
||||
zipIn.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the original ZIP file after extraction
|
||||
try {
|
||||
fileStorage.deleteFile(zipFileId);
|
||||
log.debug("Cleaned up original ZIP file: {}", zipFileId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to clean up original ZIP file {}: {}", zipFileId, e.getMessage());
|
||||
}
|
||||
|
||||
return extractedFiles;
|
||||
}
|
||||
|
||||
/** Determine content type based on file extension */
|
||||
private String determineContentType(String fileName) {
|
||||
if (fileName == null) {
|
||||
return MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
}
|
||||
|
||||
String lowerName = fileName.toLowerCase();
|
||||
if (lowerName.endsWith(".pdf")) {
|
||||
return MediaType.APPLICATION_PDF_VALUE;
|
||||
} else if (lowerName.endsWith(".txt")) {
|
||||
return MediaType.TEXT_PLAIN_VALUE;
|
||||
} else if (lowerName.endsWith(".json")) {
|
||||
return MediaType.APPLICATION_JSON_VALUE;
|
||||
} else if (lowerName.endsWith(".xml")) {
|
||||
return MediaType.APPLICATION_XML_VALUE;
|
||||
} else if (lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg")) {
|
||||
return MediaType.IMAGE_JPEG_VALUE;
|
||||
} else if (lowerName.endsWith(".png")) {
|
||||
return MediaType.IMAGE_PNG_VALUE;
|
||||
} else {
|
||||
return MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
/** Clean up files associated with a job result */
|
||||
private void cleanupJobFiles(JobResult result, String jobId) {
|
||||
// Clean up all result files
|
||||
if (result.hasFiles()) {
|
||||
for (ResultFile resultFile : result.getAllResultFiles()) {
|
||||
try {
|
||||
fileStorage.deleteFile(resultFile.getFileId());
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to delete file {} for job {}: {}",
|
||||
resultFile.getFileId(),
|
||||
jobId,
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Find the ResultFile metadata for a given file ID by searching through all job results */
|
||||
public ResultFile findResultFileByFileId(String fileId) {
|
||||
for (JobResult jobResult : jobResults.values()) {
|
||||
if (jobResult.hasFiles()) {
|
||||
for (ResultFile resultFile : jobResult.getAllResultFiles()) {
|
||||
if (fileId.equals(resultFile.getFileId())) {
|
||||
return resultFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import stirling.software.common.model.job.JobResult;
|
||||
import stirling.software.common.model.job.JobStats;
|
||||
import stirling.software.common.model.job.ResultFile;
|
||||
|
||||
class TaskManagerTest {
|
||||
|
||||
@@ -73,13 +74,17 @@ class TaskManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetFileResult() {
|
||||
void testSetFileResult() throws Exception {
|
||||
// Arrange
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
taskManager.createTask(jobId);
|
||||
String fileId = "file-id";
|
||||
String originalFileName = "test.pdf";
|
||||
String contentType = "application/pdf";
|
||||
long fileSize = 1024L;
|
||||
|
||||
// Mock the fileStorage.getFileSize() call
|
||||
when(fileStorage.getFileSize(fileId)).thenReturn(fileSize);
|
||||
|
||||
// Act
|
||||
taskManager.setFileResult(jobId, fileId, originalFileName, contentType);
|
||||
@@ -88,9 +93,17 @@ class TaskManagerTest {
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isComplete());
|
||||
assertEquals(fileId, result.getFileId());
|
||||
assertEquals(originalFileName, result.getOriginalFileName());
|
||||
assertEquals(contentType, result.getContentType());
|
||||
assertTrue(result.hasFiles());
|
||||
assertFalse(result.hasMultipleFiles());
|
||||
|
||||
var resultFiles = result.getAllResultFiles();
|
||||
assertEquals(1, resultFiles.size());
|
||||
|
||||
ResultFile resultFile = resultFiles.get(0);
|
||||
assertEquals(fileId, resultFile.getFileId());
|
||||
assertEquals(originalFileName, resultFile.getFileName());
|
||||
assertEquals(contentType, resultFile.getContentType());
|
||||
assertEquals(fileSize, resultFile.getFileSize());
|
||||
assertNotNull(result.getCompletedAt());
|
||||
}
|
||||
|
||||
@@ -163,8 +176,11 @@ class TaskManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetJobStats() {
|
||||
void testGetJobStats() throws Exception {
|
||||
// Arrange
|
||||
// Mock fileStorage.getFileSize for file operations
|
||||
when(fileStorage.getFileSize("file-id")).thenReturn(1024L);
|
||||
|
||||
// 1. Create active job
|
||||
String activeJobId = "active-job";
|
||||
taskManager.createTask(activeJobId);
|
||||
@@ -216,9 +232,15 @@ class TaskManagerTest {
|
||||
LocalDateTime oldTime = LocalDateTime.now().minusHours(1);
|
||||
ReflectionTestUtils.setField(oldJob, "completedAt", oldTime);
|
||||
ReflectionTestUtils.setField(oldJob, "complete", true);
|
||||
ReflectionTestUtils.setField(oldJob, "fileId", "file-id");
|
||||
ReflectionTestUtils.setField(oldJob, "originalFileName", "test.pdf");
|
||||
ReflectionTestUtils.setField(oldJob, "contentType", "application/pdf");
|
||||
|
||||
// Create a ResultFile and set it using the new approach
|
||||
ResultFile resultFile = ResultFile.builder()
|
||||
.fileId("file-id")
|
||||
.fileName("test.pdf")
|
||||
.contentType("application/pdf")
|
||||
.fileSize(1024L)
|
||||
.build();
|
||||
ReflectionTestUtils.setField(oldJob, "resultFiles", java.util.List.of(resultFile));
|
||||
|
||||
when(fileStorage.deleteFile("file-id")).thenReturn(true);
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ services:
|
||||
- ./stirling/latest/config:/configs:rw
|
||||
- ./stirling/latest/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-US
|
||||
UI_APPNAME: Stirling-PDF-Ultra-lite
|
||||
|
||||
@@ -18,7 +18,6 @@ services:
|
||||
- ./stirling/latest/config:/configs:rw
|
||||
- ./stirling/latest/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
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
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
echo "Running Stirling PDF with DISABLE_ADDITIONAL_FEATURES=${DISABLE_ADDITIONAL_FEATURES} and VERSION_TAG=${VERSION_TAG}"
|
||||
# Check for DISABLE_ADDITIONAL_FEATURES and download the appropriate JAR if required
|
||||
if [ "$DISABLE_ADDITIONAL_FEATURES" = "false" ] && [ "$VERSION_TAG" != "alpha" ]; then
|
||||
if [ ! -f app-security.jar ]; then
|
||||
echo "Trying to download from: https://files.stirlingpdf.com/v$VERSION_TAG/Stirling-PDF-with-login.jar"
|
||||
curl -L -o app-security.jar https://files.stirlingpdf.com/v$VERSION_TAG/Stirling-PDF-with-login.jar
|
||||
if [ "$VERSION_TAG" != "alpha" ] && [ "$VERSION_TAG" != "ALPHA" ]; then
|
||||
if [ "$DISABLE_ADDITIONAL_FEATURES" = "false" ] || [ "$DISABLE_ADDITIONAL_FEATURES" = "FALSE" ] || [ "$DOCKER_ENABLE_SECURITY" = "true" ] || [ "$DOCKER_ENABLE_SECURITY" = "TRUE" ]; then
|
||||
if [ ! -f app-security.jar ]; then
|
||||
echo "Trying to download from: https://files.stirlingpdf.com/v$VERSION_TAG/Stirling-PDF-with-login.jar"
|
||||
curl -L -o app-security.jar https://files.stirlingpdf.com/v$VERSION_TAG/Stirling-PDF-with-login.jar
|
||||
|
||||
# If the first download attempt failed, try without the 'v' prefix
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Trying to download from: https://files.stirlingpdf.com/$VERSION_TAG/Stirling-PDF-with-login.jar"
|
||||
curl -L -o app-security.jar https://files.stirlingpdf.com/$VERSION_TAG/Stirling-PDF-with-login.jar
|
||||
fi
|
||||
# If the first download attempt failed, try without the 'v' prefix
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Trying to download from: https://files.stirlingpdf.com/$VERSION_TAG/Stirling-PDF-with-login.jar"
|
||||
curl -L -o app-security.jar https://files.stirlingpdf.com/$VERSION_TAG/Stirling-PDF-with-login.jar
|
||||
fi
|
||||
|
||||
if [ $? -eq 0 ]; then # checks if curl was successful
|
||||
rm -f app.jar
|
||||
ln -s app-security.jar app.jar
|
||||
chown stirlingpdfuser:stirlingpdfgroup app.jar || true
|
||||
chmod 755 app.jar || true
|
||||
if [ $? -eq 0 ]; then # checks if curl was successful
|
||||
rm -f app.jar
|
||||
ln -s app-security.jar app.jar
|
||||
chown stirlingpdfuser:stirlingpdfgroup app.jar || true
|
||||
chmod 755 app.jar || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -195,6 +195,7 @@ ignore = [
|
||||
'PDFToBook.selectText.1',
|
||||
'PDFToText.tags',
|
||||
'addPageNumbers.selectText.3',
|
||||
'adminUserSettings.team',
|
||||
'alphabet',
|
||||
'audit.dashboard.modal.id',
|
||||
'audit.dashboard.status',
|
||||
@@ -206,6 +207,7 @@ ignore = [
|
||||
'cookieBanner.popUp.acceptAllBtn',
|
||||
'endpointStatistics.top10',
|
||||
'endpointStatistics.top20',
|
||||
'fakeScan.quality.medium',
|
||||
'fileChooser.dragAndDrop',
|
||||
'home.pipeline.title',
|
||||
'lang.afr',
|
||||
@@ -241,6 +243,7 @@ ignore = [
|
||||
'pro',
|
||||
'redact.zoom',
|
||||
'sponsor',
|
||||
'team.status',
|
||||
'text',
|
||||
'validateSignature.cert.bits',
|
||||
'validateSignature.cert.version',
|
||||
|
||||
@@ -146,5 +146,10 @@ bootJar {
|
||||
}
|
||||
}
|
||||
|
||||
// Configure main class for Spring Boot
|
||||
springBoot {
|
||||
mainClass = 'stirling.software.SPDF.SPDFApplication'
|
||||
}
|
||||
|
||||
bootJar.dependsOn ':common:jar'
|
||||
bootJar.dependsOn ':proprietary:jar'
|
||||
|
||||
@@ -26,8 +26,6 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations(
|
||||
"file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
|
||||
registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/");
|
||||
registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/");
|
||||
// .setCachePeriod(0); // Optional: disable caching
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package stirling.software.common.controller;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -14,6 +18,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.job.JobResult;
|
||||
import stirling.software.common.model.job.ResultFile;
|
||||
import stirling.software.common.service.FileStorage;
|
||||
import stirling.software.common.service.JobQueue;
|
||||
import stirling.software.common.service.TaskManager;
|
||||
@@ -78,16 +83,31 @@ public class JobController {
|
||||
return ResponseEntity.badRequest().body("Job failed: " + result.getError());
|
||||
}
|
||||
|
||||
if (result.getFileId() != null) {
|
||||
// Handle multiple files - return metadata for client to download individually
|
||||
if (result.hasMultipleFiles()) {
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(
|
||||
Map.of(
|
||||
"jobId",
|
||||
jobId,
|
||||
"hasMultipleFiles",
|
||||
true,
|
||||
"files",
|
||||
result.getAllResultFiles()));
|
||||
}
|
||||
|
||||
// Handle single file (download directly)
|
||||
if (result.hasFiles() && !result.hasMultipleFiles()) {
|
||||
try {
|
||||
byte[] fileContent = fileStorage.retrieveBytes(result.getFileId());
|
||||
List<ResultFile> files = result.getAllResultFiles();
|
||||
ResultFile singleFile = files.get(0);
|
||||
byte[] fileContent = fileStorage.retrieveBytes(singleFile.getFileId());
|
||||
return ResponseEntity.ok()
|
||||
.header("Content-Type", result.getContentType())
|
||||
.header("Content-Type", singleFile.getContentType())
|
||||
.header(
|
||||
"Content-Disposition",
|
||||
"form-data; name=\"attachment\"; filename=\""
|
||||
+ result.getOriginalFileName()
|
||||
+ "\"")
|
||||
createContentDispositionHeader(singleFile.getFileName()))
|
||||
.body(fileContent);
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrieving file for job {}: {}", jobId, e.getMessage(), e);
|
||||
@@ -170,4 +190,127 @@ public class JobController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of files for a job
|
||||
*
|
||||
* @param jobId The job ID
|
||||
* @return List of files for the job
|
||||
*/
|
||||
@GetMapping("/api/v1/general/job/{jobId}/result/files")
|
||||
public ResponseEntity<?> getJobFiles(@PathVariable("jobId") String jobId) {
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
if (result == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
if (!result.isComplete()) {
|
||||
return ResponseEntity.badRequest().body("Job is not complete yet");
|
||||
}
|
||||
|
||||
if (result.getError() != null) {
|
||||
return ResponseEntity.badRequest().body("Job failed: " + result.getError());
|
||||
}
|
||||
|
||||
List<ResultFile> files = result.getAllResultFiles();
|
||||
return ResponseEntity.ok(
|
||||
Map.of(
|
||||
"jobId", jobId,
|
||||
"fileCount", files.size(),
|
||||
"files", files));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata for an individual file by its file ID
|
||||
*
|
||||
* @param fileId The file ID
|
||||
* @return The file metadata
|
||||
*/
|
||||
@GetMapping("/api/v1/general/files/{fileId}/metadata")
|
||||
public ResponseEntity<?> getFileMetadata(@PathVariable("fileId") String fileId) {
|
||||
try {
|
||||
// Verify file exists
|
||||
if (!fileStorage.fileExists(fileId)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// Find the file metadata from any job that contains this file
|
||||
ResultFile resultFile = taskManager.findResultFileByFileId(fileId);
|
||||
|
||||
if (resultFile != null) {
|
||||
return ResponseEntity.ok(resultFile);
|
||||
} else {
|
||||
// File exists but no metadata found, get basic info efficiently
|
||||
long fileSize = fileStorage.getFileSize(fileId);
|
||||
return ResponseEntity.ok(
|
||||
Map.of(
|
||||
"fileId",
|
||||
fileId,
|
||||
"fileName",
|
||||
"unknown",
|
||||
"contentType",
|
||||
"application/octet-stream",
|
||||
"fileSize",
|
||||
fileSize));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrieving file metadata {}: {}", fileId, e.getMessage(), e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body("Error retrieving file metadata: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download an individual file by its file ID
|
||||
*
|
||||
* @param fileId The file ID
|
||||
* @return The file content
|
||||
*/
|
||||
@GetMapping("/api/v1/general/files/{fileId}")
|
||||
public ResponseEntity<?> downloadFile(@PathVariable("fileId") String fileId) {
|
||||
try {
|
||||
// Verify file exists
|
||||
if (!fileStorage.fileExists(fileId)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// Retrieve file content
|
||||
byte[] fileContent = fileStorage.retrieveBytes(fileId);
|
||||
|
||||
// Find the file metadata from any job that contains this file
|
||||
// This is for getting the original filename and content type
|
||||
ResultFile resultFile = taskManager.findResultFileByFileId(fileId);
|
||||
|
||||
String fileName = resultFile != null ? resultFile.getFileName() : "download";
|
||||
String contentType =
|
||||
resultFile != null ? resultFile.getContentType() : "application/octet-stream";
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header("Content-Type", contentType)
|
||||
.header("Content-Disposition", createContentDispositionHeader(fileName))
|
||||
.body(fileContent);
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrieving file {}: {}", fileId, e.getMessage(), e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body("Error retrieving file: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Content-Disposition header with UTF-8 filename support
|
||||
*
|
||||
* @param fileName The filename to encode
|
||||
* @return Content-Disposition header value
|
||||
*/
|
||||
private String createContentDispositionHeader(String fileName) {
|
||||
try {
|
||||
String encodedFileName =
|
||||
URLEncoder.encode(fileName, StandardCharsets.UTF_8)
|
||||
.replace("+", "%20"); // URLEncoder uses + for spaces, but we want %20
|
||||
return "attachment; filename=\"" + fileName + "\"; filename*=UTF-8''" + encodedFileName;
|
||||
} catch (Exception e) {
|
||||
// Fallback to basic filename if encoding fails
|
||||
return "attachment; filename=\"" + fileName + "\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -170,9 +170,9 @@ sizes.medium=中型尺寸
|
||||
sizes.large=大型尺寸
|
||||
sizes.x-large=超大型尺寸
|
||||
error.pdfPassword=PDF文档有密码,未提供密码或密码不正确
|
||||
error.pdfCorrupted=PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.
|
||||
error.pdfCorruptedMultiple=One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them.
|
||||
error.pdfCorruptedDuring=Error {0}: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.
|
||||
error.pdfCorrupted=PDF文件似乎已损坏或损坏。在继续此操作之前,请先尝试使用“修复PDF”功能修复文件。
|
||||
error.pdfCorruptedMultiple=一个或多个PDF文件似乎已损坏或损坏。请先对单个文件使用“修复PDF”功能,然后再尝试合并它们。
|
||||
error.pdfCorruptedDuring=错误 {0} :PDF文件似乎已损坏或损坏。在继续此操作之前,请先尝试使用“修复PDF”功能修复文件。
|
||||
|
||||
# Frontend corruption error messages
|
||||
error.pdfInvalid=The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the 'Repair PDF' feature to fix the file before proceeding.
|
||||
@@ -183,54 +183,54 @@ error.pdfEncryption=The PDF appears to have corrupted encryption data. This can
|
||||
error.fileProcessing=An error occurred while processing the file during {0} operation: {1}
|
||||
|
||||
# Generic error message templates
|
||||
error.toolNotInstalled={0} is not installed
|
||||
error.toolRequired={0} is required for {1}
|
||||
error.conversionFailed={0} conversion failed
|
||||
error.commandFailed={0} command failed
|
||||
error.toolNotInstalled={0} 未被安装
|
||||
error.toolRequired={1} 依赖于 {0}
|
||||
error.conversionFailed={0} 转换失败
|
||||
error.commandFailed={0} 执行命令失败
|
||||
error.algorithmNotAvailable={0} algorithm not available
|
||||
error.optionsNotSpecified={0} options are not specified
|
||||
error.fileFormatRequired=File must be in {0} format
|
||||
error.invalidFormat=Invalid {0} format: {1}
|
||||
error.endpointDisabled=This endpoint has been disabled by the admin
|
||||
error.urlNotReachable=URL is not reachable, please provide a valid URL
|
||||
error.endpointDisabled=该端点被管理员禁用
|
||||
error.urlNotReachable=URL无法访问,请提供有效的URL
|
||||
|
||||
# DPI and image rendering messages - used by frontend for dynamic translation
|
||||
# Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message
|
||||
# Frontend parses this and replaces with localized versions using these keys
|
||||
error.dpiExceedsLimit=DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value.
|
||||
error.pageTooBigForDpi=PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).
|
||||
error.pageTooBigExceedsArray=PDF page {0} is too large to render at {1} DPI. The resulting image would exceed Java's maximum array size. Please try a lower DPI value (recommended: 150 or less).
|
||||
error.pageTooBigFor300Dpi=PDF page {0} is too large to render at 300 DPI. The resulting image would exceed Java's maximum array size. Please use a lower DPI value for PDF-to-image conversion.
|
||||
error.dpiExceedsLimit=DPI值 {0} 超过 {1} 的最大安全限值。高DPI值可能导致内存溢出或崩溃。请尝试较低的DPI值。
|
||||
error.pageTooBigForDpi=PDF页面 {0} 太大,无法以 {1} DPI呈现。请尝试较低的DPI值(建议:150或更低)。
|
||||
error.pageTooBigExceedsArray=PDF页面 {0} 太大,无法以 {1} DPI呈现。生成的图像将超过Java的最大数组大小。请尝试较低的DPI值(建议:150或更低)。
|
||||
error.pageTooBigFor300Dpi=PDF页面 {0} 太大,无法以300 DPI呈现。生成的图像将超过Java的最大数组大小。请使用较低的DPI值进行pdf到图像的转换。
|
||||
|
||||
# URL and website conversion messages
|
||||
|
||||
# System requirements messages
|
||||
|
||||
# Authentication and security messages
|
||||
error.apiKeyInvalid=API key is not valid.
|
||||
error.userNotFound=User not found.
|
||||
error.passwordRequired=Password must not be null.
|
||||
error.accountLocked=Your account has been locked due to too many failed login attempts.
|
||||
error.invalidEmail=Invalid email addresses provided.
|
||||
error.emailAttachmentRequired=An attachment is required to send the email.
|
||||
error.signatureNotFound=Signature file not found.
|
||||
error.apiKeyInvalid=API秘钥无效。
|
||||
error.userNotFound=用户未找到。
|
||||
error.passwordRequired=密码不应为空。
|
||||
error.accountLocked=由于过多的失败登录尝试,你的账户被锁定。
|
||||
error.invalidEmail=提供了无效的电子邮件地址。
|
||||
error.emailAttachmentRequired=发送该电子邮件需要附件。
|
||||
error.signatureNotFound=证书文件未找到。
|
||||
|
||||
# File processing messages
|
||||
error.fileNotFound=File not found with ID: {0}
|
||||
error.fileNotFound=具有此ID的文件未找到: {0}
|
||||
|
||||
# Database and configuration messages
|
||||
error.noBackupScripts=No backup scripts were found.
|
||||
error.unsupportedProvider={0} is not currently supported.
|
||||
error.pathTraversalDetected=Path traversal detected for security reasons.
|
||||
error.noBackupScripts=未找到备份脚本。
|
||||
error.unsupportedProvider={0} 当前不被支持。
|
||||
error.pathTraversalDetected=检测到存在安全问题的路径遍历操作。
|
||||
|
||||
# Validation messages
|
||||
error.invalidArgument=Invalid argument: {0}
|
||||
error.argumentRequired={0} must not be null
|
||||
error.operationFailed=Operation failed: {0}
|
||||
error.angleNotMultipleOf90=Angle must be a multiple of 90
|
||||
error.pdfBookmarksNotFound=No PDF bookmarks/outline found in document
|
||||
error.fontLoadingFailed=Error processing font file
|
||||
error.fontDirectoryReadFailed=Failed to read font directory
|
||||
error.invalidArgument=不合法的参数: {0}
|
||||
error.argumentRequired={0} 不应当为空
|
||||
error.operationFailed=操作失败: {0}
|
||||
error.angleNotMultipleOf90=角度应当为90°的倍数
|
||||
error.pdfBookmarksNotFound=文档中没有找到PDF书签/大纲
|
||||
error.fontLoadingFailed=处理字体文件出错
|
||||
error.fontDirectoryReadFailed=无法读取字体目录
|
||||
delete=删除
|
||||
username=用户名
|
||||
password=密码
|
||||
@@ -260,7 +260,7 @@ disabledCurrentUserMessage=无法禁用当前用户。
|
||||
downgradeCurrentUserLongMessage=无法降级当前用户的角色。因此,当前用户将不会显示。
|
||||
userAlreadyExistsOAuthMessage=该用户已作为 OAuth2 用户存在。
|
||||
userAlreadyExistsWebMessage=该用户已作为 Web 用户存在。
|
||||
invalidRoleMessage=Invalid role.
|
||||
invalidRoleMessage=无效角色。
|
||||
error=错误
|
||||
oops=哎呀!
|
||||
help=帮助
|
||||
@@ -272,8 +272,8 @@ donate=捐款
|
||||
color=颜色
|
||||
sponsor=赞助
|
||||
info=信息
|
||||
pro=专业版
|
||||
proFeatures=Pro Features
|
||||
pro=Pro版本
|
||||
proFeatures=Pro版本功能
|
||||
page=页面
|
||||
pages=页码
|
||||
loading=加载中...
|
||||
@@ -281,11 +281,11 @@ addToDoc=添加至文件
|
||||
reset=重置
|
||||
apply=应用
|
||||
noFileSelected=未选择文件,请上传一个文件。
|
||||
view=View
|
||||
cancel=Cancel
|
||||
view=预览
|
||||
cancel=取消
|
||||
|
||||
back.toSettings=Back to Settings
|
||||
back.toHome=Back to Home
|
||||
back.toSettings=回到设置
|
||||
back.toHome=回到主页
|
||||
back.toAdmin=Back to Admin
|
||||
|
||||
legal.privacy=隐私政策
|
||||
@@ -450,17 +450,17 @@ adminUserSettings.teamName=团队名称
|
||||
adminUserSettings.teamExists=该团队已存在
|
||||
adminUserSettings.teamCreated=团队成功创建
|
||||
adminUserSettings.teamChanged=用户所属团队已更新
|
||||
adminUserSettings.teamHidden=Hidden
|
||||
adminUserSettings.teamHidden=隐藏
|
||||
adminUserSettings.totalMembers=成员总数
|
||||
adminUserSettings.confirmDeleteTeam=您确定要删除此团队吗?
|
||||
|
||||
teamCreated=Team created successfully
|
||||
teamExists=A team with that name already exists
|
||||
teamNameExists=Another team with that name already exists
|
||||
teamNotFound=Team not found
|
||||
teamDeleted=Team deleted
|
||||
teamHasUsers=Cannot delete a team with users assigned
|
||||
teamRenamed=Team renamed successfully
|
||||
teamCreated=团队创建成功
|
||||
teamExists=同名称的团队已存在
|
||||
teamNameExists=同名称的团队已存在
|
||||
teamNotFound=未找到团队
|
||||
teamDeleted=团队被删除
|
||||
teamHasUsers=不能删除已经被分配用户的团队
|
||||
teamRenamed=团队重命名成功
|
||||
|
||||
# Team user management
|
||||
team.addUser=添加用户到团队
|
||||
@@ -472,16 +472,16 @@ team.back=返回团队列表
|
||||
team.internal=内部团队
|
||||
team.internalTeamNotAccessible=内部团队为系统预设,无法访问
|
||||
team.cannotMoveInternalUsers=内部团队中的用户无法转移至其他团队
|
||||
team.hidden=Hidden
|
||||
team.name=Team Name
|
||||
team.totalMembers=Total Members
|
||||
team.members=Members
|
||||
team.username=Username
|
||||
team.role=Role
|
||||
team.status=Status
|
||||
team.enabled=Enabled
|
||||
team.disabled=Disabled
|
||||
team.noMembers=This team has no members yet.
|
||||
team.hidden=隐藏
|
||||
team.name=团队名称
|
||||
team.totalMembers=总人数
|
||||
team.members=成员
|
||||
team.username=用户名
|
||||
team.role=角色
|
||||
team.status=状态
|
||||
team.enabled=启用
|
||||
team.disabled=禁用
|
||||
team.noMembers=这个团队还没有成员。
|
||||
|
||||
|
||||
|
||||
@@ -585,9 +585,9 @@ home.addImage.title=在 PDF 中添加图片
|
||||
home.addImage.desc=将图像添加到 PDF 的指定位置。
|
||||
addImage.tags=图像、JPG、图片、照片
|
||||
|
||||
home.attachments.title=Add Attachments
|
||||
home.attachments.desc=Add or remove embedded files (attachments) to/from a PDF
|
||||
attachments.tags=embed,attach,file,attachment,attachments
|
||||
home.attachments.title=添加附件
|
||||
home.attachments.desc=在 PDF 中添加或删除嵌入文件(附件)
|
||||
attachments.tags=嵌入、附件、文件、附加
|
||||
|
||||
home.watermark.title=添加水印
|
||||
home.watermark.desc=在 PDF 中添加自定义水印。
|
||||
@@ -724,8 +724,8 @@ home.crop.desc=裁剪 PDF 以减小其文件大小(保留文本!)
|
||||
crop.tags=修剪、缩小、编辑、形状
|
||||
|
||||
home.autoSplitPDF.title=自动拆分页面
|
||||
home.autoSplitPDF.desc=使用物理扫描页面分割器 QR 代码自动拆分扫描的 PDF
|
||||
autoSplitPDF.tags=基于 QR 码、分离、扫描分割、整理
|
||||
home.autoSplitPDF.desc=使用物理扫描页面分割器二维码自动拆分扫描的 PDF
|
||||
autoSplitPDF.tags=基于二维码、分离、扫描分割、整理
|
||||
|
||||
home.sanitizePdf.title=清理
|
||||
home.sanitizePdf.desc=从 PDF 文件中删除脚本和其他元素
|
||||
@@ -831,11 +831,11 @@ validateSignature.tags=签名,验证,验证,PDF,证书,数字签名,
|
||||
replace-color.title=替换-反转-颜色
|
||||
replace-color.header=替换-反转 PDF 颜色
|
||||
home.replaceColorPdf.title=替换和反转颜色
|
||||
home.replaceColorPdf.desc=替换 PDF 中文本和背景的颜色,并将PDF全色反转以减小文件大小
|
||||
home.replaceColorPdf.desc=替换 PDF 中文本和背景的颜色,并将PDF反转颜色以减小文件大小
|
||||
replaceColorPdf.tags=更换颜色,页面操作,后端,服务器端
|
||||
replace-color.selectText.1=替换或反转颜色选项
|
||||
replace-color.selectText.2=默认(默认高对比度颜色)
|
||||
replace-color.selectText.3=定制(定制的颜色)
|
||||
replace-color.selectText.3=定制(定制的颜色)
|
||||
replace-color.selectText.4=全反转(反转所有颜色)
|
||||
replace-color.selectText.5=高对比度颜色选项
|
||||
replace-color.selectText.6=黑底白字
|
||||
@@ -1270,11 +1270,11 @@ addImage.upload=添加图片
|
||||
addImage.submit=添加图片
|
||||
|
||||
#attachments
|
||||
attachments.title=Add Attachments
|
||||
attachments.header=Add attachments
|
||||
attachments.description=Allows you to add attachments to the PDF
|
||||
attachments.descriptionPlaceholder=Enter a description for the attachments...
|
||||
attachments.addButton=Add Attachments
|
||||
attachments.title=添加附件
|
||||
attachments.header=添加附件
|
||||
attachments.description=允许您向PDF添加附件
|
||||
attachments.descriptionPlaceholder=为附件输入描述…
|
||||
attachments.addButton=添加附件
|
||||
|
||||
#merge
|
||||
merge.title=合并
|
||||
@@ -1664,7 +1664,7 @@ fileChooser.dragAndDropPDF=拖放PDF文件
|
||||
fileChooser.dragAndDropImage=拖放图片文件
|
||||
fileChooser.hoveredDragAndDrop=拖放文件到此处
|
||||
fileChooser.extractPDF=处理中...
|
||||
fileChooser.addAttachments=drag & drop attachments here
|
||||
fileChooser.addAttachments=拖放附件到此处
|
||||
|
||||
#release notes
|
||||
releases.footer=版本
|
||||
@@ -1722,14 +1722,14 @@ audit.dashboard.totalEvents=Total Events
|
||||
# Audit Dashboard Tabs
|
||||
audit.dashboard.tab.dashboard=Dashboard
|
||||
audit.dashboard.tab.events=Audit Events
|
||||
audit.dashboard.tab.export=Export
|
||||
audit.dashboard.tab.export=导出
|
||||
# Dashboard Charts
|
||||
audit.dashboard.eventsByType=Events by Type
|
||||
audit.dashboard.eventsByUser=Events by User
|
||||
audit.dashboard.eventsOverTime=Events Over Time
|
||||
audit.dashboard.period.7days=7 Days
|
||||
audit.dashboard.period.30days=30 Days
|
||||
audit.dashboard.period.90days=90 Days
|
||||
audit.dashboard.period.7days=7天
|
||||
audit.dashboard.period.30days=30天
|
||||
audit.dashboard.period.90days=90天
|
||||
|
||||
# Events Tab
|
||||
audit.dashboard.auditEvents=Audit Events
|
||||
@@ -1744,11 +1744,11 @@ audit.dashboard.filter.reset=Reset Filters
|
||||
|
||||
# Table Headers
|
||||
audit.dashboard.table.id=ID
|
||||
audit.dashboard.table.time=Time
|
||||
audit.dashboard.table.user=User
|
||||
audit.dashboard.table.type=Type
|
||||
audit.dashboard.table.details=Details
|
||||
audit.dashboard.table.viewDetails=View Details
|
||||
audit.dashboard.table.time=时间
|
||||
audit.dashboard.table.user=用户
|
||||
audit.dashboard.table.type=类型
|
||||
audit.dashboard.table.details=详情
|
||||
audit.dashboard.table.viewDetails=查看详情
|
||||
|
||||
# Pagination
|
||||
audit.dashboard.pagination.show=Show
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
{
|
||||
"moduleName": "com.opencsv:opencsv",
|
||||
"moduleUrl": "http://opencsv.sf.net",
|
||||
"moduleVersion": "5.11.1",
|
||||
"moduleVersion": "5.11.2",
|
||||
"moduleLicense": "Apache 2",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
@@ -800,13 +800,6 @@
|
||||
"moduleLicense": "GPL2 w/ CPE",
|
||||
"moduleLicenseUrl": "https://oss.oracle.com/licenses/CDDL+GPL-1.1"
|
||||
},
|
||||
{
|
||||
"moduleName": "junit:junit",
|
||||
"moduleUrl": "http://junit.org",
|
||||
"moduleVersion": "4.13.2",
|
||||
"moduleLicense": "Eclipse Public License 1.0",
|
||||
"moduleLicenseUrl": "http://www.eclipse.org/legal/epl-v10.html"
|
||||
},
|
||||
{
|
||||
"moduleName": "me.friwi:gluegen-rt",
|
||||
"moduleUrl": "http://jogamp.org/gluegen/www/",
|
||||
@@ -1310,20 +1303,6 @@
|
||||
"moduleLicense": "GNU General Public License, version 2 with the GNU Classpath Exception",
|
||||
"moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.hamcrest:hamcrest",
|
||||
"moduleUrl": "http://hamcrest.org/JavaHamcrest/",
|
||||
"moduleVersion": "3.0",
|
||||
"moduleLicense": "BSD-3-Clause",
|
||||
"moduleLicenseUrl": "https://raw.githubusercontent.com/hamcrest/JavaHamcrest/master/LICENSE"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.hamcrest:hamcrest-core",
|
||||
"moduleUrl": "http://hamcrest.org/JavaHamcrest/",
|
||||
"moduleVersion": "3.0",
|
||||
"moduleLicense": "BSD-3-Clause",
|
||||
"moduleLicenseUrl": "https://raw.githubusercontent.com/hamcrest/JavaHamcrest/master/LICENSE"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.hdrhistogram:HdrHistogram",
|
||||
"moduleUrl": "http://hdrhistogram.github.io/HdrHistogram/",
|
||||
@@ -1379,34 +1358,6 @@
|
||||
"moduleLicense": "The Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.junit.platform:junit-platform-commons",
|
||||
"moduleUrl": "https://junit.org/junit5/",
|
||||
"moduleVersion": "1.12.2",
|
||||
"moduleLicense": "Eclipse Public License v2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-v20.html"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.junit.platform:junit-platform-engine",
|
||||
"moduleUrl": "https://junit.org/junit5/",
|
||||
"moduleVersion": "1.12.2",
|
||||
"moduleLicense": "Eclipse Public License v2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-v20.html"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.junit.vintage:junit-vintage-engine",
|
||||
"moduleUrl": "https://junit.org/junit5/",
|
||||
"moduleVersion": "5.12.2",
|
||||
"moduleLicense": "Eclipse Public License v2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-v20.html"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.junit:junit-bom",
|
||||
"moduleUrl": "https://junit.org/junit5/",
|
||||
"moduleVersion": "5.12.2",
|
||||
"moduleLicense": "Eclipse Public License v2.0",
|
||||
"moduleLicenseUrl": "https://www.eclipse.org/legal/epl-v20.html"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.latencyutils:LatencyUtils",
|
||||
"moduleUrl": "http://latencyutils.github.io/LatencyUtils/",
|
||||
@@ -1516,13 +1467,6 @@
|
||||
"moduleLicense": "The Apache Software License, Version 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.opentest4j:opentest4j",
|
||||
"moduleUrl": "https://github.com/ota4j-team/opentest4j",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "The Apache License, Version 2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.ow2.asm:asm",
|
||||
"moduleUrl": "http://asm.ow2.org",
|
||||
|
||||
@@ -98,6 +98,61 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Responsive text sizing for drag & drop area */
|
||||
#fileInputText {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
max-width: 100%;
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Progressive font size reduction for high zoom levels */
|
||||
@media only screen and (max-width: 1280px) {
|
||||
#fileInputText {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 960px) {
|
||||
#fileInputText {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
#fileInputText {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 640px) {
|
||||
#fileInputText {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 480px) {
|
||||
#fileInputText {
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 384px) {
|
||||
#fileInputText {
|
||||
font-size: 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Also scale the container at ultra-high zoom */
|
||||
.input-container {
|
||||
height: 130px;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.file-input-btn {
|
||||
display: inline-block;
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
/* Base Container Styles */
|
||||
.multi-tool-container {
|
||||
max-width: 95vw;
|
||||
margin: 0 auto;
|
||||
max-width: 100vw;
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* Form Elements */
|
||||
label {
|
||||
text-align: left;
|
||||
display: block;
|
||||
@@ -17,6 +20,7 @@ label {
|
||||
flex-grow: 5;
|
||||
}
|
||||
|
||||
/* Action Bar Styles */
|
||||
.mt-action-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@@ -28,16 +32,17 @@ label {
|
||||
padding: 1.25rem;
|
||||
border-radius: 2rem;
|
||||
margin: 0px 25px;
|
||||
justify-content:center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.mt-action-bar>* {
|
||||
.mt-action-bar > * {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-file-uploader {
|
||||
width:100%
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mt-action-bar svg,
|
||||
.mt-action-btn svg {
|
||||
width: 20px;
|
||||
@@ -50,20 +55,21 @@ label {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Action Button Styles */
|
||||
.mt-action-btn {
|
||||
position: sticky;
|
||||
bottom: 10%;
|
||||
margin: auto;
|
||||
margin-bottom: 25px;
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 2rem;
|
||||
z-index: 12;
|
||||
background-color: var(--md-sys-color-surface-container-low) ;
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 12px 0px 0px;
|
||||
width: fit-content;
|
||||
justify-content: center;
|
||||
padding: 10px 20px
|
||||
padding: 10px 20px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.mt-action-btn .btn {
|
||||
@@ -71,68 +77,132 @@ label {
|
||||
height: 3.5rem;
|
||||
border-radius: 20px;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Card and Container Styles */
|
||||
.bg-card {
|
||||
background-color: var(--md-sys-color-surface-5);
|
||||
border-radius: 3rem;
|
||||
padding: 25px 0;
|
||||
padding: 15px 0;
|
||||
margin-left: 55px;
|
||||
margin-right: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
#pages-container-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
justify-content: center;
|
||||
padding: 0.75rem;
|
||||
border-radius: 25px;
|
||||
min-height: 275px;
|
||||
margin: 0 0 30px 0;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
#pages-container {
|
||||
margin: 0 auto;
|
||||
width: 95%;
|
||||
font-size: 0;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
justify-content: flex-start;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* width */
|
||||
/* Scrollbar Styles */
|
||||
#pages-container-wrapper::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
#pages-container-wrapper::-webkit-scrollbar-track {
|
||||
background: var(--scroll-bar-color);
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
#pages-container-wrapper::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
background: var(--scroll-bar-thumb);
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
#pages-container-wrapper::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scroll-bar-thumb-hover);
|
||||
}
|
||||
|
||||
|
||||
/* Page Container Base Styles */
|
||||
.page-container {
|
||||
display: inline-block;
|
||||
list-style-type: none;
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
line-height: 50px;
|
||||
margin: 15px 25px;
|
||||
margin-top: 15px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
aspect-ratio: 1;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
transition: width 1s linear;
|
||||
transition: width 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Responsive Page Container Sizes */
|
||||
@media only screen and (max-width: 480px) {
|
||||
.page-container {
|
||||
width: calc(100vw - 90px);
|
||||
height: calc(100vw - 90px);
|
||||
max-width: 300px;
|
||||
max-height: 300px;
|
||||
margin: 5px auto;
|
||||
}
|
||||
#pages-container { gap: 10px; }
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 481px) and (max-width: 768px) {
|
||||
.page-container {
|
||||
width: calc((100vw - 120px - 12px) / 2);
|
||||
height: calc((100vw - 120px - 12px) / 2);
|
||||
max-width: 250px;
|
||||
max-height: 250px;
|
||||
margin: 6px;
|
||||
}
|
||||
#pages-container { gap: 12px; }
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 769px) and (max-width: 1199px) {
|
||||
.page-container {
|
||||
width: calc((100vw - 140px - 45px) / 3);
|
||||
height: calc((100vw - 140px - 45px) / 3);
|
||||
max-width: 220px;
|
||||
max-height: 220px;
|
||||
margin: 7px;
|
||||
}
|
||||
#pages-container { gap: 15px; }
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1200px) and (max-width: 1280px) {
|
||||
.page-container {
|
||||
width: calc((100vw - 160px - 60px) / 4);
|
||||
height: calc((100vw - 160px - 60px) / 4);
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
margin: 8px;
|
||||
}
|
||||
#pages-container { gap: 17px; }
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1281px) {
|
||||
.page-container {
|
||||
width: calc((100vw - 180px - 80px) / 5);
|
||||
height: calc((100vw - 180px - 80px) / 5);
|
||||
max-width: 190px;
|
||||
max-height: 190px;
|
||||
margin: 10px;
|
||||
}
|
||||
#pages-container { gap: 20px; }
|
||||
}
|
||||
|
||||
/* Split Page Styles */
|
||||
.page-container.split-before {
|
||||
border-left: 1px dashed var(--md-sys-color-on-surface);
|
||||
padding-left: -1px;
|
||||
@@ -146,56 +216,35 @@ label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* RTL Language Support */
|
||||
.page-container:last-child:lang(ar),
|
||||
/* Arabic */
|
||||
.page-container:last-child:lang(he),
|
||||
/* Hebrew */
|
||||
.page-container:last-child:lang(fa),
|
||||
/* Persian */
|
||||
.page-container:last-child:lang(ur),
|
||||
/* Urdu */
|
||||
.page-container:last-child:lang(ckb),
|
||||
/* Sorani Kurdish */
|
||||
.page-container:last-child:lang(ks),
|
||||
/* Kashmiri */
|
||||
.page-container:last-child:lang(kk),
|
||||
/* Kazakh */
|
||||
.page-container:last-child:lang(uz),
|
||||
/* Uzbek */
|
||||
.page-container:last-child:lang(ky),
|
||||
/* Kyrgyz */
|
||||
.page-container:last-child:lang(bal),
|
||||
/* Baluchi */
|
||||
.page-container:last-child:lang(dv),
|
||||
/* Divehi */
|
||||
.page-container:last-child:lang(ps),
|
||||
/* Pashto */
|
||||
.page-container:last-child:lang(sdg),
|
||||
/* Southern Kurdish */
|
||||
.page-container:last-child:lang(syr),
|
||||
/* Syriac */
|
||||
.page-container:last-child:lang(mzn),
|
||||
/* Mazanderani */
|
||||
.page-container:last-child:lang(tgl),
|
||||
/* Tagalog */
|
||||
.page-container:last-child:lang(pnb),
|
||||
/* Western Punjabi */
|
||||
.page-container:last-child:lang(ug),
|
||||
/* Uyghur */
|
||||
.page-container:last-child:lang(nqo),
|
||||
/* N'Ko */
|
||||
.page-container:last-child:lang(bqi)
|
||||
|
||||
/* Bakhtiari */
|
||||
{
|
||||
.page-container:last-child:lang(bqi) {
|
||||
margin-left: auto !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Page Image Styles */
|
||||
.page-container img {
|
||||
max-width: calc(100% - 15px);
|
||||
max-height: calc(100% - 15px);
|
||||
max-width: calc(100% - 8px);
|
||||
max-height: calc(100% - 8px);
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
@@ -206,10 +255,7 @@ label {
|
||||
transition: rotate 0.3s;
|
||||
}
|
||||
|
||||
#add-pdf-button {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Page Number Styles */
|
||||
.page-number {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
@@ -224,14 +270,20 @@ label {
|
||||
font-weight: 450;
|
||||
}
|
||||
|
||||
/* Tool Header and Button Styles */
|
||||
.tool-header {
|
||||
margin: 0.5rem 1rem 2rem;
|
||||
margin: 0.5rem 1rem 1rem;
|
||||
}
|
||||
|
||||
#select-pages-button {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#add-pdf-button {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Selected Pages Container Styles */
|
||||
.selected-pages-container {
|
||||
background-color: var(--md-sys-color-surface);
|
||||
border-radius: 16px;
|
||||
@@ -247,12 +299,42 @@ label {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.selected-pages-header {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.selected-pages-header h5 {
|
||||
margin: 0 0 8px 0 !important;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.selected-pages-header #csv-input {
|
||||
width: 100%;
|
||||
height: 2.5rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--md-sys-color-outline);
|
||||
background-color: var(--md-sys-color-surface);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
padding: 0 12px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.selected-pages-header #csv-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--md-sys-color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(var(--md-sys-color-primary-rgb), 0.2);
|
||||
}
|
||||
|
||||
/* Pages List Styles */
|
||||
.pages-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
min-height: 0;
|
||||
max-height: 10rem;
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -293,12 +375,251 @@ label {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
.btn {
|
||||
position: relative;
|
||||
/* Zoom Level Responsive Styles */
|
||||
@media only screen and (min-width: 2000px) {
|
||||
.mt-action-btn {
|
||||
gap: 12px;
|
||||
padding: 12px 24px;
|
||||
border-radius: 2.5rem;
|
||||
}
|
||||
.mt-action-btn .btn {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border-radius: 22px;
|
||||
}
|
||||
.mt-action-btn .btn .material-symbols-rounded {
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 767px) { #pages-container { width:300px; } }
|
||||
@media only screen and (min-width: 768px) and (max-width: 991px) { #pages-container { width: 600px; } }
|
||||
@media only screen and (min-width: 992px) and (max-width: 1199px) { #pages-container { width: 900px; } }
|
||||
@media only screen and (min-width: 1200px) and (max-width: 1399px) { #pages-container { width: 900px; } }
|
||||
@media only screen and (min-width: 1399px) { #pages-container { width: 1200px; } }
|
||||
@media only screen and (min-width: 2560px) {
|
||||
.mt-action-btn {
|
||||
gap: 15px;
|
||||
padding: 15px 30px;
|
||||
border-radius: 3rem;
|
||||
}
|
||||
.mt-action-btn .btn {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
border-radius: 28px;
|
||||
}
|
||||
.mt-action-btn .btn .material-symbols-rounded {
|
||||
font-size: 2.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 3840px) {
|
||||
.mt-action-btn {
|
||||
gap: 20px;
|
||||
padding: 20px 40px;
|
||||
border-radius: 4rem;
|
||||
}
|
||||
.mt-action-btn .btn {
|
||||
width: 7rem;
|
||||
height: 7rem;
|
||||
border-radius: 40px;
|
||||
}
|
||||
.mt-action-btn .btn .material-symbols-rounded {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 7680px) {
|
||||
.mt-action-btn {
|
||||
gap: 40px;
|
||||
padding: 40px 80px;
|
||||
border-radius: 8rem;
|
||||
}
|
||||
.mt-action-btn .btn {
|
||||
width: 14rem;
|
||||
height: 14rem;
|
||||
border-radius: 80px;
|
||||
}
|
||||
.mt-action-btn .btn .material-symbols-rounded {
|
||||
font-size: 6rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Zoom-responsive Sidebar Styles */
|
||||
@media only screen and (max-width: 1280px) {
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mt-action-btn {
|
||||
position: fixed !important;
|
||||
left: 10px !important;
|
||||
top: 80px !important;
|
||||
bottom: 20px !important;
|
||||
margin: 0 !important;
|
||||
transform: none !important;
|
||||
border-radius: 16px !important;
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
height: calc(100vh - 100px) !important;
|
||||
overflow-y: auto !important;
|
||||
scrollbar-width: none !important;
|
||||
-ms-overflow-style: none !important;
|
||||
z-index: 12;
|
||||
width: 70px !important;
|
||||
gap: 8px !important;
|
||||
padding: 12px 8px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.mt-action-btn:has(.btn:nth-last-child(n+7)) {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.mt-action-btn::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mt-action-btn .btn {
|
||||
width: 48px !important;
|
||||
height: 48px !important;
|
||||
border-radius: 12px !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
flex-shrink: 0;
|
||||
font-size: 16px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.mt-action-btn .btn .material-symbols-rounded {
|
||||
font-size: 20px !important;
|
||||
line-height: 1 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24 !important;
|
||||
}
|
||||
|
||||
.multi-tool-container {
|
||||
margin-left: 0 !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.mt-action-bar {
|
||||
margin-left: 25px !important;
|
||||
margin-right: 25px !important;
|
||||
}
|
||||
|
||||
.mt-action-btn:not(:has(*:nth-child(6))) {
|
||||
justify-content: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile and High Zoom Responsive Styles */
|
||||
@media only screen and (max-width: 960px) {
|
||||
.mt-action-btn {
|
||||
left: 8px !important;
|
||||
top: 75px !important;
|
||||
bottom: 15px !important;
|
||||
height: calc(100vh - 90px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.mt-action-btn {
|
||||
left: 5px !important;
|
||||
width: 60px !important;
|
||||
top: calc(var(--navbar-height, 60px) + 10px) !important;
|
||||
bottom: 10px !important;
|
||||
height: calc(100vh - var(--navbar-height, 60px) - 20px) !important;
|
||||
}
|
||||
|
||||
.mt-action-btn .btn {
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.mt-action-btn .btn .material-symbols-rounded {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
#pages-container {
|
||||
margin: 0 auto !important;
|
||||
display: flex !important;
|
||||
flex-wrap: wrap !important;
|
||||
justify-content: center !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
width: calc(100vw - 55px) !important;
|
||||
height: calc(100vw - 60px) !important;
|
||||
max-width: 350px !important;
|
||||
max-height: 350px !important;
|
||||
margin: 5px auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 480px) {
|
||||
.mt-action-btn {
|
||||
left: 2px !important;
|
||||
top: calc(var(--navbar-height, 60px) + 5px) !important;
|
||||
bottom: 5px !important;
|
||||
height: calc(100vh - var(--navbar-height, 60px) - 10px) !important;
|
||||
width: 50px !important;
|
||||
gap: 6px !important;
|
||||
padding: 10px 6px !important;
|
||||
}
|
||||
|
||||
.mt-action-btn .btn {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
border-radius: 10px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.mt-action-btn .btn .material-symbols-rounded {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 384px) {
|
||||
.mt-action-btn {
|
||||
left: 2px !important;
|
||||
top: calc(var(--navbar-height, 60px) + 5px) !important;
|
||||
bottom: 5px !important;
|
||||
height: calc(100vh - var(--navbar-height, 60px) - 10px) !important;
|
||||
width: 40px !important;
|
||||
gap: 4px !important;
|
||||
padding: 8px 4px !important;
|
||||
}
|
||||
|
||||
.mt-action-btn .btn {
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
border-radius: 8px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.mt-action-btn .btn .material-symbols-rounded {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
width: calc(100vw - 55px) !important;
|
||||
height: calc(100vw - 60px) !important;
|
||||
max-width: 280px !important;
|
||||
max-height: 280px !important;
|
||||
margin: 3px auto !important;
|
||||
}
|
||||
|
||||
.page-container img {
|
||||
max-width: calc(100% - 4px) !important;
|
||||
max-height: calc(100% - 4px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -63,6 +63,79 @@
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Responsive navbar brand - prevent hamburger from wrapping */
|
||||
.navbar-brand {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0; /* Allow shrinking */
|
||||
}
|
||||
|
||||
.navbar-brand .icon-text {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Progressive text shrinking to prevent hamburger wrap */
|
||||
@media only screen and (max-width: 480px) {
|
||||
.navbar-brand .icon-text {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.main-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 420px) {
|
||||
.navbar-brand .icon-text {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.main-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 380px) {
|
||||
.navbar-brand .icon-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.main-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 340px) {
|
||||
.navbar-brand .icon-text {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.main-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ultra-small screens - hide text, keep icon */
|
||||
@media only screen and (max-width: 320px) {
|
||||
.navbar-brand .icon-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
vertical-align: middle;
|
||||
font-size: 2rem !important;
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
if (window.isDownloadScriptInitialized) return; // Prevent re-execution
|
||||
window.isDownloadScriptInitialized = true;
|
||||
|
||||
// Global PDF processing count tracking for survey system
|
||||
window.incrementPdfProcessingCount = function() {
|
||||
let pdfProcessingCount = parseInt(localStorage.getItem('pdfProcessingCount') || '0');
|
||||
pdfProcessingCount++;
|
||||
localStorage.setItem('pdfProcessingCount', pdfProcessingCount.toString());
|
||||
};
|
||||
|
||||
const {
|
||||
pdfPasswordPrompt,
|
||||
multipleInputsForSingleRequest,
|
||||
@@ -312,6 +319,11 @@
|
||||
pdf_pages: pageCount,
|
||||
});
|
||||
}
|
||||
|
||||
// Increment PDF processing count for survey tracking
|
||||
if (success && typeof window.incrementPdfProcessingCount === 'function') {
|
||||
window.incrementPdfProcessingCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -271,6 +271,11 @@ class PdfContainer {
|
||||
pdf_pages: pageCount,
|
||||
});
|
||||
}
|
||||
|
||||
// Increment PDF processing count for survey tracking
|
||||
if (success && typeof window.incrementPdfProcessingCount === 'function') {
|
||||
window.incrementPdfProcessingCount();
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
|
||||
@@ -66,19 +66,16 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const dontShowAgain = document.getElementById('dontShowAgain');
|
||||
const takeSurveyButton = document.getElementById('takeSurvey');
|
||||
|
||||
const viewThresholds = [5, 10, 15, 22, 30, 50, 75, 100, 150, 200];
|
||||
const pdfProcessingThresholds = [8, 15, 22, 35, 50, 75, 100, 150];
|
||||
|
||||
// Check if survey version changed and reset page views if it did
|
||||
// Check if survey version changed and reset PDF processing count if it did
|
||||
const storedVersion = localStorage.getItem('surveyVersion');
|
||||
if (storedVersion && storedVersion !== surveyVersion) {
|
||||
localStorage.setItem('pageViews', '0');
|
||||
localStorage.setItem('pdfProcessingCount', '0');
|
||||
localStorage.setItem('surveyVersion', surveyVersion);
|
||||
}
|
||||
|
||||
let pageViews = parseInt(localStorage.getItem('pageViews') || '0');
|
||||
|
||||
pageViews++;
|
||||
localStorage.setItem('pageViews', pageViews.toString());
|
||||
let pdfProcessingCount = parseInt(localStorage.getItem('pdfProcessingCount') || '0');
|
||||
|
||||
function shouldShowSurvey() {
|
||||
if(!window.showSurvey) {
|
||||
@@ -90,11 +87,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
}
|
||||
|
||||
// If survey version changed and we hit a threshold, show the survey
|
||||
if (localStorage.getItem('surveyVersion') !== surveyVersion && viewThresholds.includes(pageViews)) {
|
||||
if (localStorage.getItem('surveyVersion') !== surveyVersion && pdfProcessingThresholds.includes(pdfProcessingCount)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return viewThresholds.includes(pageViews);
|
||||
return pdfProcessingThresholds.includes(pdfProcessingCount);
|
||||
}
|
||||
|
||||
if (shouldShowSurvey()) {
|
||||
|
||||
@@ -1838,7 +1838,7 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.annotationEditorLayer freeTextEditor .overlay.enabled {
|
||||
.annotationEditorLayer .freeTextEditor .overlay.enabled {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
@@ -1852,7 +1852,7 @@
|
||||
height:100%;
|
||||
}
|
||||
|
||||
.annotationEditorLayer freeTextEditor .overlay.enabled{
|
||||
.annotationEditorLayer .freeTextEditor .overlay.enabled{
|
||||
display:block;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
const browserZoom = currentDPR / systemDPR;
|
||||
|
||||
// Counter-scale to maintain same visual size
|
||||
const isMobile = window.innerWidth <= 768 || /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
const isMobile = window.innerWidth <= 768 && /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
let baseScale = isMobile ? 3 : (isHighDPI ? 2.2 : 1.1); // Prioritize mobile scaling over high DPI
|
||||
const navScale = baseScale / currentDPR;
|
||||
// Dropdowns at 80% (20% smaller)
|
||||
@@ -81,6 +81,12 @@
|
||||
console.log('DPR:', currentDPR, 'isHighDPI:', isHighDPI, 'baseScale:', baseScale, 'navScale:', navScale, 'effective width:', effectiveWidth);
|
||||
}
|
||||
|
||||
// Set CSS custom property for mobile navbar scaling (for sidebar positioning)
|
||||
// Use the ACTUAL scaled height, not a fixed assumption
|
||||
const baseHeight = 60;
|
||||
const actualScaledHeight = baseHeight * navScale;
|
||||
document.documentElement.style.setProperty('--navbar-height', `${actualScaledHeight}px`);
|
||||
|
||||
setTimeout(() => {
|
||||
const dropdowns = document.querySelectorAll('.dropdown-menu');
|
||||
dropdowns.forEach(dropdown => {
|
||||
@@ -379,10 +385,10 @@
|
||||
Browse
|
||||
</label>
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<div class="d-flex justify-content-start align-items-center" id="fileInputText">
|
||||
<div th:text="#{fileChooser.click}" style="margin-right: 5px"></div>
|
||||
<div th:text="#{fileChooser.or}" style="margin-right: 5px"></div>
|
||||
<div th:text="#{fileChooser.dragAndDrop}" id="dragAndDrop"></div>
|
||||
<div class="text-center" id="fileInputText" style="word-wrap: break-word; hyphens: auto; line-height: 1.2;">
|
||||
<span th:text="#{fileChooser.click}"></span>
|
||||
<span th:text="#{fileChooser.or}" style="margin: 0 5px;"></span>
|
||||
<span th:text="#{fileChooser.dragAndDrop}" id="dragAndDrop"></span>
|
||||
</div>
|
||||
<hr th:if="${@GoogleDriveEnabled == true}" class="horizontal-divider" >
|
||||
</div>
|
||||
|
||||
@@ -33,18 +33,16 @@
|
||||
</div>
|
||||
|
||||
<div id="selected-pages-display" class="selected-pages-container hidden">
|
||||
<div style="display:flex; height:3rem; margin-right:1rem">
|
||||
<h5 th:text="#{multiTool.selectedPages}" style="white-space: nowrap; margin-right: 1rem;">Selected
|
||||
Pages</h5>
|
||||
<input type="text" id="csv-input" class="form-control" style="height:2.5rem" placeholder="1,3,5-10"
|
||||
value="">
|
||||
<div class="selected-pages-header">
|
||||
<h5 th:text="#{multiTool.selectedPages}">Selected Pages</h5>
|
||||
<input type="text" id="csv-input" class="form-control" placeholder="1,3,5-10" value="">
|
||||
</div>
|
||||
<ul id="selected-pages-list" class="pages-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="multi-tool-container">
|
||||
<div class="d-flex flex-wrap" id="pages-container-wrapper">
|
||||
<div id="pages-container-wrapper">
|
||||
<div id="pages-container">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+3
-2
@@ -19,6 +19,7 @@ import jakarta.servlet.http.HttpSession;
|
||||
|
||||
import stirling.software.common.model.job.JobResult;
|
||||
import stirling.software.common.model.job.JobStats;
|
||||
import stirling.software.common.model.job.ResultFile;
|
||||
import stirling.software.common.service.FileStorage;
|
||||
import stirling.software.common.service.JobQueue;
|
||||
import stirling.software.common.service.TaskManager;
|
||||
@@ -138,7 +139,7 @@ class JobControllerTest {
|
||||
|
||||
JobResult mockResult = new JobResult();
|
||||
mockResult.setJobId(jobId);
|
||||
mockResult.completeWithFile(fileId, originalFileName, contentType);
|
||||
mockResult.completeWithSingleFile(fileId, originalFileName, contentType, fileContent.length);
|
||||
|
||||
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||
when(fileStorage.retrieveBytes(fileId)).thenReturn(fileContent);
|
||||
@@ -215,7 +216,7 @@ class JobControllerTest {
|
||||
|
||||
JobResult mockResult = new JobResult();
|
||||
mockResult.setJobId(jobId);
|
||||
mockResult.completeWithFile(fileId, originalFileName, contentType);
|
||||
mockResult.completeWithSingleFile(fileId, originalFileName, contentType, 1024L);
|
||||
|
||||
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||
when(fileStorage.retrieveBytes(fileId)).thenThrow(new RuntimeException("File not found"));
|
||||
|
||||
Reference in New Issue
Block a user