Compare commits

...
21 Commits
Author SHA1 Message Date
James Brunton 7f63354e4e Lazily load PDF.js to reduce chunk size 2025-12-18 17:21:15 +00:00
James Brunton c6762532e5 Lazily load settings to reduce chunk size 2025-12-18 17:21:15 +00:00
James Brunton f44346a569 Ban large chunks 2025-12-18 12:44:45 +00:00
James Brunton 596d92b29d Lazily load settings to reduce chunk size 2025-12-18 12:11:46 +00:00
James Brunton 7cec27175a Lazily load onboarding to reduce chunk size 2025-12-18 12:04:28 +00:00
James Brunton 3136aefbf1 Lazily load file manager to reduce chunk size 2025-12-18 12:04:20 +00:00
James Brunton f8456e4279 Lazily load workbench to reduce chunk size 2025-12-18 12:00:09 +00:00
James Brunton 7344b3a1a7 Lazily load auth to reduce chunk size 2025-12-18 11:57:28 +00:00
James Brunton 40b49cc96e Lazily load config sections to reduce chunk size 2025-12-18 11:51:25 +00:00
James Brunton 9baad91fad Lazily load tools in tool registry to reduce chunk size 2025-12-18 11:43:50 +00:00
James Brunton 5cb2c5ee24 (drop me) 2025-12-18 10:28:04 +00:00
James Brunton 58a2a1b09b Add clean command 2025-12-18 10:13:44 +00:00
James Brunton 12b843ac33 Fix chunk size warnings 2025-12-18 10:08:42 +00:00
James Brunton d2d70fe793 Fix import warnings 2025-12-18 10:06:35 +00:00
James Brunton 31a653be32 Fix icon name warnings 2025-12-18 09:34:15 +00:00
Stephan Paternotte 49bea34576 Update translation.toml (#5254)
Updated translations of latest additions,
Saintized lots of accented letters,
Brought consistency in formal addressing of the user
2025-12-17 13:37:47 +00:00
Anthony StirlingandConnorYoh f9a44c4da4 Saml fixes (#5256)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
2025-12-17 10:52:48 +00:00
Anthony Stirling 4ec75d4d8c allow static overrides (#5258)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-17 10:07:51 +00:00
Reece Browne 93ed05b054 Only allow dual page view when there is more than one page, update to… (#5246) 2025-12-16 16:02:22 +00:00
Reece BrowneandClaude Sonnet 4.5 195b1472e4 Bug/v2/viewer annotations (#5245)
Show uneditable annotations on viewer
show editable annotations layer when in annotation tools (sign, add
image, add text)
Remove draw tool from viewer (this is replaced wholesale in an upcoming
PR so it wasn't worth doing the work to ensure it worked with the new
annotation layer set up_)
refactoring work, mostly renaming variables we can use for all
annotation based tools that had sign specific names.
remove "tools" tooltip

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 15:52:47 +00:00
James Brunton 340006ceea Add Sign Up functionality to desktop app (#5244)
# Description of Changes
Adds Sign Up with email to desktop app. SSO sign up will come in a
future PR.
2025-12-16 14:55:53 +00:00
48 changed files with 1373 additions and 893 deletions
@@ -37,10 +37,6 @@ public class AppConfig {
private final ApplicationProperties applicationProperties;
@Getter
@Value("${baseUrl:http://localhost}")
private String baseUrl;
@Getter
@Value("${server.servlet.context-path:/}")
private String contextPath;
@@ -49,6 +45,17 @@ public class AppConfig {
@Value("${server.port:8080}")
private String serverPort;
/**
* Get the backend URL from system configuration. Falls back to http://localhost if not
* configured.
*
* @return The backend base URL for SAML/OAuth/API callbacks
*/
public String getBackendUrl() {
String backendUrl = applicationProperties.getSystem().getBackendUrl();
return (backendUrl != null && !backendUrl.isBlank()) ? backendUrl : "http://localhost";
}
@Value("${v2}")
public boolean v2Enabled;
@@ -138,13 +138,13 @@ public class SPDFApplication {
@PostConstruct
public void init() {
String baseUrl = appConfig.getBaseUrl();
String backendUrl = appConfig.getBackendUrl();
String contextPath = appConfig.getContextPath();
String serverPort = appConfig.getServerPort();
baseUrlStatic = baseUrl;
baseUrlStatic = backendUrl;
contextPathStatic = contextPath;
serverPortStatic = serverPort;
String url = baseUrl + ":" + getStaticPort() + contextPath;
String url = backendUrl + ":" + getStaticPort() + contextPath;
// Log Tauri mode information
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_TAURI_MODE", "false"))) {
@@ -33,14 +33,35 @@ public class WebMvcConfig implements WebMvcConfigurer {
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Cache hashed assets (JS/CSS with content hashes) for 1 year
// These files have names like index-ChAS4tCC.js that change when content changes
// Check customFiles/static first, then fall back to classpath
registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:/static/assets/")
.addResourceLocations(
"file:"
+ stirling.software.common.configuration.InstallationPathConfig
.getStaticPath()
+ "assets/",
"classpath:/static/assets/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic());
// Don't cache index.html - it needs to be fresh to reference latest hashed assets
// Note: index.html is handled by ReactRoutingController for dynamic processing
registry.addResourceHandler("/index.html")
.addResourceLocations("classpath:/static/")
.addResourceLocations(
"file:"
+ stirling.software.common.configuration.InstallationPathConfig
.getStaticPath(),
"classpath:/static/")
.setCacheControl(CacheControl.noCache().mustRevalidate());
// Handle all other static resources (js, css, images, fonts, etc.)
// Check customFiles/static first for user overrides
registry.addResourceHandler("/**")
.addResourceLocations(
"file:"
+ stirling.software.common.configuration.InstallationPathConfig
.getStaticPath(),
"classpath:/static/")
.setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS));
}
@Override
@@ -66,7 +66,8 @@ public class ConfigController {
AppConfig appConfig = applicationContext.getBean(AppConfig.class);
// Extract key configuration values from AppConfig
configData.put("baseUrl", appConfig.getBaseUrl());
// Note: Frontend expects "baseUrl" field name for compatibility
configData.put("baseUrl", appConfig.getBackendUrl());
configData.put("contextPath", appConfig.getContextPath());
configData.put("serverPort", appConfig.getServerPort());
@@ -3,9 +3,14 @@ package stirling.software.SPDF.controller.web;
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 org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
@@ -14,6 +19,11 @@ import org.springframework.web.bind.annotation.GetMapping;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.InstallationPathConfig;
@Slf4j
@Controller
public class ReactRoutingController {
@@ -22,24 +32,44 @@ public class ReactRoutingController {
private String cachedIndexHtml;
private boolean indexHtmlExists = false;
private boolean useExternalIndexHtml = false;
@PostConstruct
public void init() {
// Only cache if index.html exists (production builds)
log.info("Static files custom path: {}", InstallationPathConfig.getStaticPath());
// Check for external index.html first (customFiles/static/)
Path externalIndexPath = Paths.get(InstallationPathConfig.getStaticPath(), "index.html");
log.debug("Checking for custom index.html at: {}", externalIndexPath);
if (Files.exists(externalIndexPath) && Files.isReadable(externalIndexPath)) {
log.info("Using custom index.html from: {}", externalIndexPath);
try {
this.cachedIndexHtml = processIndexHtml();
this.indexHtmlExists = true;
this.useExternalIndexHtml = true;
return;
} catch (IOException e) {
log.warn("Failed to load custom index.html, falling back to classpath", e);
}
}
// Fall back to classpath index.html
ClassPathResource resource = new ClassPathResource("static/index.html");
if (resource.exists()) {
try {
this.cachedIndexHtml = processIndexHtml();
this.indexHtmlExists = true;
this.useExternalIndexHtml = false;
} catch (IOException e) {
// Failed to cache, will process on each request
log.warn("Failed to cache index.html", e);
this.indexHtmlExists = false;
}
}
}
private String processIndexHtml() throws IOException {
ClassPathResource resource = new ClassPathResource("static/index.html");
Resource resource = getIndexHtmlResource();
try (InputStream inputStream = resource.getInputStream()) {
String html = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
@@ -62,6 +92,17 @@ public class ReactRoutingController {
}
}
private Resource getIndexHtmlResource() throws IOException {
// Check external location first
Path externalIndexPath = Paths.get(InstallationPathConfig.getStaticPath(), "index.html");
if (Files.exists(externalIndexPath) && Files.isReadable(externalIndexPath)) {
return new FileSystemResource(externalIndexPath.toFile());
}
// Fall back to classpath
return new ClassPathResource("static/index.html");
}
@GetMapping(
value = {"/", "/index.html"},
produces = MediaType.TEXT_HTML_VALUE)
@@ -198,7 +198,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private SamlClient getSamlClient(
String registrationId, SAML2 samlConf, List<X509Certificate> certificates)
throws SamlException {
String serverUrl = appConfig.getBaseUrl() + ":" + appConfig.getServerPort();
String serverUrl = appConfig.getBackendUrl() + ":" + appConfig.getServerPort();
String relyingPartyIdentifier =
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
@@ -344,7 +344,8 @@ public class SecurityConfiguration {
log.error("Error configuring SAML 2 login", e);
throw new RuntimeException(e);
}
});
})
.saml2Metadata(metadata -> {});
}
} else {
log.debug("Login is not enabled.");
@@ -102,16 +102,31 @@ public class Saml2Configuration {
log.error("Failed to load SAML2 SP credentials: {}", e.getMessage(), e);
throw new IllegalStateException("Failed to load SAML2 SP credentials", e);
}
// Get backend URL from configuration (for SAML endpoints)
String backendUrl = applicationProperties.getSystem().getBackendUrl();
if (backendUrl == null || backendUrl.isBlank()) {
backendUrl = "{baseUrl}"; // Fallback to Spring's auto-resolution
log.warn(
"system.backendUrl not configured - SAML metadata will use request-based URLs. Set system.backendUrl for production use.");
} else {
log.info("Using configured backend URL for SAML: {}", backendUrl);
}
String entityId =
backendUrl + "/saml2/service-provider-metadata/" + samlConf.getRegistrationId();
String acsLocation = backendUrl + "/login/saml2/sso/{registrationId}";
String sloResponseLocation = backendUrl + "/login";
RelyingPartyRegistration rp =
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
.signingX509Credentials(c -> c.add(signingCredential))
.entityId(samlConf.getIdpIssuer())
.entityId(entityId)
.singleLogoutServiceBinding(Saml2MessageBinding.POST)
.singleLogoutServiceLocation(samlConf.getIdpSingleLogoutUrl())
.singleLogoutServiceResponseLocation("{baseUrl}/login")
.singleLogoutServiceResponseLocation(sloResponseLocation)
.assertionConsumerServiceBinding(Saml2MessageBinding.POST)
.assertionConsumerServiceLocation(
"{baseUrl}/login/saml2/sso/{registrationId}")
.assertionConsumerServiceLocation(acsLocation)
.authnRequestsSigned(true)
.assertingPartyMetadata(
metadata ->
@@ -127,7 +142,7 @@ public class Saml2Configuration {
.singleLogoutServiceLocation(
samlConf.getIdpSingleLogoutUrl())
.singleLogoutServiceResponseLocation(
"{baseUrl}/login")
sloResponseLocation)
.wantAuthnRequestsSigned(true))
.build();
@@ -51,7 +51,8 @@ class UserLicenseSettingsServiceTest {
when(applicationProperties.getPremium()).thenReturn(premium);
when(applicationProperties.getAutomaticallyGenerated()).thenReturn(automaticallyGenerated);
when(automaticallyGenerated.getIsNewServer()).thenReturn(false); // Default: not a new server
when(automaticallyGenerated.getIsNewServer())
.thenReturn(false); // Default: not a new server
when(settingsRepository.findSettings()).thenReturn(Optional.of(mockSettings));
when(userService.getTotalUsersCount()).thenReturn(80L);
when(settingsRepository.save(any(UserLicenseSettings.class)))
+128 -41
View File
@@ -102,6 +102,7 @@
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"puppeteer": "^24.25.0",
"rollup-plugin-visualizer": "^6.0.5",
"typescript": "^5.9.2",
"typescript-eslint": "^8.44.1",
"vite": "^7.1.7",
@@ -457,7 +458,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -501,7 +501,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -582,7 +581,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.5.0.tgz",
"integrity": "sha512-Yrh9XoVaT8cUgzgqpJ7hx5wg6BqQrCFirqqlSwVb+Ly9oNn4fZbR9GycIWmzJOU5XBnaOJjXfQSaDyoNP0woNA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/engines": "1.5.0",
"@embedpdf/models": "1.5.0"
@@ -682,7 +680,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.5.0.tgz",
"integrity": "sha512-p7PTNNaIr4gH3jLwX+eLJe1DeUXgi21kVGN6SRx/pocH8esg4jqoOeD/YiRRZoZnPOiy0jBXVhkPkwSmY7a2hQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@@ -699,7 +696,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.5.0.tgz",
"integrity": "sha512-ckHgTfvkW6c5Ta7Mc+Dl9C2foVnvEpqEJ84wyBnqrU0OWbe/jsiPhyKBVeartMGqNI/kVfaQTXupyrKhekAVmg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@@ -717,7 +713,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.5.0.tgz",
"integrity": "sha512-P4YpIZfaW69etYIjphyaL4cGl2pB14h3OdTE0tRQ2pZYZHFLTvlt4q9B3PVSdhlSrHK5nob7jfLGon2U7xCslg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@@ -771,7 +766,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.5.0.tgz",
"integrity": "sha512-ywwSj0ByrlkvrJIHKRzqxARkOZriki8VJUC+T4MV8fGyF4CzvCRJyKlPktahFz+VxhoodqTh7lBCib68dH+GvA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@@ -806,7 +800,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.5.0.tgz",
"integrity": "sha512-RNmTZCZ8X1mA8cw9M7TMDuhO9GtkOalGha2bBL3En3D1IlDRS7PzNNMSMV7eqT7OQICSTltlpJ8p8Qi5esvL/Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@@ -843,7 +836,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.5.0.tgz",
"integrity": "sha512-zrxLBAZQoPswDuf9q9DrYaQc6B0Ysc2U1hueTjNH/4+ydfl0BFXZkKR63C2e3YmWtXvKjkoIj0GyPzsiBORLUw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@@ -919,7 +911,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.5.0.tgz",
"integrity": "sha512-G8GDyYRhfehw72+r4qKkydnA5+AU8qH67g01Y12b0DzI0VIzymh/05Z4dK8DsY3jyWPXJfw2hlg5+KDHaMBHgQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@@ -1075,7 +1066,6 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -1119,7 +1109,6 @@
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -2150,7 +2139,6 @@
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.6.tgz",
"integrity": "sha512-paTl+0x+O/QtgMtqVJaG8maD8sfiOdgPmLOyG485FmeGZ1L3KMdEkhxZtmdGlDFsLXhmMGQ57ducT90bvhXX5A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@floating-ui/react": "^0.27.16",
"clsx": "^2.1.1",
@@ -2201,7 +2189,6 @@
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.6.tgz",
"integrity": "sha512-liHfaWXHAkLjJy+Bkr29UsCwAoDQ/a64WrM67lksx8F0qqyjR5RQH8zVlhuOjdpQnwtlUkE/YiTvbJiPcoI0bw==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "^18.x || ^19.x"
}
@@ -2269,7 +2256,6 @@
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz",
"integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/core-downloads-tracker": "^7.3.5",
@@ -3202,7 +3188,6 @@
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz",
"integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.16"
}
@@ -3321,6 +3306,7 @@
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz",
"integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"acorn": "^8.9.0"
}
@@ -4097,7 +4083,6 @@
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -4426,7 +4411,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4437,7 +4421,6 @@
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -4507,7 +4490,6 @@
"integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.3",
"@typescript-eslint/types": "8.46.3",
@@ -5221,6 +5203,7 @@
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.24.tgz",
"integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/shared": "3.5.24"
}
@@ -5230,6 +5213,7 @@
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.24.tgz",
"integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.24",
"@vue/shared": "3.5.24"
@@ -5240,6 +5224,7 @@
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz",
"integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.24",
"@vue/runtime-core": "3.5.24",
@@ -5252,6 +5237,7 @@
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.24.tgz",
"integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-ssr": "3.5.24",
"@vue/shared": "3.5.24"
@@ -5278,7 +5264,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5686,6 +5671,7 @@
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">= 0.4"
}
@@ -5962,7 +5948,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@@ -6758,6 +6743,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-lazy-prop": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/define-properties": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
@@ -7010,8 +7005,7 @@
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz",
"integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==",
"dev": true,
"license": "BSD-3-Clause",
"peer": true
"license": "BSD-3-Clause"
},
"node_modules/dezalgo": {
"version": "1.0.4",
@@ -7406,7 +7400,6 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7577,7 +7570,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -7744,7 +7736,8 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/espree": {
"version": "10.4.0",
@@ -7809,6 +7802,7 @@
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.2.tgz",
"integrity": "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
}
@@ -8899,7 +8893,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -9222,6 +9215,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"dev": true,
"license": "MIT",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -9376,6 +9385,7 @@
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "^1.0.6"
}
@@ -9580,6 +9590,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-docker": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@@ -9696,7 +9719,6 @@
"integrity": "sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.19",
"@asamuzakjp/dom-selector": "^6.7.3",
@@ -10283,7 +10305,8 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/locate-path": {
"version": "6.0.0",
@@ -10961,6 +10984,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/open": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"define-lazy-prop": "^2.0.0",
"is-docker": "^2.1.1",
"is-wsl": "^2.2.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -11442,7 +11483,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -11722,7 +11762,6 @@
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz",
"integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -12105,7 +12144,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -12115,7 +12153,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -12728,6 +12765,60 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rollup-plugin-visualizer": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-6.0.5.tgz",
"integrity": "sha512-9+HlNgKCVbJDs8tVtjQ43US12eqaiHyyiLMdBwQ7vSZPiHMysGNo2E88TAp1si5wx8NAoYriI2A5kuKfIakmJg==",
"dev": true,
"license": "MIT",
"dependencies": {
"open": "^8.0.0",
"picomatch": "^4.0.2",
"source-map": "^0.7.4",
"yargs": "^17.5.1"
},
"bin": {
"rollup-plugin-visualizer": "dist/bin/cli.js"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"rolldown": "1.x || ^1.0.0-beta",
"rollup": "2.x || 3.x || 4.x"
},
"peerDependenciesMeta": {
"rolldown": {
"optional": true
},
"rollup": {
"optional": true
}
}
},
"node_modules/rollup-plugin-visualizer/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/rollup-plugin-visualizer/node_modules/source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
"integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">= 12"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -13627,6 +13718,7 @@
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">= 0.4"
}
@@ -13835,7 +13927,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -14137,7 +14228,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -14219,7 +14309,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"napi-postinstall": "^0.3.0"
},
@@ -14424,7 +14513,6 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -14595,7 +14683,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -14609,7 +14696,6 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@@ -15221,7 +15307,8 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/zod": {
"version": "3.25.76",
+2
View File
@@ -68,6 +68,7 @@
"scripts": {
"predev": "npm run generate-icons",
"dev": "vite",
"clean": "rm -rf dist",
"prebuild": "npm run generate-icons",
"lint": "eslint --max-warnings=0",
"build": "vite build",
@@ -149,6 +150,7 @@
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"puppeteer": "^24.25.0",
"rollup-plugin-visualizer": "^6.0.5",
"typescript": "^5.9.2",
"typescript-eslint": "^8.44.1",
"vite": "^7.1.7",
File diff suppressed because it is too large Load Diff
+93
View File
@@ -589,6 +589,26 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom 0.2.16",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "convert_case"
version = "0.4.0"
@@ -716,6 +736,12 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
version = "0.1.6"
@@ -908,6 +934,15 @@ dependencies = [
"syn 2.0.108",
]
[[package]]
name = "dlv-list"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
dependencies = [
"const-random",
]
[[package]]
name = "document-features"
version = "0.2.12"
@@ -1626,6 +1661,12 @@ dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.16.0"
@@ -2820,6 +2861,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-multimap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
dependencies = [
"dlv-list",
"hashbrown 0.14.5",
]
[[package]]
name = "ordered-stream"
version = "0.2.0"
@@ -3651,6 +3702,16 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "rust-ini"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
dependencies = [
"cfg-if",
"ordered-multimap",
]
[[package]]
name = "rust_decimal"
version = "1.39.0"
@@ -4256,6 +4317,7 @@ dependencies = [
"sha2",
"tauri",
"tauri-build",
"tauri-plugin-deep-link",
"tauri-plugin-fs",
"tauri-plugin-http",
"tauri-plugin-log",
@@ -4615,6 +4677,27 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-deep-link"
version = "2.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e82759f7c7d51de3cbde51c04b3f2332de52436ed84541182cd8944b04e9e73"
dependencies = [
"dunce",
"plist",
"rust-ini",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.17",
"tracing",
"url",
"windows-registry",
"windows-result 0.3.4",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.4.4"
@@ -4735,6 +4818,7 @@ dependencies = [
"serde",
"serde_json",
"tauri",
"tauri-plugin-deep-link",
"thiserror 2.0.17",
"tracing",
"windows-sys 0.60.2",
@@ -4954,6 +5038,15 @@ dependencies = [
"time-core",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "tiny_http"
version = "0.12.0"
+2 -1
View File
@@ -29,9 +29,10 @@ tauri-plugin-log = "2.0.0-rc"
tauri-plugin-shell = "2.1.0"
tauri-plugin-fs = "2.4.4"
tauri-plugin-http = "2.4.4"
tauri-plugin-single-instance = "2.0.1"
tauri-plugin-single-instance = { version = "2.3.6", features = ["deep-link"] }
tauri-plugin-store = "2.1.0"
tauri-plugin-opener = "2.0.0"
tauri-plugin-deep-link = "2.4.5"
keyring = { version = "3.6.1", features = ["apple-native", "windows-native"] }
tokio = { version = "1.0", features = ["time", "sync"] }
reqwest = { version = "0.11", features = ["json"] }
+3 -1
View File
@@ -19,6 +19,8 @@
{
"identifier": "fs:allow-read-file",
"allow": [{ "path": "**" }]
}
},
"opener:default",
"shell:allow-open"
]
}
+36 -1
View File
@@ -1,4 +1,4 @@
use tauri::{Manager, RunEvent, WindowEvent, Emitter};
use tauri::{AppHandle, Emitter, Manager, RunEvent, WindowEvent};
mod utils;
mod commands;
@@ -28,6 +28,17 @@ use commands::{
};
use state::connection_state::AppConnectionState;
use utils::{add_log, get_tauri_logs};
use tauri_plugin_deep_link::DeepLinkExt;
fn dispatch_deep_link(app: &AppHandle, url: &str) {
add_log(format!("🔗 Dispatching deep link: {}", url));
let _ = app.emit("deep-link", url.to_string());
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_focus();
let _ = window.unminimize();
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
@@ -42,6 +53,7 @@ pub fn run() {
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_deep_link::init())
.manage(AppConnectionState::default())
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
// This callback runs when a second instance tries to start
@@ -78,6 +90,29 @@ pub fn run() {
}
}
{
let app_handle = app.handle();
// On macOS the plugin registers schemes via bundle metadata, so runtime registration is required only on Windows/Linux
#[cfg(any(target_os = "linux", target_os = "windows"))]
if let Err(err) = app_handle.deep_link().register_all() {
add_log(format!("⚠️ Failed to register deep link handler: {}", err));
}
if let Ok(Some(urls)) = app_handle.deep_link().get_current() {
let initial_handle = app_handle.clone();
for url in urls {
dispatch_deep_link(&initial_handle, url.as_str());
}
}
let event_app_handle = app_handle.clone();
app_handle.deep_link().on_open_url(move |event| {
for url in event.urls() {
dispatch_deep_link(&event_app_handle, url.as_str());
}
});
}
// Start backend immediately, non-blocking
let app_handle = app.handle().clone();
+7
View File
@@ -77,6 +77,13 @@
},
"fs": {
"requireLiteralLeadingDot": false
},
"deep-link": {
"desktop": {
"schemes": [
"stirlingpdf"
]
}
}
}
}
@@ -1,5 +1,7 @@
import { FileId } from '@app/types/file';
import { PDFDocument, PDFPage, PageBreakSettings } from '@app/types/pageEditor';
import { pdfWorkerManager } from '@app/services/pdfWorkerManager';
import { thumbnailGenerationService } from '@app/services/thumbnailGenerationService';
// V1-style DOM-first command system (replaces the old React state commands)
export abstract class DOMCommand {
@@ -727,8 +729,6 @@ export class InsertFilesCommand extends DOMCommand {
private async generateThumbnailsForInsertedPages(updatedDocument: PDFDocument): Promise<void> {
try {
const { thumbnailGenerationService } = await import('@app/services/thumbnailGenerationService');
// Group pages by file ID to generate thumbnails efficiently
const pagesByFileId = new Map<FileId, PDFPage[]>();
@@ -809,7 +809,6 @@ export class InsertFilesCommand extends DOMCommand {
const clonedArrayBuffer = arrayBuffer.slice(0);
// Use PDF.js via the worker manager to extract pages
const { pdfWorkerManager } = await import('@app/services/pdfWorkerManager');
const pdf = await pdfWorkerManager.createDocument(clonedArrayBuffer);
const pageCount = pdf.numPages;
@@ -1,4 +1,5 @@
import { Dispatch, SetStateAction, useCallback } from "react";
import JSZip from "jszip";
import type {
useFileActions,
@@ -194,8 +195,7 @@ export const usePageEditorExport = ({
);
if (files.length > 1) {
const JSZip = await import("jszip");
const zip = new JSZip.default();
const zip = new JSZip();
files.forEach((file) => {
zip.file(file.name, file);
@@ -1,6 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Tooltip } from '@app/components/shared/Tooltip';
import AppsIcon from '@mui/icons-material/AppsRounded';
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
import { useNavigationState, useNavigationActions } from '@app/contexts/NavigationContext';
@@ -11,13 +10,11 @@ import QuickAccessButton from '@app/components/shared/quickAccessBar/QuickAccess
interface AllToolsNavButtonProps {
activeButton: string;
setActiveButton: (id: string) => void;
tooltipPosition?: 'left' | 'right' | 'top' | 'bottom';
}
const AllToolsNavButton: React.FC<AllToolsNavButtonProps> = ({
activeButton,
setActiveButton,
tooltipPosition = 'right'
}) => {
const { t } = useTranslation();
const { handleReaderToggle, handleBackToTools, selectedToolKey, leftPanelView } = useToolWorkflow();
@@ -55,26 +52,18 @@ const AllToolsNavButton: React.FC<AllToolsNavButtonProps> = ({
};
return (
<Tooltip
content={t("quickAccess.allTools", "Tools")}
position={tooltipPosition}
arrow
containerStyle={{ marginTop: "-1rem" }}
maxWidth={200}
>
<div className="mt-4 mb-2">
<QuickAccessButton
icon={<AppsIcon sx={{ fontSize: isActive ? '1.875rem' : '1.5rem' }} />}
label={t("quickAccess.allTools", "Tools")}
isActive={isActive}
onClick={handleNavClick}
href={navProps.href}
ariaLabel={t("quickAccess.allTools", "Tools")}
textClassName="all-tools-text"
component="a"
/>
</div>
</Tooltip>
<div className="mt-4 mb-2">
<QuickAccessButton
icon={<AppsIcon sx={{ fontSize: isActive ? '1.875rem' : '1.5rem' }} />}
label={t("quickAccess.allTools", "Tools")}
isActive={isActive}
onClick={handleNavClick}
href={navProps.href}
ariaLabel={t("quickAccess.allTools", "Tools")}
textClassName="all-tools-text"
component="a"
/>
</div>
);
};
@@ -1,4 +1,4 @@
import React, { useMemo, useState, useEffect, useCallback } from 'react';
import React, { useMemo, useState, useEffect, useCallback, Suspense } from 'react';
import { Modal, Text, ActionIcon, Tooltip, Group } from '@mantine/core';
import { useNavigate, useLocation } from 'react-router-dom';
import LocalIcon from '@app/components/shared/LocalIcon';
@@ -223,7 +223,9 @@ const AppConfigModalInner: React.FC<AppConfigModalProps> = ({ opened, onClose })
</ActionIcon>
</div>
<div className="modal-body">
{activeComponent}
<Suspense fallback={<div style={{ padding: '2rem', textAlign: 'center' }}>Loading...</div>}>
{activeComponent}
</Suspense>
</div>
</div>
</div>
@@ -1,4 +1,4 @@
import React, { useState, useRef, forwardRef, useEffect } from "react";
import React, { useState, useRef, forwardRef, useEffect, lazy, Suspense } from "react";
import { Stack, Divider, Menu, Indicator } from "@mantine/core";
import { useTranslation } from 'react-i18next';
import { useNavigate, useLocation } from 'react-router-dom';
@@ -14,8 +14,10 @@ import '@app/components/shared/quickAccessBar/QuickAccessBar.css';
import { Tooltip } from '@app/components/shared/Tooltip';
import AllToolsNavButton from '@app/components/shared/AllToolsNavButton';
import ActiveToolButton from "@app/components/shared/quickAccessBar/ActiveToolButton";
import AppConfigModal from '@app/components/shared/AppConfigModal';
import { useAppConfig } from '@app/contexts/AppConfigContext';
// Lazy-load AppConfigModal
const AppConfigModal = lazy(() => import('@app/components/shared/AppConfigModal'));
import { useLicenseAlert } from "@app/hooks/useLicenseAlert";
import { requestStartTour } from '@app/constants/events';
import QuickAccessButton from '@app/components/shared/quickAccessBar/QuickAccessButton';
@@ -365,10 +367,12 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
</div>
</div>
<AppConfigModal
opened={configModalOpen}
onClose={() => setConfigModalOpen(false)}
/>
<Suspense fallback={null}>
<AppConfigModal
opened={configModalOpen}
onClose={() => setConfigModalOpen(false)}
/>
</Suspense>
</div>
);
});
@@ -17,7 +17,7 @@ export default function LoginRequiredBanner({ show }: LoginRequiredBannerProps)
return (
<Alert
icon={<LocalIcon icon="lock-rounded" width={20} height={20} />}
icon={<LocalIcon icon="lock" width={20} height={20} />}
title={t('admin.settings.loginDisabled.title', 'Login Mode Required')}
color="blue"
variant="light"
@@ -1,8 +1,10 @@
import React from 'react';
import React, { lazy } from 'react';
import { useTranslation } from 'react-i18next';
import { NavKey } from '@app/components/shared/config/types';
import HotkeysSection from '@app/components/shared/config/configSections/HotkeysSection';
import GeneralSection from '@app/components/shared/config/configSections/GeneralSection';
// Lazy load config sections
const HotkeysSection = lazy(() => import('@app/components/shared/config/configSections/HotkeysSection'));
const GeneralSection = lazy(() => import('@app/components/shared/config/configSections/GeneralSection'));
export interface ConfigNavItem {
key: NavKey;
@@ -193,7 +193,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
size="sm"
color={updateSummary.max_priority === 'urgent' ? 'red' : 'blue'}
onClick={() => setUpdateModalOpened(true)}
leftSection={<LocalIcon icon="system-update-rounded" width="1rem" height="1rem" />}
leftSection={<LocalIcon icon="system-update-alt-rounded" width="1rem" height="1rem" />}
>
{t('settings.general.updates.viewDetails', 'View Details')}
</Button>
@@ -1,15 +1,9 @@
import React, { useState, useEffect } from 'react';
import { ActionIcon, Popover } from '@mantine/core';
import React from 'react';
import { ActionIcon } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import LocalIcon from '@app/components/shared/LocalIcon';
import { Tooltip } from '@app/components/shared/Tooltip';
import { ViewerContext } from '@app/contexts/ViewerContext';
import { useSignature } from '@app/contexts/SignatureContext';
import { ColorSwatchButton, ColorPicker } from '@app/components/annotation/shared/ColorPicker';
import { useFileState, useFileContext } from '@app/contexts/FileContext';
import { generateThumbnailWithMetadata } from '@app/utils/thumbnailUtils';
import { createProcessedFile } from '@app/contexts/file/fileActions';
import { createStirlingFile, createNewStirlingFileStub } from '@app/types/fileContext';
import { useNavigationState } from '@app/contexts/NavigationContext';
import { useSidebarContext } from '@app/contexts/SidebarContext';
import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide';
@@ -23,32 +17,14 @@ export default function ViewerAnnotationControls({ currentView, disabled = false
const { t } = useTranslation();
const { sidebarRefs } = useSidebarContext();
const { position: tooltipPosition, offset: tooltipOffset } = useRightRailTooltipSide(sidebarRefs);
const [selectedColor, setSelectedColor] = useState('#000000');
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
const [isHoverColorPickerOpen, setIsHoverColorPickerOpen] = useState(false);
// Viewer context for PDF controls - safely handle when not available
const viewerContext = React.useContext(ViewerContext);
// Signature context for accessing drawing API
const { signatureApiRef, isPlacementMode } = useSignature();
// File state for save functionality
const { state, selectors } = useFileState();
const { actions: fileActions } = useFileContext();
const activeFiles = selectors.getFiles();
// Check if we're in sign mode
const { selectedTool } = useNavigationState();
const isSignMode = selectedTool === 'sign';
// Turn off annotation mode when switching away from viewer
useEffect(() => {
if (currentView !== 'viewer' && viewerContext?.isAnnotationMode) {
viewerContext.setAnnotationMode(false);
}
}, [currentView, viewerContext]);
// Don't show any annotation controls in sign mode
if (isSignMode) {
return null;
@@ -65,7 +41,7 @@ export default function ViewerAnnotationControls({ currentView, disabled = false
onClick={() => {
viewerContext?.toggleAnnotationsVisibility();
}}
disabled={disabled || currentView !== 'viewer' || viewerContext?.isAnnotationMode || isPlacementMode}
disabled={disabled || currentView !== 'viewer'}
>
<LocalIcon
icon={viewerContext?.isAnnotationsVisible ? "visibility" : "visibility-off-rounded"}
@@ -74,164 +50,6 @@ export default function ViewerAnnotationControls({ currentView, disabled = false
/>
</ActionIcon>
</Tooltip>
{/* Annotation Mode Toggle with Drawing Controls */}
{viewerContext?.isAnnotationMode ? (
// When active: Show color picker on hover
<div
onMouseEnter={() => setIsHoverColorPickerOpen(true)}
onMouseLeave={() => setIsHoverColorPickerOpen(false)}
style={{ display: 'inline-flex' }}
>
<Popover
opened={isHoverColorPickerOpen}
onClose={() => setIsHoverColorPickerOpen(false)}
position="left"
withArrow
shadow="md"
offset={8}
>
<Popover.Target>
<ActionIcon
variant="filled"
color="blue"
radius="md"
className="right-rail-icon"
onClick={() => {
viewerContext?.toggleAnnotationMode();
setIsHoverColorPickerOpen(false); // Close hover color picker when toggling off
// Deactivate drawing tool when exiting annotation mode
if (signatureApiRef?.current) {
try {
signatureApiRef.current.deactivateTools();
} catch (error) {
console.log('Signature API not ready:', error);
}
}
}}
disabled={disabled}
aria-label="Drawing mode active"
>
<LocalIcon icon="edit" width="1.5rem" height="1.5rem" />
</ActionIcon>
</Popover.Target>
<Popover.Dropdown>
<div style={{ minWidth: '8rem' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem', padding: '0.5rem' }}>
<div style={{ fontSize: '0.8rem', fontWeight: 500 }}>Drawing Color</div>
<ColorSwatchButton
color={selectedColor}
size={32}
onClick={() => {
setIsHoverColorPickerOpen(false); // Close hover picker
setIsColorPickerOpen(true); // Open main color picker modal
}}
/>
</div>
</div>
</Popover.Dropdown>
</Popover>
</div>
) : (
// When inactive: Show "Draw" tooltip
<Tooltip content={t('rightRail.draw', 'Draw')} position={tooltipPosition} offset={tooltipOffset} arrow portalTarget={document.body}>
<ActionIcon
variant="subtle"
radius="md"
className="right-rail-icon"
onClick={() => {
viewerContext?.toggleAnnotationMode();
// Activate ink drawing tool when entering annotation mode
if (signatureApiRef?.current && currentView === 'viewer') {
try {
signatureApiRef.current.activateDrawMode();
signatureApiRef.current.updateDrawSettings(selectedColor, 2);
} catch (error) {
console.log('Signature API not ready:', error);
}
}
}}
disabled={disabled}
aria-label={typeof t === 'function' ? t('rightRail.draw', 'Draw') : 'Draw'}
>
<LocalIcon icon="edit" width="1.5rem" height="1.5rem" />
</ActionIcon>
</Tooltip>
)}
{/* Save PDF with Annotations */}
<Tooltip content={t('rightRail.save', 'Save')} position={tooltipPosition} offset={tooltipOffset} arrow portalTarget={document.body}>
<ActionIcon
variant="subtle"
radius="md"
className="right-rail-icon"
onClick={async () => {
if (viewerContext?.exportActions?.saveAsCopy && currentView === 'viewer') {
try {
const pdfArrayBuffer = await viewerContext.exportActions.saveAsCopy();
if (pdfArrayBuffer) {
// Create new File object with flattened annotations
const blob = new Blob([pdfArrayBuffer], { type: 'application/pdf' });
// Get the original file name or use a default
const originalFileName = activeFiles.length > 0 ? activeFiles[0].name : 'document.pdf';
const newFile = new File([blob], originalFileName, { type: 'application/pdf' });
// Replace the current file in context with the saved version (exact same logic as Sign tool)
if (activeFiles.length > 0) {
// Generate thumbnail and metadata for the saved file
const thumbnailResult = await generateThumbnailWithMetadata(newFile);
const processedFileMetadata = createProcessedFile(thumbnailResult.pageCount, thumbnailResult.thumbnail);
// Get current file info
const currentFileIds = state.files.ids;
if (currentFileIds.length > 0) {
const currentFileId = currentFileIds[0];
const currentRecord = selectors.getStirlingFileStub(currentFileId);
if (!currentRecord) {
console.error('No file record found for:', currentFileId);
return;
}
// Create output stub and file (exact same as Sign tool)
const outputStub = createNewStirlingFileStub(newFile, undefined, thumbnailResult.thumbnail, processedFileMetadata);
const outputStirlingFile = createStirlingFile(newFile, outputStub.id);
// Replace the original file with the saved version
await fileActions.consumeFiles([currentFileId], [outputStirlingFile], [outputStub]);
}
}
}
} catch (error) {
console.error('Error saving PDF:', error);
}
}
}}
disabled={disabled}
>
<LocalIcon icon="save" width="1.5rem" height="1.5rem" />
</ActionIcon>
</Tooltip>
{/* Color Picker Modal */}
<ColorPicker
isOpen={isColorPickerOpen}
onClose={() => setIsColorPickerOpen(false)}
selectedColor={selectedColor}
onColorChange={(color) => {
setSelectedColor(color);
// Update drawing tool color if annotation mode is active
if (viewerContext?.isAnnotationMode && signatureApiRef?.current && currentView === 'viewer') {
try {
signatureApiRef.current.updateDrawSettings(color, 2);
} catch (error) {
console.log('Unable to update drawing settings:', error);
}
}
}}
title="Choose Drawing Color"
/>
</>
);
}
@@ -63,7 +63,7 @@ export default function ToastRenderer() {
{/* Icon */}
<div className="toast-icon">
{t.icon ?? (
<LocalIcon icon={`material-symbols:${getDefaultIconName(t)}`} width={20} height={20} />
<LocalIcon icon={getDefaultIconName(t)} width={20} height={20} />
)}
</div>
@@ -86,7 +86,7 @@ export default function ToastRenderer() {
}}
className={`toast-button toast-expand-button ${t.isExpanded ? 'toast-expand-button--expanded' : ''}`}
>
<LocalIcon icon="material-symbols:expand-more-rounded" />
<LocalIcon icon="expand-more-rounded" />
</button>
)}
<button
@@ -94,7 +94,7 @@ export default function ToastRenderer() {
onClick={() => dismiss(t.id)}
className="toast-button"
>
<LocalIcon icon="material-symbols:close-rounded" width={20} height={20} />
<LocalIcon icon="close-rounded" width={20} height={20} />
</button>
</div>
</div>
@@ -42,7 +42,7 @@ const AddAttachmentsSettings = ({ parameters, onParameterChange, disabled = fals
component="label"
htmlFor="attachments-input"
disabled={disabled}
leftSection={<LocalIcon icon="plus" width="14" height="14" />}
leftSection={<LocalIcon icon="add-rounded" width="14" height="14" />}
>
{parameters.attachments?.length > 0
? t("AddAttachmentsRequest.addMoreFiles", "Add more files...")
@@ -234,7 +234,7 @@ export const SavedSignaturesSection = ({
{groupedSignatures.personal.length > 0 && activePersonalSignature && (
<Stack gap="xs">
<Group gap="xs">
<LocalIcon icon="material-symbols:person-rounded" width={18} height={18} />
<LocalIcon icon="person-rounded" width={18} height={18} />
<Text fw={600} size="sm">
{translate('saved.personalHeading', 'Personal Signatures')}
</Text>
@@ -257,7 +257,7 @@ export const SavedSignaturesSection = ({
onClick={() => setActivePersonalIndex(prev => Math.max(0, prev - 1))}
disabled={disabled || activePersonalIndex === 0}
>
<LocalIcon icon="material-symbols:chevron-left-rounded" width={18} height={18} />
<LocalIcon icon="chevron-left-rounded" width={18} height={18} />
</ActionIcon>
<ActionIcon
variant="light"
@@ -265,7 +265,7 @@ export const SavedSignaturesSection = ({
onClick={() => setActivePersonalIndex(prev => Math.min(groupedSignatures.personal.length - 1, prev + 1))}
disabled={disabled || activePersonalIndex >= groupedSignatures.personal.length - 1}
>
<LocalIcon icon="material-symbols:chevron-right-rounded" width={18} height={18} />
<LocalIcon icon="chevron-right-rounded" width={18} height={18} />
</ActionIcon>
</Group>
</Group>
@@ -284,7 +284,7 @@ export const SavedSignaturesSection = ({
onClick={() => onUseSignature(activePersonalSignature)}
disabled={disabled}
>
<LocalIcon icon="material-symbols:check-circle-outline-rounded" width={18} height={18} />
<LocalIcon icon="check-circle-outline-rounded" width={18} height={18} />
</ActionIcon>
<Tooltip label={translate('saved.delete', 'Remove')}>
<ActionIcon
@@ -294,7 +294,7 @@ export const SavedSignaturesSection = ({
onClick={() => onDeleteSignature(activePersonalSignature)}
disabled={disabled}
>
<LocalIcon icon="material-symbols:delete-outline-rounded" width={18} height={18} />
<LocalIcon icon="delete-outline-rounded" width={18} height={18} />
</ActionIcon>
</Tooltip>
</Group>
@@ -317,7 +317,7 @@ export const SavedSignaturesSection = ({
{groupedSignatures.shared.length > 0 && activeSharedSignature && (
<Stack gap="xs">
<Group gap="xs">
<LocalIcon icon="material-symbols:groups-rounded" width={18} height={18} />
<LocalIcon icon="groups-rounded" width={18} height={18} />
<Text fw={600} size="sm">
{translate('saved.sharedHeading', 'Shared Signatures')}
</Text>
@@ -340,7 +340,7 @@ export const SavedSignaturesSection = ({
onClick={() => setActiveSharedIndex(prev => Math.max(0, prev - 1))}
disabled={disabled || activeSharedIndex === 0}
>
<LocalIcon icon="material-symbols:chevron-left-rounded" width={18} height={18} />
<LocalIcon icon="chevron-left-rounded" width={18} height={18} />
</ActionIcon>
<ActionIcon
variant="light"
@@ -348,7 +348,7 @@ export const SavedSignaturesSection = ({
onClick={() => setActiveSharedIndex(prev => Math.min(groupedSignatures.shared.length - 1, prev + 1))}
disabled={disabled || activeSharedIndex >= groupedSignatures.shared.length - 1}
>
<LocalIcon icon="material-symbols:chevron-right-rounded" width={18} height={18} />
<LocalIcon icon="chevron-right-rounded" width={18} height={18} />
</ActionIcon>
</Group>
</Group>
@@ -367,7 +367,7 @@ export const SavedSignaturesSection = ({
onClick={() => onUseSignature(activeSharedSignature)}
disabled={disabled}
>
<LocalIcon icon="material-symbols:check-circle-outline-rounded" width={18} height={18} />
<LocalIcon icon="check-circle-outline-rounded" width={18} height={18} />
</ActionIcon>
{isAdmin && (
<Tooltip label={translate('saved.delete', 'Remove')}>
@@ -378,7 +378,7 @@ export const SavedSignaturesSection = ({
onClick={() => onDeleteSignature(activeSharedSignature)}
disabled={disabled}
>
<LocalIcon icon="material-symbols:delete-outline-rounded" width={18} height={18} />
<LocalIcon icon="delete-outline-rounded" width={18} height={18} />
</ActionIcon>
</Tooltip>
)}
@@ -424,7 +424,7 @@ export const SavedSignaturesSection = ({
onClick={() => setActiveLocalStorageIndex(prev => Math.max(0, prev - 1))}
disabled={disabled || activeLocalStorageIndex === 0}
>
<LocalIcon icon="material-symbols:chevron-left-rounded" width={18} height={18} />
<LocalIcon icon="chevron-left-rounded" width={18} height={18} />
</ActionIcon>
<ActionIcon
variant="light"
@@ -432,7 +432,7 @@ export const SavedSignaturesSection = ({
onClick={() => setActiveLocalStorageIndex(prev => Math.min(groupedSignatures.localStorage.length - 1, prev + 1))}
disabled={disabled || activeLocalStorageIndex >= groupedSignatures.localStorage.length - 1}
>
<LocalIcon icon="material-symbols:chevron-right-rounded" width={18} height={18} />
<LocalIcon icon="chevron-right-rounded" width={18} height={18} />
</ActionIcon>
</Group>
</Group>
@@ -451,7 +451,7 @@ export const SavedSignaturesSection = ({
onClick={() => onUseSignature(activeLocalStorageSignature)}
disabled={disabled}
>
<LocalIcon icon="material-symbols:check-circle-outline-rounded" width={18} height={18} />
<LocalIcon icon="check-circle-outline-rounded" width={18} height={18} />
</ActionIcon>
<Tooltip label={translate('saved.delete', 'Remove')}>
<ActionIcon
@@ -461,7 +461,7 @@ export const SavedSignaturesSection = ({
onClick={() => onDeleteSignature(activeLocalStorageSignature)}
disabled={disabled}
>
<LocalIcon icon="material-symbols:delete-outline-rounded" width={18} height={18} />
<LocalIcon icon="delete-outline-rounded" width={18} height={18} />
</ActionIcon>
</Tooltip>
</Group>
@@ -370,7 +370,7 @@ const SignSettings = ({
isReady,
onClick,
'personal',
'material-symbols:person-rounded',
'person-rounded',
translate('saved.savePersonal', 'Save Personal')
);
@@ -379,7 +379,7 @@ const SignSettings = ({
isReady,
onClick,
'shared',
'material-symbols:groups-rounded',
'groups-rounded',
translate('saved.saveShared', 'Save Shared')
);
@@ -964,7 +964,7 @@ const SignSettings = ({
gap: '0.4rem',
}}
>
<LocalIcon icon="material-symbols:pause-rounded" width={20} height={20} />
<LocalIcon icon="pause-rounded" width={20} height={20} />
<Text component="span" size="sm" fw={500}>
{translate('mode.pause', 'Pause placement')}
</Text>
@@ -987,7 +987,7 @@ const SignSettings = ({
gap: '0.4rem',
}}
>
<LocalIcon icon="material-symbols:play-arrow-rounded" width={20} height={20} />
<LocalIcon icon="play-arrow-rounded" width={20} height={20} />
<Text component="span" size="sm" fw={500}>
{translate('mode.resume', 'Resume placement')}
</Text>
@@ -51,6 +51,7 @@ const EmbedPdfViewerContent = ({
getScrollState,
getRotationState,
isAnnotationMode,
setAnnotationMode,
isAnnotationsVisible,
exportActions,
} = useViewer();
@@ -82,15 +83,18 @@ const EmbedPdfViewerContent = ({
// Navigation guard for unsaved changes
const { setHasUnsavedChanges, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker } = useNavigationGuard();
// Check if we're in signature mode OR viewer annotation mode
// Check if we're in an annotation tool
const { selectedTool } = useNavigationState();
// Tools that use the stamp/signature placement system with hover preview
const isSignatureMode = selectedTool === 'sign' || selectedTool === 'addText' || selectedTool === 'addImage';
// Tools that require the annotation layer (Sign, Add Text, Add Image)
const isInAnnotationTool = selectedTool === 'sign' || selectedTool === 'addText' || selectedTool === 'addImage';
// Sync isAnnotationMode in ViewerContext with current tool
useEffect(() => {
setAnnotationMode(isInAnnotationTool);
}, [isInAnnotationTool, setAnnotationMode]);
// Enable annotations when: in sign mode, OR annotation mode is active, OR we want to show existing annotations
const shouldEnableAnnotations = isSignatureMode || isAnnotationMode || isAnnotationsVisible;
const isPlacementOverlayActive = Boolean(
isSignatureMode && shouldEnableAnnotations && isPlacementMode && signatureConfig
isInAnnotationTool && isPlacementMode && signatureConfig
);
// Track which file tab is active
@@ -333,7 +337,8 @@ const EmbedPdfViewerContent = ({
key={currentFile && isStirlingFile(currentFile) ? currentFile.fileId : (effectiveFile.file instanceof File ? effectiveFile.file.name : effectiveFile.url)}
file={effectiveFile.file}
url={effectiveFile.url}
enableAnnotations={shouldEnableAnnotations}
enableAnnotations={isAnnotationMode}
showBakedAnnotations={isAnnotationsVisible}
signatureApiRef={signatureApiRef as React.RefObject<any>}
historyApiRef={historyApiRef as React.RefObject<any>}
onSignatureAdded={() => {
@@ -52,12 +52,13 @@ interface LocalEmbedPDFProps {
file?: File | Blob;
url?: string | null;
enableAnnotations?: boolean;
showBakedAnnotations?: boolean;
onSignatureAdded?: (annotation: any) => void;
signatureApiRef?: React.RefObject<SignatureAPI>;
historyApiRef?: React.RefObject<HistoryAPI>;
}
export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) {
export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedAnnotations = true, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) {
const { t } = useTranslation();
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: any}>>([]);
@@ -100,7 +101,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
}),
createPluginRegistration(RenderPluginPackage, {
withForms: true,
withAnnotations: true,
withAnnotations: showBakedAnnotations && !enableAnnotations, // Show baked annotations only when: visibility is ON and annotation layer is OFF
}),
// Register interaction manager (required for zoom and selection features)
@@ -166,7 +167,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
// Register print plugin for printing PDFs
createPluginRegistration(PrintPluginPackage),
];
}, [pdfUrl]);
}, [pdfUrl, enableAnnotations, showBakedAnnotations]);
// Initialize the engine with the React hook - use local WASM for offline support
const { engine, isLoading, error } = usePdfiumEngine({
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { Button, Paper, Group, NumberInput } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useViewer } from '@app/contexts/ViewerContext';
import { Tooltip } from '@app/components/shared/Tooltip';
import FirstPageIcon from '@mui/icons-material/FirstPage';
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
@@ -209,21 +210,27 @@ export function PdfViewerToolbar({
</Button>
{/* Dual Page Toggle */}
<Button
variant={isDualPageActive ? "filled" : "light"}
color="blue"
size="md"
radius="xl"
onClick={handleDualPageToggle}
style={{ minWidth: '2.5rem' }}
title={
<Tooltip
content={
isDualPageActive
? t("viewer.singlePageView", "Single Page View")
: t("viewer.dualPageView", "Dual Page View")
}
position="top"
arrow
>
{isDualPageActive ? <DescriptionIcon fontSize="small" /> : <ViewWeekIcon fontSize="small" />}
</Button>
<Button
variant={isDualPageActive ? "filled" : "light"}
color="blue"
size="md"
radius="xl"
onClick={handleDualPageToggle}
disabled={scrollState.totalPages <= 1}
style={{ minWidth: '2.5rem' }}
>
{isDualPageActive ? <DescriptionIcon fontSize="small" /> : <ViewWeekIcon fontSize="small" />}
</Button>
</Tooltip>
{/* Zoom Controls */}
<Group gap={4} align="center" style={{ marginLeft: 16 }}>
@@ -95,7 +95,6 @@ interface ViewerContextType {
// Annotation/drawing mode for viewer
isAnnotationMode: boolean;
setAnnotationMode: (enabled: boolean) => void;
toggleAnnotationMode: () => void;
// Active file index for multi-file viewing
activeFileIndex: number;
@@ -230,10 +229,6 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
setIsAnnotationModeState(enabled);
};
const toggleAnnotationMode = () => {
setIsAnnotationModeState(prev => !prev);
};
// State getters - read from bridge refs
const getScrollState = (): ScrollState => {
return bridgeRefs.current.scroll?.state || { currentPage: 1, totalPages: 0 };
@@ -318,7 +313,6 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
toggleAnnotationsVisibility,
isAnnotationMode,
setAnnotationMode,
toggleAnnotationMode,
// Active file index
activeFileIndex,
@@ -13,7 +13,7 @@ import {
ProcessedFileMetadata,
} from '@app/types/fileContext';
import { FileId, ToolOperation } from '@app/types/file';
import { generateThumbnailWithMetadata } from '@app/utils/thumbnailUtils';
import { generateThumbnailWithMetadata, generateThumbnailForFile } from '@app/utils/thumbnailUtils';
import { FileLifecycleManager } from '@app/contexts/file/lifecycle';
import { buildQuickKeySet } from '@app/contexts/file/fileSelectors';
import { StirlingFile } from '@app/types/fileContext';
@@ -296,7 +296,6 @@ export async function addFiles(
// Non-PDF files: simple thumbnail generation, no processedFile metadata
try {
if (DEBUG) console.log(`📄 Generating simple thumbnail for non-PDF file ${file.name}`);
const { generateThumbnailForFile } = await import('@app/utils/thumbnailUtils');
thumbnail = await generateThumbnailForFile(file);
if (DEBUG) console.log(`📄 Generated simple thumbnail for ${file.name}: no page count, thumbnail: SUCCESS`);
} catch (error) {
@@ -1,19 +1,8 @@
import { useMemo } from "react";
import { useMemo, lazy } from "react";
import LocalIcon from "@app/components/shared/LocalIcon";
import { useTranslation } from "react-i18next";
import { devApiLink } from "@app/constants/links";
import SplitPdfPanel from "@app/tools/Split";
import CompressPdfPanel from "@app/tools/Compress";
import OCRPanel from "@app/tools/OCR";
import ConvertPanel from "@app/tools/Convert";
import Sanitize from "@app/tools/Sanitize";
import AddPassword from "@app/tools/AddPassword";
import ChangePermissions from "@app/tools/ChangePermissions";
import RemoveBlanks from "@app/tools/RemoveBlanks";
import RemovePages from "@app/tools/RemovePages";
import ReorganizePages from "@app/tools/ReorganizePages";
import { reorganizePagesOperationConfig } from "@app/hooks/tools/reorganizePages/useReorganizePagesOperation";
import RemovePassword from "@app/tools/RemovePassword";
import {
SubcategoryId,
ToolCategoryId,
@@ -23,34 +12,9 @@ import {
LinkToolRegistry,
} from "@app/data/toolsTaxonomy";
import { isSuperToolId, isLinkToolId } from '@app/types/toolId';
import AdjustContrast from "@app/tools/AdjustContrast";
import AdjustContrastSingleStepSettings from "@app/components/tools/adjustContrast/AdjustContrastSingleStepSettings";
import { adjustContrastOperationConfig } from "@app/hooks/tools/adjustContrast/useAdjustContrastOperation";
import { getSynonyms } from "@app/utils/toolSynonyms";
import { useProprietaryToolRegistry } from "@app/data/useProprietaryToolRegistry";
import GetPdfInfo from "@app/tools/GetPdfInfo";
import AddWatermark from "@app/tools/AddWatermark";
import AddStamp from "@app/tools/AddStamp";
import AddAttachments from "@app/tools/AddAttachments";
import Merge from '@app/tools/Merge';
import EditTableOfContents from '@app/tools/EditTableOfContents';
import Repair from "@app/tools/Repair";
import AutoRename from "@app/tools/AutoRename";
import SingleLargePage from "@app/tools/SingleLargePage";
import PageLayout from "@app/tools/PageLayout";
import UnlockPdfForms from "@app/tools/UnlockPdfForms";
import RemoveCertificateSign from "@app/tools/RemoveCertificateSign";
import RemoveImage from "@app/tools/RemoveImage";
import CertSign from "@app/tools/CertSign";
import BookletImposition from "@app/tools/BookletImposition";
import Flatten from "@app/tools/Flatten";
import Rotate from "@app/tools/Rotate";
import PdfTextEditor from "@app/tools/pdfTextEditor/PdfTextEditor";
import ChangeMetadata from "@app/tools/ChangeMetadata";
import Crop from "@app/tools/Crop";
import Sign from "@app/tools/Sign";
import AddText from "@app/tools/AddText";
import AddImage from "@app/tools/AddImage";
import { compressOperationConfig } from "@app/hooks/tools/compress/useCompressOperation";
import { splitOperationConfig } from "@app/hooks/tools/split/useSplitOperation";
import { addPasswordOperationConfig } from "@app/hooks/tools/addPassword/useAddPasswordOperation";
@@ -88,50 +52,7 @@ import { scannerImageSplitOperationConfig } from "@app/hooks/tools/scannerImageS
import { addPageNumbersOperationConfig } from "@app/components/tools/addPageNumbers/useAddPageNumbersOperation";
import { extractPagesOperationConfig } from "@app/hooks/tools/extractPages/useExtractPagesOperation";
import { ENDPOINTS as SPLIT_ENDPOINT_NAMES } from '@app/constants/splitConstants';
import CompressSettings from "@app/components/tools/compress/CompressSettings";
import AddPasswordSettings from "@app/components/tools/addPassword/AddPasswordSettings";
import RemovePasswordSettings from "@app/components/tools/removePassword/RemovePasswordSettings";
import SanitizeSettings from "@app/components/tools/sanitize/SanitizeSettings";
import AddWatermarkSingleStepSettings from "@app/components/tools/addWatermark/AddWatermarkSingleStepSettings";
import OCRSettings from "@app/components/tools/ocr/OCRSettings";
import ConvertSettings from "@app/components/tools/convert/ConvertSettings";
import ChangePermissionsSettings from "@app/components/tools/changePermissions/ChangePermissionsSettings";
import BookletImpositionSettings from "@app/components/tools/bookletImposition/BookletImpositionSettings";
import FlattenSettings from "@app/components/tools/flatten/FlattenSettings";
import RedactSingleStepSettings from "@app/components/tools/redact/RedactSingleStepSettings";
import Redact from "@app/tools/Redact";
import AdjustPageScale from "@app/tools/AdjustPageScale";
import ReplaceColor from "@app/tools/ReplaceColor";
import ScannerImageSplit from "@app/tools/ScannerImageSplit";
import OverlayPdfs from "@app/tools/OverlayPdfs";
import { ToolId } from "@app/types/toolId";
import MergeSettings from '@app/components/tools/merge/MergeSettings';
import AdjustPageScaleSettings from "@app/components/tools/adjustPageScale/AdjustPageScaleSettings";
import ScannerImageSplitSettings from "@app/components/tools/scannerImageSplit/ScannerImageSplitSettings";
import ChangeMetadataSingleStep from "@app/components/tools/changeMetadata/ChangeMetadataSingleStep";
import SignSettings from "@app/components/tools/sign/SignSettings";
import AddPageNumbers from "@app/tools/AddPageNumbers";
import RemoveAnnotations from "@app/tools/RemoveAnnotations";
import PageLayoutSettings from "@app/components/tools/pageLayout/PageLayoutSettings";
import ExtractImages from "@app/tools/ExtractImages";
import ExtractPages from "@app/tools/ExtractPages";
import ExtractImagesSettings from "@app/components/tools/extractImages/ExtractImagesSettings";
import ExtractPagesSettings from "@app/components/tools/extractPages/ExtractPagesSettings";
import ReplaceColorSettings from "@app/components/tools/replaceColor/ReplaceColorSettings";
import AddStampAutomationSettings from "@app/components/tools/addStamp/AddStampAutomationSettings";
import CertSignAutomationSettings from "@app/components/tools/certSign/CertSignAutomationSettings";
import CropAutomationSettings from "@app/components/tools/crop/CropAutomationSettings";
import RotateAutomationSettings from "@app/components/tools/rotate/RotateAutomationSettings";
import SplitAutomationSettings from "@app/components/tools/split/SplitAutomationSettings";
import AddAttachmentsSettings from "@app/components/tools/addAttachments/AddAttachmentsSettings";
import RemovePagesSettings from "@app/components/tools/removePages/RemovePagesSettings";
import RemoveBlanksSettings from "@app/components/tools/removeBlanks/RemoveBlanksSettings";
import AddPageNumbersAutomationSettings from "@app/components/tools/addPageNumbers/AddPageNumbersAutomationSettings";
import OverlayPdfsSettings from "@app/components/tools/overlayPdfs/OverlayPdfsSettings";
import ValidateSignature from "@app/tools/ValidateSignature";
import ShowJS from "@app/tools/ShowJS";
import Automate from "@app/tools/Automate";
import Compare from "@app/tools/Compare";
import { CONVERT_SUPPORTED_FORMATS } from "@app/constants/convertSupportedFornats";
@@ -156,7 +77,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
pdfTextEditor: {
icon: <LocalIcon icon="edit-square-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.pdfTextEditor.title", "PDF Text Editor"),
component: PdfTextEditor,
component: lazy(() => import("@app/tools/pdfTextEditor/PdfTextEditor")),
description: t(
"home.pdfTextEditor.desc",
"Review and edit text and images in PDFs with grouped text editing and PDF regeneration"
@@ -186,21 +107,21 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
merge: {
icon: <LocalIcon icon="library-add-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.merge.title", "Merge"),
component: Merge,
component: lazy(() => import("@app/tools/Merge")),
description: t("home.merge.desc", "Merge multiple PDFs into a single document"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1,
endpoints: ["merge-pdfs"],
operationConfig: mergeOperationConfig,
automationSettings: MergeSettings,
automationSettings: lazy(() => import("@app/components/tools/merge/MergeSettings")),
synonyms: getSynonyms(t, "merge")
},
// Signing
certSign: {
icon: <LocalIcon icon="workspace-premium-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.certSign.title", "Certificate Sign"),
component: CertSign,
component: lazy(() => import("@app/tools/CertSign")),
description: t("home.certSign.desc", "Sign PDF documents using digital certificates"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.SIGNING,
@@ -208,24 +129,24 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
maxFiles: -1,
endpoints: ["cert-sign"],
operationConfig: certSignOperationConfig,
automationSettings: CertSignAutomationSettings,
automationSettings: lazy(() => import("@app/components/tools/certSign/CertSignAutomationSettings")),
},
sign: {
icon: <LocalIcon icon="signature-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.sign.title", "Sign"),
component: Sign,
component: lazy(() => import("@app/tools/Sign")),
description: t("home.sign.desc", "Adds signature to PDF by drawing, text or image"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.SIGNING,
operationConfig: signOperationConfig,
automationSettings: SignSettings, // TODO:: not all settings shown, suggested next tools shown
automationSettings: lazy(() => import("@app/components/tools/sign/SignSettings")), // TODO:: not all settings shown, suggested next tools shown
synonyms: getSynonyms(t, "sign"),
supportsAutomate: false, //TODO make support Sign
},
addText: {
icon: <LocalIcon icon="material-symbols:text-fields-rounded" width="1.5rem" height="1.5rem" />,
icon: <LocalIcon icon="text-fields-rounded" width="1.5rem" height="1.5rem" />,
name: t('home.addText.title', 'Add Text'),
component: AddText,
component: lazy(() => import("@app/tools/AddText")),
description: t('home.addText.desc', 'Add custom text anywhere in your PDF'),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
@@ -237,7 +158,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
addImage: {
icon: <LocalIcon icon="image-rounded" width="1.5rem" height="1.5rem" />,
name: t('home.addImage.title', 'Add Image'),
component: AddImage,
component: lazy(() => import("@app/tools/AddImage")),
description: t('home.addImage.desc', 'Add images anywhere in your PDF'),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
@@ -252,33 +173,33 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
addPassword: {
icon: <LocalIcon icon="password-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.addPassword.title", "Add Password"),
component: AddPassword,
component: lazy(() => import("@app/tools/AddPassword")),
description: t("home.addPassword.desc", "Add password protection and restrictions to PDF files"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
maxFiles: -1,
endpoints: ["add-password"],
operationConfig: addPasswordOperationConfig,
automationSettings: AddPasswordSettings,
automationSettings: lazy(() => import("@app/components/tools/addPassword/AddPasswordSettings")),
synonyms: getSynonyms(t, "addPassword")
},
watermark: {
icon: <LocalIcon icon="branding-watermark-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.watermark.title", "Add Watermark"),
component: AddWatermark,
component: lazy(() => import("@app/tools/AddWatermark")),
maxFiles: -1,
description: t("home.watermark.desc", "Add a custom watermark to your PDF document."),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
endpoints: ["add-watermark"],
operationConfig: addWatermarkOperationConfig,
automationSettings: AddWatermarkSingleStepSettings,
automationSettings: lazy(() => import("@app/components/tools/addWatermark/AddWatermarkSingleStepSettings")),
synonyms: getSynonyms(t, "watermark")
},
addStamp: {
icon: <LocalIcon icon="approval-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.addStamp.title", "Add Stamp to PDF"),
component: AddStamp,
component: lazy(() => import("@app/tools/AddStamp")),
description: t("home.addStamp.desc", "Add text or add image stamps at set locations"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
@@ -286,38 +207,38 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
maxFiles: -1,
endpoints: ["add-stamp"],
operationConfig: addStampOperationConfig,
automationSettings: AddStampAutomationSettings,
automationSettings: lazy(() => import("@app/components/tools/addStamp/AddStampAutomationSettings")),
},
sanitize: {
icon: <LocalIcon icon="cleaning-services-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.sanitize.title", "Sanitize"),
component: Sanitize,
component: lazy(() => import("@app/tools/Sanitize")),
maxFiles: -1,
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"),
endpoints: ["sanitize-pdf"],
operationConfig: sanitizeOperationConfig,
automationSettings: SanitizeSettings,
automationSettings: lazy(() => import("@app/components/tools/sanitize/SanitizeSettings")),
synonyms: getSynonyms(t, "sanitize")
},
flatten: {
icon: <LocalIcon icon="layers-clear-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.flatten.title", "Flatten"),
component: Flatten,
component: lazy(() => import("@app/tools/Flatten")),
description: t("home.flatten.desc", "Remove all interactive elements and forms from a PDF"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
maxFiles: -1,
endpoints: ["flatten"],
operationConfig: flattenOperationConfig,
automationSettings: FlattenSettings,
automationSettings: lazy(() => import("@app/components/tools/flatten/FlattenSettings")),
synonyms: getSynonyms(t, "flatten")
},
unlockPDFForms: {
icon: <LocalIcon icon="preview-off-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.unlockPDFForms.title", "Unlock PDF Forms"),
component: UnlockPdfForms,
component: lazy(() => import("@app/tools/UnlockPdfForms")),
description: t("home.unlockPDFForms.desc", "Remove read-only property of form fields in a PDF document."),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
@@ -330,20 +251,20 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
changePermissions: {
icon: <LocalIcon icon="lock-outline" width="1.5rem" height="1.5rem" />,
name: t("home.changePermissions.title", "Change Permissions"),
component: ChangePermissions,
component: lazy(() => import("@app/tools/ChangePermissions")),
description: t("home.changePermissions.desc", "Change document restrictions and permissions"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_SECURITY,
maxFiles: -1,
endpoints: ["add-password"],
operationConfig: changePermissionsOperationConfig,
automationSettings: ChangePermissionsSettings,
automationSettings: lazy(() => import("@app/components/tools/changePermissions/ChangePermissionsSettings")),
synonyms: getSynonyms(t, "changePermissions"),
},
getPdfInfo: {
icon: <LocalIcon icon="fact-check-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.getPdfInfo.title", "Get ALL Info on PDF"),
component: GetPdfInfo,
component: lazy(() => import("@app/tools/GetPdfInfo")),
description: t("home.getPdfInfo.desc", "Grabs any and all information possible on PDFs"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.VERIFICATION,
@@ -356,7 +277,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
validateSignature: {
icon: <LocalIcon icon="verified-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.validateSignature.title", "Validate PDF Signature"),
component: ValidateSignature,
component: lazy(() => import("@app/tools/ValidateSignature")),
description: t("home.validateSignature.desc", "Verify digital signatures and certificates in PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.VERIFICATION,
@@ -386,20 +307,20 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
changeMetadata: {
icon: <LocalIcon icon="assignment-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.changeMetadata.title", "Change Metadata"),
component: ChangeMetadata,
component: lazy(() => import("@app/tools/ChangeMetadata")),
description: t("home.changeMetadata.desc", "Change/Remove/Add metadata from a PDF document"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_REVIEW,
maxFiles: -1,
endpoints: ["update-metadata"],
operationConfig: changeMetadataOperationConfig,
automationSettings: ChangeMetadataSingleStep,
automationSettings: lazy(() => import("@app/components/tools/changeMetadata/ChangeMetadataSingleStep")),
synonyms: getSynonyms(t, "changeMetadata")
},
editTableOfContents: {
icon: <LocalIcon icon="toc-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.editTableOfContents.title", "Edit Table of Contents"),
component: EditTableOfContents,
component: lazy(() => import("@app/tools/EditTableOfContents")),
description: t(
"home.editTableOfContents.desc",
"Add or edit bookmarks and table of contents in PDF documents"
@@ -418,44 +339,44 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
crop: {
icon: <LocalIcon icon="crop-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.crop.title", "Crop PDF"),
component: Crop,
component: lazy(() => import("@app/tools/Crop")),
description: t("home.crop.desc", "Crop a PDF to reduce its size (maintains text!)"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
maxFiles: -1,
endpoints: ["crop"],
operationConfig: cropOperationConfig,
automationSettings: CropAutomationSettings,
automationSettings: lazy(() => import("@app/components/tools/crop/CropAutomationSettings")),
},
rotate: {
icon: <LocalIcon icon="rotate-right-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.rotate.title", "Rotate"),
component: Rotate,
component: lazy(() => import("@app/tools/Rotate")),
description: t("home.rotate.desc", "Easily rotate your PDFs."),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
maxFiles: -1,
endpoints: ["rotate-pdf"],
operationConfig: rotateOperationConfig,
automationSettings: RotateAutomationSettings,
automationSettings: lazy(() => import("@app/components/tools/rotate/RotateAutomationSettings")),
synonyms: getSynonyms(t, "rotate")
},
split: {
icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.split.title", "Split"),
component: SplitPdfPanel,
component: lazy(() => import("@app/tools/Split")),
description: t("home.split.desc", "Split PDFs into multiple documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
endpoints: Array.from(new Set(Object.values(SPLIT_ENDPOINT_NAMES))),
operationConfig: splitOperationConfig,
automationSettings: SplitAutomationSettings,
automationSettings: lazy(() => import("@app/components/tools/split/SplitAutomationSettings")),
synonyms: getSynonyms(t, "split")
},
reorganizePages: {
icon: <LocalIcon icon="move-down-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.reorganizePages.title", "Reorganize Pages"),
component: ReorganizePages,
component: lazy(() => import("@app/tools/ReorganizePages")),
description: t(
"home.reorganizePages.desc",
"Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."
@@ -471,24 +392,24 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
scalePages: {
icon: <LocalIcon icon="crop-free-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.scalePages.title", "Adjust page size/scale"),
component: AdjustPageScale,
component: lazy(() => import("@app/tools/AdjustPageScale")),
description: t("home.scalePages.desc", "Change the size/scale of a page and/or its contents."),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
maxFiles: -1,
endpoints: ["scale-pages"],
operationConfig: adjustPageScaleOperationConfig,
automationSettings: AdjustPageScaleSettings,
automationSettings: lazy(() => import("@app/components/tools/adjustPageScale/AdjustPageScaleSettings")),
synonyms: getSynonyms(t, "scalePages")
},
addPageNumbers: {
icon: <LocalIcon icon="123-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.addPageNumbers.title", "Add Page Numbers"),
component: AddPageNumbers,
component: lazy(() => import("@app/tools/AddPageNumbers")),
description: t("home.addPageNumbers.desc", "Add Page numbers throughout a document in a set location"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
automationSettings: AddPageNumbersAutomationSettings,
automationSettings: lazy(() => import("@app/components/tools/addPageNumbers/AddPageNumbersAutomationSettings")),
maxFiles: -1,
endpoints: ["add-page-numbers"],
operationConfig: addPageNumbersOperationConfig,
@@ -497,21 +418,21 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
pageLayout: {
icon: <LocalIcon icon="dashboard-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.pageLayout.title", "Multi-Page Layout"),
component: PageLayout,
component: lazy(() => import("@app/tools/PageLayout")),
description: t("home.pageLayout.desc", "Merge multiple pages of a PDF document into a single page"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
maxFiles: -1,
endpoints: ["multi-page-layout"],
automationSettings: PageLayoutSettings,
automationSettings: lazy(() => import("@app/components/tools/pageLayout/PageLayoutSettings")),
synonyms: getSynonyms(t, "pageLayout")
},
bookletImposition: {
icon: <LocalIcon icon="menu-book-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.bookletImposition.title", "Booklet Imposition"),
component: BookletImposition,
component: lazy(() => import("@app/tools/BookletImposition")),
operationConfig: bookletImpositionOperationConfig,
automationSettings: BookletImpositionSettings,
automationSettings: lazy(() => import("@app/components/tools/bookletImposition/BookletImpositionSettings")),
description: t("home.bookletImposition.desc", "Create booklets with proper page ordering and multi-page layout for printing and binding"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
@@ -521,7 +442,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
icon: <LocalIcon icon="looks-one-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.pdfToSinglePage.title", "PDF to Single Large Page"),
component: SingleLargePage,
component: lazy(() => import("@app/tools/SingleLargePage")),
description: t("home.pdfToSinglePage.desc", "Merges all PDF pages into one large single page"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
@@ -535,7 +456,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
addAttachments: {
icon: <LocalIcon icon="attachment-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.addAttachments.title", "Add Attachments"),
component: AddAttachments,
component: lazy(() => import("@app/tools/AddAttachments")),
description: t("home.addAttachments.desc", "Add or remove embedded files (attachments) to/from a PDF"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
@@ -543,7 +464,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
maxFiles: 1,
endpoints: ["add-attachments"],
operationConfig: addAttachmentsOperationConfig,
automationSettings: AddAttachmentsSettings,
automationSettings: lazy(() => import("@app/components/tools/addAttachments/AddAttachmentsSettings")),
},
// Extraction
@@ -551,26 +472,26 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
extractPages: {
icon: <LocalIcon icon="upload-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.extractPages.title", "Extract Pages"),
component: ExtractPages,
component: lazy(() => import("@app/tools/ExtractPages")),
description: t("home.extractPages.desc", "Extract specific pages from a PDF document"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.EXTRACTION,
synonyms: getSynonyms(t, "extractPages"),
automationSettings: ExtractPagesSettings,
automationSettings: lazy(() => import("@app/components/tools/extractPages/ExtractPagesSettings")),
operationConfig: extractPagesOperationConfig,
endpoints: ["rearrange-pages"],
},
extractImages: {
icon: <LocalIcon icon="photo-library-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.extractImages.title", "Extract Images"),
component: ExtractImages,
component: lazy(() => import("@app/tools/ExtractImages")),
description: t("home.extractImages.desc", "Extract images from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.EXTRACTION,
maxFiles: -1,
endpoints: ["extract-images"],
operationConfig: extractImagesOperationConfig,
automationSettings: ExtractImagesSettings,
automationSettings: lazy(() => import("@app/components/tools/extractImages/ExtractImagesSettings")),
synonyms: getSynonyms(t, "extractImages")
},
@@ -579,7 +500,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
removePages: {
icon: <LocalIcon icon="delete-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removePages.title", "Remove Pages"),
component: RemovePages,
component: lazy(() => import("@app/tools/RemovePages")),
description: t("home.removePages.desc", "Remove specific pages from a PDF document"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL,
@@ -587,12 +508,12 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
endpoints: ["remove-pages"],
synonyms: getSynonyms(t, "removePages"),
operationConfig: removePagesOperationConfig,
automationSettings: RemovePagesSettings,
automationSettings: lazy(() => import("@app/components/tools/removePages/RemovePagesSettings")),
},
removeBlanks: {
icon: <LocalIcon icon="scan-delete-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removeBlanks.title", "Remove Blank Pages"),
component: RemoveBlanks,
component: lazy(() => import("@app/tools/RemoveBlanks")),
description: t("home.removeBlanks.desc", "Remove blank pages from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL,
@@ -600,12 +521,12 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
endpoints: ["remove-blanks"],
synonyms: getSynonyms(t, "removeBlanks"),
operationConfig: removeBlanksOperationConfig,
automationSettings: RemoveBlanksSettings,
automationSettings: lazy(() => import("@app/components/tools/removeBlanks/RemoveBlanksSettings")),
},
removeAnnotations: {
icon: <LocalIcon icon="thread-unread-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removeAnnotations.title", "Remove Annotations"),
component: RemoveAnnotations,
component: lazy(() => import("@app/tools/RemoveAnnotations")),
description: t("home.removeAnnotations.desc", "Remove annotations and comments from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL,
@@ -618,7 +539,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
removeImage: {
icon: <LocalIcon icon="remove-selection-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removeImage.title", "Remove Images"),
component: RemoveImage,
component: lazy(() => import("@app/tools/RemoveImage")),
description: t("home.removeImage.desc", "Remove all images from a PDF document"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL,
@@ -631,20 +552,20 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
removePassword: {
icon: <LocalIcon icon="lock-open-right-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removePassword.title", "Remove Password"),
component: RemovePassword,
component: lazy(() => import("@app/tools/RemovePassword")),
description: t("home.removePassword.desc", "Remove password protection from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL,
endpoints: ["remove-password"],
maxFiles: -1,
operationConfig: removePasswordOperationConfig,
automationSettings: RemovePasswordSettings,
automationSettings: lazy(() => import("@app/components/tools/removePassword/RemovePasswordSettings")),
synonyms: getSynonyms(t, "removePassword")
},
removeCertSign: {
icon: <LocalIcon icon="remove-moderator-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.removeCertSign.title", "Remove Certificate Sign"),
component: RemoveCertificateSign,
component: lazy(() => import("@app/tools/RemoveCertificateSign")),
description: t("home.removeCertSign.desc", "Remove digital signature from PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL,
@@ -660,7 +581,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
automate: {
icon: <LocalIcon icon="automation-outline" width="1.5rem" height="1.5rem" />,
name: t("home.automate.title", "Automate"),
component: Automate,
component: lazy(() => import("@app/tools/Automate")),
description: t(
"home.automate.desc",
"Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."
@@ -676,7 +597,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
autoRename: {
icon: <LocalIcon icon="match-word-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.autoRename.title", "Auto Rename PDF File"),
component: AutoRename,
component: lazy(() => import("@app/tools/AutoRename")),
maxFiles: -1,
endpoints: ["auto-rename"],
operationConfig: autoRenameOperationConfig,
@@ -692,18 +613,18 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
adjustContrast: {
icon: <LocalIcon icon="palette" width="1.5rem" height="1.5rem" />,
name: t("home.adjustContrast.title", "Adjust Colors/Contrast"),
component: AdjustContrast,
component: lazy(() => import("@app/tools/AdjustContrast")),
description: t("home.adjustContrast.desc", "Adjust colors and contrast of PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
operationConfig: adjustContrastOperationConfig,
automationSettings: AdjustContrastSingleStepSettings,
automationSettings: lazy(() => import("@app/components/tools/adjustContrast/AdjustContrastSingleStepSettings")),
synonyms: getSynonyms(t, "adjustContrast"),
},
repair: {
icon: <LocalIcon icon="build-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.repair.title", "Repair"),
component: Repair,
component: lazy(() => import("@app/tools/Repair")),
description: t("home.repair.desc", "Repair corrupted or damaged PDF files"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
@@ -716,39 +637,39 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
scannerImageSplit: {
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.scannerImageSplit.title", "Detect & Split Scanned Photos"),
component: ScannerImageSplit,
component: lazy(() => import("@app/tools/ScannerImageSplit")),
description: t("home.scannerImageSplit.desc", "Detect and split scanned photos into separate pages"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
maxFiles: -1,
endpoints: ["extract-image-scans"],
operationConfig: scannerImageSplitOperationConfig,
automationSettings: ScannerImageSplitSettings,
automationSettings: lazy(() => import("@app/components/tools/scannerImageSplit/ScannerImageSplitSettings")),
synonyms: getSynonyms(t, "ScannerImageSplit"),
},
overlayPdfs: {
icon: <LocalIcon icon="layers-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.overlay-pdfs.title", "Overlay PDFs"),
component: OverlayPdfs,
component: lazy(() => import("@app/tools/OverlayPdfs")),
description: t("home.overlay-pdfs.desc", "Overlay one PDF on top of another"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
endpoints: ["overlay-pdf"],
operationConfig: overlayPdfsOperationConfig,
synonyms: getSynonyms(t, "overlay-pdfs"),
automationSettings: OverlayPdfsSettings
automationSettings: lazy(() => import("@app/components/tools/overlayPdfs/OverlayPdfsSettings"))
},
replaceColor: {
icon: <LocalIcon icon="format-color-fill-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.replaceColor.title", "Replace & Invert Color"),
component: ReplaceColor,
component: lazy(() => import("@app/tools/ReplaceColor")),
description: t("home.replaceColor.desc", "Replace or invert colors in PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
maxFiles: -1,
endpoints: ["replace-invert-pdf"],
operationConfig: replaceColorOperationConfig,
automationSettings: ReplaceColorSettings,
automationSettings: lazy(() => import("@app/components/tools/replaceColor/ReplaceColorSettings")),
synonyms: getSynonyms(t, "replaceColor"),
},
scannerEffect: {
@@ -768,7 +689,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
showJS: {
icon: <LocalIcon icon="javascript-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.showJS.title", "Show JavaScript"),
component: ShowJS,
component: lazy(() => import("@app/tools/ShowJS")),
description: t("home.showJS.desc", "Extract and display JavaScript code from PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
@@ -831,7 +752,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
compare: {
icon: <LocalIcon icon="compare-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.compare.title", "Compare"),
component: Compare,
component: lazy(() => import("@app/tools/Compare")),
description: t("home.compare.desc", "Compare two PDF documents and highlight differences"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
@@ -844,20 +765,20 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
compress: {
icon: <LocalIcon icon="zoom-in-map-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.compress.title", "Compress"),
component: CompressPdfPanel,
component: lazy(() => import("@app/tools/Compress")),
description: t("home.compress.desc", "Compress PDFs to reduce their file size."),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1,
endpoints: ["compress-pdf"],
operationConfig: compressOperationConfig,
automationSettings: CompressSettings,
automationSettings: lazy(() => import("@app/components/tools/compress/CompressSettings")),
synonyms: getSynonyms(t, "compress")
},
convert: {
icon: <LocalIcon icon="sync-alt-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.convert.title", "Convert"),
component: ConvertPanel,
component: lazy(() => import("@app/tools/Convert")),
description: t("home.convert.desc", "Convert files to and from PDF format"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
@@ -881,34 +802,34 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
],
operationConfig: convertOperationConfig,
automationSettings: ConvertSettings,
automationSettings: lazy(() => import("@app/components/tools/convert/ConvertSettings")),
synonyms: getSynonyms(t, "convert")
},
ocr: {
icon: <LocalIcon icon="quick-reference-all-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.ocr.title", "OCR"),
component: OCRPanel,
component: lazy(() => import("@app/tools/OCR")),
description: t("home.ocr.desc", "Extract text from scanned PDFs using Optical Character Recognition"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1,
endpoints: ["ocr-pdf"],
operationConfig: ocrOperationConfig,
automationSettings: OCRSettings,
automationSettings: lazy(() => import("@app/components/tools/ocr/OCRSettings")),
synonyms: getSynonyms(t, "ocr")
},
redact: {
icon: <LocalIcon icon="visibility-off-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.redact.title", "Redact"),
component: Redact,
component: lazy(() => import("@app/tools/Redact")),
description: t("home.redact.desc", "Permanently remove sensitive information from PDF documents"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1,
endpoints: ["auto-redact"],
operationConfig: redactOperationConfig,
automationSettings: RedactSingleStepSettings,
automationSettings: lazy(() => import("@app/components/tools/redact/RedactSingleStepSettings")),
synonyms: getSynonyms(t, "redact")
},
};
@@ -1,5 +1,6 @@
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import JSZip from 'jszip';
import { OCRParameters, defaultParameters } from '@app/hooks/tools/ocr/useOCRParameters';
import { useToolOperation, ToolOperationConfig, ToolType } from '@app/hooks/tools/shared/useToolOperation';
import { createStandardErrorHandler } from '@app/utils/toolErrorHandler';
@@ -18,8 +19,7 @@ function getMimeType(filename: string): string {
// Lightweight ZIP extractor (keep or replace with a shared util if you have one)
async function extractZipFile(zipBlob: Blob): Promise<File[]> {
const JSZip = await import('jszip');
const zip = new JSZip.default();
const zip = new JSZip();
const zipContent = await zip.loadAsync(await zipBlob.arrayBuffer());
const out: File[] = [];
for (const [filename, file] of Object.entries(zipContent.files)) {
+25 -12
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState, lazy, Suspense } from "react";
import { useTranslation } from "react-i18next";
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
import { Group, useMantineColorScheme } from "@mantine/core";
@@ -15,13 +15,16 @@ import { useViewer } from "@app/contexts/ViewerContext";
import AppsIcon from '@mui/icons-material/AppsRounded';
import ToolPanel from "@app/components/tools/ToolPanel";
import Workbench from "@app/components/layout/Workbench";
import QuickAccessBar from "@app/components/shared/QuickAccessBar";
import RightRail from "@app/components/shared/RightRail";
import FileManager from "@app/components/FileManager";
import LocalIcon from "@app/components/shared/LocalIcon";
// Lazy-load heavy components that aren't needed on initial render
const Workbench = lazy(() => import("@app/components/layout/Workbench"));
const FileManager = lazy(() => import("@app/components/FileManager"));
const AppConfigModal = lazy(() => import("@app/components/shared/AppConfigModal"));
import { useFilesModalContext } from "@app/contexts/FilesModalContext";
import AppConfigModal from "@app/components/shared/AppConfigModal";
import "@app/pages/HomePage.css";
@@ -218,7 +221,9 @@ export default function HomePage() {
<div className="mobile-slide" aria-label={t('home.mobile.workbenchSlide', 'Workspace panel')}>
<div className="mobile-slide-content">
<div className="flex-1 min-h-0 flex">
<Workbench />
<Suspense fallback={<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>Loading...</div>}>
<Workbench />
</Suspense>
<RightRail />
</div>
</div>
@@ -268,11 +273,15 @@ export default function HomePage() {
<span className="mobile-bottom-button-label">{t('quickAccess.config', 'Config')}</span>
</button>
</div>
<FileManager selectedTool={selectedTool as any /* FIX ME */} />
<AppConfigModal
opened={configModalOpen}
onClose={() => setConfigModalOpen(false)}
/>
<Suspense fallback={null}>
<FileManager selectedTool={selectedTool as any /* FIX ME */} />
</Suspense>
<Suspense fallback={null}>
<AppConfigModal
opened={configModalOpen}
onClose={() => setConfigModalOpen(false)}
/>
</Suspense>
</div>
) : (
<Group
@@ -283,9 +292,13 @@ export default function HomePage() {
>
<QuickAccessBar ref={quickAccessRef} />
<ToolPanel />
<Workbench />
<Suspense fallback={<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>Loading...</div>}>
<Workbench />
</Suspense>
<RightRail />
<FileManager selectedTool={selectedTool as any /* FIX ME */} />
<Suspense fallback={null}>
<FileManager selectedTool={selectedTool as any /* FIX ME */} />
</Suspense>
</Group>
)}
</div>
+22 -6
View File
@@ -3,9 +3,14 @@
*
* Prevents infinite worker creation by managing PDF.js workers globally
* and ensuring proper cleanup when operations complete.
*
* Uses dynamic imports to avoid bundling PDF.js in main bundle.
*/
import { GlobalWorkerOptions, getDocument, PDFDocumentProxy } from 'pdfjs-dist/legacy/build/pdf.mjs';
// Dynamic import types for TypeScript
type PDFDocumentProxy = import('pdfjs-dist').PDFDocumentProxy;
type GlobalWorkerOptions = typeof import('pdfjs-dist').GlobalWorkerOptions;
type GetDocumentFn = typeof import('pdfjs-dist').getDocument;
class PDFWorkerManager {
private static instance: PDFWorkerManager;
@@ -13,9 +18,10 @@ class PDFWorkerManager {
private workerCount = 0;
private maxWorkers = 10; // Limit concurrent workers
private isInitialized = false;
private pdfjsLib: { GlobalWorkerOptions: GlobalWorkerOptions; getDocument: GetDocumentFn } | null = null;
private constructor() {
this.initializeWorker();
// Don't initialize worker in constructor - defer until first use
}
static getInstance(): PDFWorkerManager {
@@ -26,11 +32,14 @@ class PDFWorkerManager {
}
/**
* Initialize PDF.js worker once globally
* Lazy-load PDF.js library and initialize worker once globally
*/
private initializeWorker(): void {
private async initializeWorker(): Promise<void> {
if (!this.isInitialized) {
GlobalWorkerOptions.workerSrc = new URL(
// Dynamic import - only loads when first PDF operation is needed
this.pdfjsLib = await import('pdfjs-dist/legacy/build/pdf.mjs');
this.pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
import.meta.url
).toString();
@@ -51,6 +60,13 @@ class PDFWorkerManager {
verbosity?: number;
} = {}
): Promise<PDFDocumentProxy> {
// Lazy-load PDF.js on first use
await this.initializeWorker();
if (!this.pdfjsLib) {
throw new Error('Failed to load PDF.js library');
}
// Wait if we've hit the worker limit
if (this.activeDocuments.size >= this.maxWorkers) {
await this.waitForAvailableWorker();
@@ -68,7 +84,7 @@ class PDFWorkerManager {
pdfData = data; // Pass through as-is
}
const loadingTask = getDocument(
const loadingTask = this.pdfjsLib.getDocument(
typeof pdfData === 'string' ? {
url: pdfData,
disableAutoFetch: options.disableAutoFetch ?? true,
@@ -14,6 +14,7 @@ interface SaaSLoginScreenProps {
onLogin: (username: string, password: string) => Promise<void>;
onOAuthSuccess: (userInfo: UserInfo) => Promise<void>;
onSelfHostedClick: () => void;
onSwitchToSignup: () => void;
loading: boolean;
error: string | null;
}
@@ -23,6 +24,7 @@ export const SaaSLoginScreen: React.FC<SaaSLoginScreenProps> = ({
onLogin,
onOAuthSuccess,
onSelfHostedClick,
onSwitchToSignup,
loading,
error,
}) => {
@@ -89,6 +91,20 @@ export const SaaSLoginScreen: React.FC<SaaSLoginScreenProps> = ({
submitButtonText={t('setup.login.submit', 'Login')}
/>
<div className="navigation-link-container" style={{ marginTop: '0.5rem', textAlign: 'right' }}>
<button
type="button"
onClick={() => {
setValidationError(null);
onSwitchToSignup();
}}
className="navigation-link-button"
disabled={loading}
>
{t('signup.signUp', 'Sign Up')}
</button>
</div>
<SelfHostedLink onClick={onSelfHostedClick} disabled={loading} />
</>
);
@@ -0,0 +1,104 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import LoginHeader from '@app/routes/login/LoginHeader';
import ErrorMessage from '@app/routes/login/ErrorMessage';
import SignupForm from '@app/routes/signup/SignupForm';
import { useSignupFormValidation, SignupFieldErrors } from '@app/routes/signup/SignupFormValidation';
import { authService } from '@app/services/authService';
import '@app/routes/authShared/auth.css';
interface SaaSSignupScreenProps {
loading: boolean;
error: string | null;
onLogin: (username: string, password: string) => Promise<void>;
onSwitchToLogin: () => void;
}
export const SaaSSignupScreen: React.FC<SaaSSignupScreenProps> = ({
loading,
error,
onLogin: _onLogin,
onSwitchToLogin: _onSwitchToLogin,
}) => {
const { t } = useTranslation();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [validationError, setValidationError] = useState<string | null>(null);
const [signupFieldErrors, setSignupFieldErrors] = useState<SignupFieldErrors>({});
const [signupSuccessMessage, setSignupSuccessMessage] = useState<string | null>(null);
const [isSignupSubmitting, setIsSignupSubmitting] = useState(false);
const { validateSignupForm } = useSignupFormValidation();
const displayError = error || validationError;
const handleSignupSubmit = async () => {
setValidationError(null);
setSignupSuccessMessage(null);
setSignupFieldErrors({});
const validation = validateSignupForm(email, password, confirmPassword);
if (!validation.isValid) {
setValidationError(validation.error);
setSignupFieldErrors(validation.fieldErrors || {});
return;
}
try {
setIsSignupSubmitting(true);
await authService.signUpSaas(email.trim(), password);
setSignupSuccessMessage(t('signup.checkEmailConfirmation', 'Check your email for a confirmation link to complete your registration.'));
setSignupFieldErrors({});
setValidationError(null);
} catch (err) {
setSignupSuccessMessage(null);
const message = err instanceof Error ? err.message : t('signup.unexpectedError', { message: 'Unknown error' });
setValidationError(message);
} finally {
setIsSignupSubmitting(false);
}
};
return (
<>
<LoginHeader
title={t('signup.title', 'Create an account')}
subtitle={t('signup.subtitle', 'Join Stirling PDF')}
/>
<ErrorMessage error={displayError} />
{signupSuccessMessage && (
<div className="success-message">
<p className="success-message-text">{signupSuccessMessage}</p>
</div>
)}
<SignupForm
email={email}
password={password}
confirmPassword={confirmPassword}
setEmail={(value) => {
setEmail(value);
setValidationError(null);
setSignupFieldErrors({});
}}
setPassword={(value) => {
setPassword(value);
setValidationError(null);
setSignupFieldErrors({});
}}
setConfirmPassword={(value) => {
setConfirmPassword(value);
setValidationError(null);
setSignupFieldErrors({});
}}
onSubmit={handleSignupSubmit}
isSubmitting={loading || isSignupSubmitting}
fieldErrors={signupFieldErrors}
showName={false}
showTerms={false}
/>
</>
);
};
@@ -2,16 +2,20 @@ import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { DesktopAuthLayout } from '@app/components/SetupWizard/DesktopAuthLayout';
import { SaaSLoginScreen } from '@app/components/SetupWizard/SaaSLoginScreen';
import { SaaSSignupScreen } from '@app/components/SetupWizard/SaaSSignupScreen';
import { ServerSelectionScreen } from '@app/components/SetupWizard/ServerSelectionScreen';
import { SelfHostedLoginScreen } from '@app/components/SetupWizard/SelfHostedLoginScreen';
import { ServerConfig, connectionModeService } from '@app/services/connectionModeService';
import { authService, UserInfo } from '@app/services/authService';
import { tauriBackendService } from '@app/services/tauriBackendService';
import { STIRLING_SAAS_URL } from '@desktop/constants/connection';
import { listen } from '@tauri-apps/api/event';
import { useEffect } from 'react';
import '@app/routes/authShared/auth.css';
enum SetupStep {
SaaSLogin,
SaaSSignup,
ServerSelection,
SelfHostedLogin,
}
@@ -80,6 +84,16 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
setActiveStep(SetupStep.ServerSelection);
};
const handleSwitchToSignup = () => {
setError(null);
setActiveStep(SetupStep.SaaSSignup);
};
const handleSwitchToLogin = () => {
setError(null);
setActiveStep(SetupStep.SaaSLogin);
};
const handleServerSelection = (config: ServerConfig) => {
setServerConfig(config);
setError(null);
@@ -128,6 +142,48 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
}
};
useEffect(() => {
const unsubscribePromise = listen<string>('deep-link', async (event) => {
const url = event.payload;
if (!url) return;
try {
const parsed = new URL(url);
// Supabase sends tokens in the URL hash
const hash = parsed.hash.replace(/^#/, '');
const params = new URLSearchParams(hash);
const accessToken = params.get('access_token');
const type = params.get('type') || parsed.searchParams.get('type');
if (!type || (type !== 'signup' && type !== 'recovery' && type !== 'magiclink')) {
return;
}
if (!accessToken) {
console.error('[SetupWizard] Deep link missing access_token');
return;
}
setLoading(true);
setError(null);
await authService.completeSupabaseSession(accessToken, serverConfig?.url || STIRLING_SAAS_URL);
await connectionModeService.switchToSaaS(serverConfig?.url || STIRLING_SAAS_URL);
tauriBackendService.startBackend().catch(console.error);
onComplete();
} catch (err) {
console.error('[SetupWizard] Failed to handle deep link', err);
setError(err instanceof Error ? err.message : 'Failed to complete signup');
setLoading(false);
}
});
return () => {
void unsubscribePromise.then((unsub) => unsub());
};
}, [onComplete, serverConfig?.url]);
const handleBack = () => {
setError(null);
if (activeStep === SetupStep.SelfHostedLogin) {
@@ -135,6 +191,8 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
} else if (activeStep === SetupStep.ServerSelection) {
setActiveStep(SetupStep.SaaSLogin);
setServerConfig({ url: STIRLING_SAAS_URL });
} else if (activeStep === SetupStep.SaaSSignup) {
setActiveStep(SetupStep.SaaSLogin);
}
};
@@ -147,11 +205,21 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
onLogin={handleSaaSLogin}
onOAuthSuccess={handleSaaSLoginOAuth}
onSelfHostedClick={handleSelfHostedClick}
onSwitchToSignup={handleSwitchToSignup}
loading={loading}
error={error}
/>
)}
{activeStep === SetupStep.SaaSSignup && (
<SaaSSignupScreen
loading={loading}
error={error}
onLogin={handleSaaSLogin}
onSwitchToLogin={handleSwitchToLogin}
/>
)}
{activeStep === SetupStep.ServerSelection && (
<ServerSelectionScreen
onSelect={handleServerSelection}
@@ -6,6 +6,12 @@
// The SaaS authentication server
export const STIRLING_SAAS_URL: string = import.meta.env.VITE_SAAS_SERVER_URL || '';
// SaaS signup URL for creating new cloud accounts
export const STIRLING_SAAS_SIGNUP_URL: string = import.meta.env.VITE_SAAS_SIGNUP_URL || '';
// Supabase publishable key from environment variable
// Used for SaaS authentication
export const SUPABASE_KEY: string = import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb';
// Desktop deep link callback for Supabase email confirmations
export const DESKTOP_DEEP_LINK_CALLBACK = 'stirlingpdf://auth/callback';
+62 -1
View File
@@ -1,6 +1,6 @@
import { invoke } from '@tauri-apps/api/core';
import axios from 'axios';
import { STIRLING_SAAS_URL, SUPABASE_KEY } from '@app/constants/connection';
import { DESKTOP_DEEP_LINK_CALLBACK, STIRLING_SAAS_URL, SUPABASE_KEY } from '@app/constants/connection';
export interface UserInfo {
username: string;
@@ -131,6 +131,67 @@ export class AuthService {
this.notifyListeners();
}
async completeSupabaseSession(accessToken: string, serverUrl: string): Promise<UserInfo> {
if (!accessToken || !accessToken.trim()) {
throw new Error('Invalid access token');
}
if (!SUPABASE_KEY) {
throw new Error('VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY is not configured');
}
await this.saveTokenEverywhere(accessToken);
const userInfo = await this.fetchSupabaseUserInfo(serverUrl, accessToken);
await invoke('save_user_info', {
username: userInfo.username,
email: userInfo.email || null,
});
this.setAuthStatus('authenticated', userInfo);
return userInfo;
}
async signUpSaas(email: string, password: string): Promise<void> {
if (!STIRLING_SAAS_URL) {
throw new Error('VITE_SAAS_SERVER_URL is not configured');
}
if (!SUPABASE_KEY) {
throw new Error('VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY is not configured');
}
const redirectParam = encodeURIComponent(DESKTOP_DEEP_LINK_CALLBACK);
const signupUrl = `${STIRLING_SAAS_URL.replace(/\/$/, '')}/auth/v1/signup?redirect_to=${redirectParam}`;
try {
const response = await axios.post(
signupUrl,
{ email, password, email_redirect_to: DESKTOP_DEEP_LINK_CALLBACK },
{
headers: {
'Content-Type': 'application/json;charset=UTF-8',
apikey: SUPABASE_KEY,
Authorization: `Bearer ${SUPABASE_KEY}`,
},
}
);
if (response.status >= 400) {
throw new Error('Sign up failed');
}
} catch (error) {
if (axios.isAxiosError(error)) {
const message =
error.response?.data?.error_description ||
error.response?.data?.msg ||
error.response?.data?.message ||
error.message;
throw new Error(message || 'Sign up failed');
}
throw error instanceof Error ? error : new Error('Sign up failed');
}
}
async login(serverUrl: string, username: string, password: string): Promise<UserInfo> {
try {
console.log('Logging in to:', serverUrl);
@@ -1,4 +1,5 @@
import { invoke, isTauri } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
export interface FileOpenService {
getOpenedFiles(): Promise<string[]>;
@@ -60,8 +61,6 @@ class TauriFileOpenService implements FileOpenService {
// Only import if in Tauri environment
if (isTauri()) {
const { listen } = await import('@tauri-apps/api/event');
// Check again after async import
if (isCleanedUp) {
return;
+14 -8
View File
@@ -1,14 +1,18 @@
import { Suspense } from "react";
import { Suspense, lazy } from "react";
import { Routes, Route } from "react-router-dom";
import { AppProviders } from "@app/components/AppProviders";
import { AppLayout } from "@app/components/AppLayout";
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
import Landing from "@app/routes/Landing";
import Login from "@app/routes/Login";
import Signup from "@app/routes/Signup";
import AuthCallback from "@app/routes/AuthCallback";
import InviteAccept from "@app/routes/InviteAccept";
import Onboarding from "@app/components/onboarding/Onboarding";
// Lazy-load auth routes - only loaded when user navigates to them
const Landing = lazy(() => import("@app/routes/Landing"));
const Login = lazy(() => import("@app/routes/Login"));
const Signup = lazy(() => import("@app/routes/Signup"));
const AuthCallback = lazy(() => import("@app/routes/AuthCallback"));
const InviteAccept = lazy(() => import("@app/routes/InviteAccept"));
// Lazy-load Onboarding - only shown once for first-time users
const Onboarding = lazy(() => import("@app/components/onboarding/Onboarding"));
// Import global styles
import "@app/styles/tailwind.css";
@@ -34,7 +38,9 @@ export default function App() {
{/* Main app routes - Landing handles auth logic */}
<Route path="/*" element={<Landing />} />
</Routes>
<Onboarding />
<Suspense fallback={null}>
<Onboarding />
</Suspense>
</AppLayout>
</AppProviders>
</Suspense>
@@ -1,23 +1,25 @@
import React from 'react';
import React, { lazy } from 'react';
import { useTranslation } from 'react-i18next';
import { useConfigNavSections as useCoreConfigNavSections, createConfigNavSections as createCoreConfigNavSections, ConfigNavSection } from '@core/components/shared/config/configNavSections';
import PeopleSection from '@app/components/shared/config/configSections/PeopleSection';
import TeamsSection from '@app/components/shared/config/configSections/TeamsSection';
import AdminGeneralSection from '@app/components/shared/config/configSections/AdminGeneralSection';
import AdminSecuritySection from '@app/components/shared/config/configSections/AdminSecuritySection';
import AdminConnectionsSection from '@app/components/shared/config/configSections/AdminConnectionsSection';
import AdminPrivacySection from '@app/components/shared/config/configSections/AdminPrivacySection';
import AdminDatabaseSection from '@app/components/shared/config/configSections/AdminDatabaseSection';
import AdminAdvancedSection from '@app/components/shared/config/configSections/AdminAdvancedSection';
import AdminLegalSection from '@app/components/shared/config/configSections/AdminLegalSection';
import AdminPlanSection from '@app/components/shared/config/configSections/AdminPlanSection';
import AdminFeaturesSection from '@app/components/shared/config/configSections/AdminFeaturesSection';
import AdminEndpointsSection from '@app/components/shared/config/configSections/AdminEndpointsSection';
import AdminAuditSection from '@app/components/shared/config/configSections/AdminAuditSection';
import AdminUsageSection from '@app/components/shared/config/configSections/AdminUsageSection';
import ApiKeys from '@app/components/shared/config/configSections/ApiKeys';
import AccountSection from '@app/components/shared/config/configSections/AccountSection';
import GeneralSection from '@app/components/shared/config/configSections/GeneralSection';
// Lazy load all config sections
const PeopleSection = lazy(() => import('@app/components/shared/config/configSections/PeopleSection'));
const TeamsSection = lazy(() => import('@app/components/shared/config/configSections/TeamsSection'));
const AdminGeneralSection = lazy(() => import('@app/components/shared/config/configSections/AdminGeneralSection'));
const AdminSecuritySection = lazy(() => import('@app/components/shared/config/configSections/AdminSecuritySection'));
const AdminConnectionsSection = lazy(() => import('@app/components/shared/config/configSections/AdminConnectionsSection'));
const AdminPrivacySection = lazy(() => import('@app/components/shared/config/configSections/AdminPrivacySection'));
const AdminDatabaseSection = lazy(() => import('@app/components/shared/config/configSections/AdminDatabaseSection'));
const AdminAdvancedSection = lazy(() => import('@app/components/shared/config/configSections/AdminAdvancedSection'));
const AdminLegalSection = lazy(() => import('@app/components/shared/config/configSections/AdminLegalSection'));
const AdminPlanSection = lazy(() => import('@app/components/shared/config/configSections/AdminPlanSection'));
const AdminFeaturesSection = lazy(() => import('@app/components/shared/config/configSections/AdminFeaturesSection'));
const AdminEndpointsSection = lazy(() => import('@app/components/shared/config/configSections/AdminEndpointsSection'));
const AdminAuditSection = lazy(() => import('@app/components/shared/config/configSections/AdminAuditSection'));
const AdminUsageSection = lazy(() => import('@app/components/shared/config/configSections/AdminUsageSection'));
const ApiKeys = lazy(() => import('@app/components/shared/config/configSections/ApiKeys'));
const AccountSection = lazy(() => import('@app/components/shared/config/configSections/AccountSection'));
const GeneralSection = lazy(() => import('@app/components/shared/config/configSections/GeneralSection'));
/**
* Hook version of proprietary config nav sections with proper i18n support
+45 -1
View File
@@ -2,6 +2,29 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import tsconfigPaths from 'vite-tsconfig-paths';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import { visualizer } from "rollup-plugin-visualizer";
import type { OutputBundle } from "rollup";
const maxChunkSize = 500;
function failOnLargeChunks(limitKb: number) {
return {
name: "fail-on-large-chunks",
generateBundle(_: unknown, bundle: OutputBundle) {
for (const [fileName, chunk] of Object.entries(bundle)) {
if (chunk.type === "chunk") {
const sizeKb = Buffer.byteLength(chunk.code) / 1024;
if (sizeKb > limitKb) {
throw new Error(
`❌ Chunk "${fileName}" is ${sizeKb.toFixed(1)} KB (limit: ${limitKb} KB)`
);
}
}
}
},
};
}
export default defineConfig(({ mode }) => {
// When DISABLE_ADDITIONAL_FEATURES is false (or unset), enable proprietary features
@@ -17,6 +40,10 @@ export default defineConfig(({ mode }) => {
return {
plugins: [
visualizer({
open: true,
filename: "stats.html"
}),
react(),
tsconfigPaths({
projects: [tsconfigProject],
@@ -29,8 +56,25 @@ export default defineConfig(({ mode }) => {
dest: 'pdfium'
}
]
})
}),
failOnLargeChunks(maxChunkSize),
],
build: {
chunkSizeWarningLimit: maxChunkSize,
rollupOptions: {
output: {
// Manual chunks for better code splitting
manualChunks: {
// Large UI libraries
'vendor-ui': ['@mantine/core', '@mantine/hooks', '@mantine/dates', '@mantine/dropzone'],
// React and related
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
// Other large dependencies
'vendor-utils': ['jszip'],
},
},
},
},
server: {
host: true,
// make sure this port matches the devUrl port in tauri.conf.json file