Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f15f559bd | ||
|
|
4079542369 | ||
|
|
1c27687099 | ||
|
|
09711670ad | ||
|
|
933f103dc6 | ||
|
|
e31cb01f81 | ||
|
|
a8a849e848 | ||
|
|
14ed892f51 | ||
|
|
18f85b1aa8 | ||
|
|
d38e5692e7 | ||
|
|
f1fec4e0d4 | ||
|
|
cdeebc010d | ||
|
|
dd717fc69b | ||
|
|
19ff2444a8 | ||
|
|
2148bc2b5c | ||
|
|
8ec664d61f | ||
|
|
a544092c36 | ||
|
|
a2c2579f38 | ||
|
|
a256fd9593 | ||
|
|
28c70cbae7 | ||
|
|
f82b8a85c0 | ||
|
|
ba80ca1f67 | ||
|
|
0bd4d26638 | ||
|
|
6be4c98f7c | ||
|
|
bb2b321b28 | ||
|
|
6d98b0b209 | ||
|
|
7205ba9c9a | ||
|
|
cf2c7517eb | ||
|
|
e88c69be70 | ||
|
|
02dfafb254 | ||
|
|
404e353183 | ||
|
|
7bc9a40487 | ||
|
|
a4fd02bc58 | ||
|
|
b2a52eca79 | ||
|
|
31fda096ec | ||
|
|
0d6966de92 | ||
|
|
976fb958fd | ||
|
|
ac88125bf5 | ||
|
|
7f406774e3 | ||
|
|
d9e03ccf2c | ||
|
|
cbf48b409b | ||
|
|
8f84bb1349 | ||
|
|
81374d9b7c | ||
|
|
e4db9e183d | ||
|
|
928a591839 | ||
|
|
634eb564d6 | ||
|
|
82cf8cfde4 | ||
|
|
d70ec668f1 | ||
|
|
535c95b1cb | ||
|
|
b3c1b4791c |
@@ -1,74 +0,0 @@
|
||||
# Node modules and build artifacts
|
||||
node_modules
|
||||
frontend/node_modules
|
||||
frontend/dist
|
||||
frontend/build
|
||||
frontend/.vite
|
||||
frontend/.tauri
|
||||
|
||||
# Gradle build artifacts
|
||||
.gradle
|
||||
build
|
||||
bin
|
||||
target
|
||||
out
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Java compiled files
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
# Test reports
|
||||
test-results
|
||||
coverage
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
.dockerignore
|
||||
|
||||
# Temporary files
|
||||
tmp
|
||||
temp
|
||||
*.tmp
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# Runtime database and config files (locked by running app)
|
||||
app/core/configs/**
|
||||
stirling/**
|
||||
stirling-pdf-DB*.mv.db
|
||||
stirling-pdf-DB*.trace.db
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
!README.md
|
||||
docs
|
||||
|
||||
# CI/CD
|
||||
.github
|
||||
.gitlab-ci.yml
|
||||
@@ -17,11 +17,18 @@ on:
|
||||
branches: [main, V2]
|
||||
paths:
|
||||
- 'frontend/src-tauri/**'
|
||||
- 'frontend/src/desktop/**'
|
||||
- 'frontend/tsconfig.desktop.json'
|
||||
- 'frontend/src/**'
|
||||
- 'frontend/package.json'
|
||||
- 'frontend/package-lock.json'
|
||||
- '.github/workflows/tauri-build.yml'
|
||||
push:
|
||||
branches: [main, V2]
|
||||
paths:
|
||||
- 'frontend/src-tauri/**'
|
||||
- 'frontend/src/**'
|
||||
- 'frontend/package.json'
|
||||
- 'frontend/package-lock.json'
|
||||
- '.github/workflows/tauri-build.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -40,19 +47,21 @@ jobs:
|
||||
"windows")
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"macos")
|
||||
echo 'matrix={"include":[{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
# "macos")
|
||||
# echo 'matrix={"include":[{"platform":"macos-latest","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-13","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
# ;;
|
||||
"linux")
|
||||
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
*)
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
# Disabled Mac builds: {"platform":"macos-latest","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-13","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# For PR/push events, build all platforms
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
# Disabled Mac builds: {"platform":"macos-latest","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-13","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}
|
||||
fi
|
||||
|
||||
build:
|
||||
@@ -87,7 +96,7 @@ jobs:
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
targets: ${{ (matrix.platform == 'macos-latest' || matrix.platform == 'macos-13') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
|
||||
|
||||
@@ -174,96 +183,80 @@ jobs:
|
||||
working-directory: ./frontend
|
||||
run: npm install
|
||||
|
||||
- name: Import Apple Developer Certificate
|
||||
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
echo "Importing Apple Developer Certificate..."
|
||||
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||
# Create temporary keychain
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
# Import certificate
|
||||
security import certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
# Clean up
|
||||
rm certificate.p12
|
||||
# Disabled Mac builds - Import Apple Developer Certificate
|
||||
# - name: Import Apple Developer Certificate
|
||||
# if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-13'
|
||||
# env:
|
||||
# APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
# APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
# APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
# APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
# KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
# run: |
|
||||
# echo "Importing Apple Developer Certificate..."
|
||||
# echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||
# security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
# security default-keychain -s build.keychain
|
||||
# security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
# security set-keychain-settings -t 3600 -u build.keychain
|
||||
# security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||
# security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
# security find-identity -v -p codesigning build.keychain
|
||||
# - name: Verify Certificate
|
||||
# if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-13'
|
||||
# run: |
|
||||
# echo "Verifying Apple Developer Certificate..."
|
||||
# CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
|
||||
# echo "Certificate Info: $CERT_INFO"
|
||||
# CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||
# echo "Certificate ID: $CERT_ID"
|
||||
# echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
||||
# echo "Certificate imported."
|
||||
|
||||
- name: Verify Certificate
|
||||
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
||||
run: |
|
||||
echo "Verifying Apple Developer Certificate..."
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
CERT_INFO=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | grep "Developer ID Application")
|
||||
echo "Certificate Info: $CERT_INFO"
|
||||
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||
echo "Certificate ID: $CERT_ID"
|
||||
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> $GITHUB_ENV
|
||||
echo "Certificate imported successfully."
|
||||
# - name: Check DMG creation dependencies (macOS only)
|
||||
# if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-13'
|
||||
# run: |
|
||||
# echo "🔍 Checking DMG creation dependencies on ${{ matrix.platform }}..."
|
||||
# echo "hdiutil version: $(hdiutil --version || echo 'NOT FOUND')"
|
||||
# echo "create-dmg availability: $(which create-dmg || echo 'NOT FOUND')"
|
||||
# echo "Available disk space: $(df -h /tmp | tail -1)"
|
||||
# echo "macOS version: $(sw_vers -productVersion)"
|
||||
# echo "Available tools:"
|
||||
# ls -la /usr/bin/hd* || echo "No hd* tools found"
|
||||
|
||||
- name: Check DMG creation dependencies (macOS only)
|
||||
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
||||
run: |
|
||||
echo "🔍 Checking DMG creation dependencies on ${{ matrix.platform }}..."
|
||||
echo "hdiutil version: $(hdiutil --version || echo 'NOT FOUND')"
|
||||
echo "create-dmg availability: $(which create-dmg || echo 'NOT FOUND')"
|
||||
echo "Available disk space: $(df -h /tmp | tail -1)"
|
||||
echo "macOS version: $(sw_vers -productVersion)"
|
||||
echo "Available tools:"
|
||||
ls -la /usr/bin/hd* || echo "No hd* tools found"
|
||||
|
||||
- name: Build Tauri app
|
||||
- name: Build Tauri app
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }}
|
||||
SIGN: 1
|
||||
CI: true
|
||||
CI: true
|
||||
with:
|
||||
projectPath: ./frontend
|
||||
tauriScript: npx tauri
|
||||
args: ${{ matrix.args }}
|
||||
|
||||
- name: Verify notarization (macOS only)
|
||||
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
||||
run: |
|
||||
echo "🔍 Verifying notarization status..."
|
||||
cd ./frontend/src-tauri/target
|
||||
DMG_FILE=$(find . -name "*.dmg" | head -1)
|
||||
if [ -n "$DMG_FILE" ]; then
|
||||
echo "Found DMG: $DMG_FILE"
|
||||
echo "Checking notarization ticket..."
|
||||
spctl -a -vvv -t install "$DMG_FILE" || echo "⚠️ Notarization check failed or not yet complete"
|
||||
stapler validate "$DMG_FILE" || echo "⚠️ No notarization ticket attached"
|
||||
else
|
||||
echo "⚠️ No DMG file found to verify"
|
||||
fi
|
||||
|
||||
|
||||
- name: Rename artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p ./dist
|
||||
cd ./frontend/src-tauri/target
|
||||
|
||||
|
||||
# Find and rename artifacts based on platform
|
||||
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||
find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \;
|
||||
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
|
||||
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
|
||||
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
||||
find . -name "*.app" -exec cp -r {} "../../../dist/Stirling-PDF-${{ matrix.name }}.app" \;
|
||||
# Disabled Mac builds
|
||||
# elif [ "${{ matrix.platform }}" = "macos-latest" ] || [ "${{ matrix.platform }}" = "macos-13" ]; then
|
||||
# find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
||||
# find . -name "*.app" -exec cp -r {} "../../../dist/Stirling-PDF-${{ matrix.name }}.app" \;
|
||||
else
|
||||
find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \;
|
||||
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
|
||||
@@ -280,7 +273,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cd ./frontend/src-tauri/target
|
||||
|
||||
|
||||
# Check for expected artifacts based on platform
|
||||
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||
echo "Checking for Windows artifacts..."
|
||||
@@ -289,13 +282,14 @@ jobs:
|
||||
echo "❌ No Windows executable found"
|
||||
exit 1
|
||||
fi
|
||||
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
|
||||
echo "Checking for macOS artifacts..."
|
||||
find . -name "*.dmg" -o -name "*.app" | head -5
|
||||
if [ $(find . -name "*.dmg" -o -name "*.app" | wc -l) -eq 0 ]; then
|
||||
echo "❌ No macOS artifacts found"
|
||||
exit 1
|
||||
fi
|
||||
# Disabled Mac builds
|
||||
# elif [ "${{ matrix.platform }}" = "macos-latest" ] || [ "${{ matrix.platform }}" = "macos-13" ]; then
|
||||
# echo "Checking for macOS artifacts..."
|
||||
# find . -name "*.dmg" -o -name "*.app" | head -5
|
||||
# if [ $(find . -name "*.dmg" -o -name "*.app" | wc -l) -eq 0 ]; then
|
||||
# echo "❌ No macOS artifacts found"
|
||||
# exit 1
|
||||
# fi
|
||||
else
|
||||
echo "Checking for Linux artifacts..."
|
||||
find . -name "*.deb" -o -name "*.AppImage" | head -5
|
||||
@@ -304,7 +298,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
echo "✅ Build artifacts found for ${{ matrix.name }}"
|
||||
|
||||
- name: Test artifact sizes
|
||||
@@ -337,4 +331,4 @@ jobs:
|
||||
echo "❌ Some Tauri builds failed."
|
||||
echo "Please check the logs and fix any issues."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -115,46 +115,46 @@ Stirling-PDF currently supports 40 languages!
|
||||
|
||||
| Language | Progress |
|
||||
| -------------------------------------------- | -------------------------------------- |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| English (English) (en_GB) |  |
|
||||
| English (US) (en_US) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Malayalam (മലയാളം) (ml_IN) |  |
|
||||
|
||||
## Stirling PDF Enterprise
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Invite management controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/invite"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/invite")
|
||||
@Tag(
|
||||
name = "Invite",
|
||||
description =
|
||||
"""
|
||||
Invite-link generation and acceptance endpoints for onboarding new users.
|
||||
|
||||
Provides the ability to issue invitation tokens, send optional email invites,
|
||||
validate and accept invite links, and manage pending invitations for teams.
|
||||
|
||||
Typical use cases include:
|
||||
• Admin workflows for issuing time-limited invitations to external users
|
||||
• Self-service invite acceptance and team assignment
|
||||
• License limit enforcement when provisioning new accounts
|
||||
|
||||
Target users: administrators and automation scripts orchestrating user onboarding.
|
||||
""")
|
||||
public @interface InviteApi {}
|
||||
@@ -505,19 +505,10 @@ public class ApplicationProperties {
|
||||
public static class Ui {
|
||||
private String appNameNavbar;
|
||||
private List<String> languages;
|
||||
private String logoStyle = "classic"; // Options: "classic" (default) or "modern"
|
||||
|
||||
public String getAppNameNavbar() {
|
||||
return appNameNavbar != null && !appNameNavbar.trim().isEmpty() ? appNameNavbar : null;
|
||||
}
|
||||
|
||||
public String getLogoStyle() {
|
||||
// Validate and return either "modern" or "classic"
|
||||
if ("modern".equalsIgnoreCase(logoStyle)) {
|
||||
return "modern";
|
||||
}
|
||||
return "classic"; // default
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
@@ -8,6 +8,4 @@ public interface UserServiceInterface {
|
||||
long getTotalUsersCount();
|
||||
|
||||
boolean isCurrentUserAdmin();
|
||||
|
||||
boolean isCurrentUserFirstLogin();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,658 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDResources;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.*;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public final class FormUtils {
|
||||
|
||||
private FormUtils() {}
|
||||
|
||||
public static boolean hasAnyRotatedPage(PDDocument document) {
|
||||
try {
|
||||
for (PDPage page : document.getPages()) {
|
||||
int rot = page.getRotation();
|
||||
int norm = ((rot % 360) + 360) % 360;
|
||||
if (norm != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to inspect page rotations: {}", e.getMessage(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void copyAndTransformFormFields(
|
||||
PDDocument sourceDocument,
|
||||
PDDocument newDocument,
|
||||
int totalPages,
|
||||
int pagesPerSheet,
|
||||
int cols,
|
||||
int rows,
|
||||
float cellWidth,
|
||||
float cellHeight)
|
||||
throws IOException {
|
||||
|
||||
PDDocumentCatalog sourceCatalog = sourceDocument.getDocumentCatalog();
|
||||
PDAcroForm sourceAcroForm = sourceCatalog.getAcroForm();
|
||||
|
||||
if (sourceAcroForm == null || sourceAcroForm.getFields().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PDDocumentCatalog newCatalog = newDocument.getDocumentCatalog();
|
||||
PDAcroForm newAcroForm = new PDAcroForm(newDocument);
|
||||
newCatalog.setAcroForm(newAcroForm);
|
||||
|
||||
PDResources dr = new PDResources();
|
||||
PDType1Font helvetica = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||
PDType1Font zapfDingbats = new PDType1Font(Standard14Fonts.FontName.ZAPF_DINGBATS);
|
||||
dr.put(COSName.getPDFName("Helv"), helvetica);
|
||||
dr.put(COSName.getPDFName("ZaDb"), zapfDingbats);
|
||||
newAcroForm.setDefaultResources(dr);
|
||||
newAcroForm.setDefaultAppearance("/Helv 12 Tf 0 g");
|
||||
|
||||
// Do not mutate the source AcroForm; skip bad widgets during copy
|
||||
newAcroForm.setNeedAppearances(true);
|
||||
|
||||
Map<String, Integer> fieldNameCounters = new HashMap<>();
|
||||
|
||||
// Build widget -> field map once for efficient lookups
|
||||
Map<PDAnnotationWidget, PDField> widgetFieldMap = buildWidgetFieldMap(sourceAcroForm);
|
||||
|
||||
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||
PDPage sourcePage = sourceDocument.getPage(pageIndex);
|
||||
List<PDAnnotation> annotations = sourcePage.getAnnotations();
|
||||
|
||||
if (annotations.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int destinationPageIndex = pageIndex / pagesPerSheet;
|
||||
int adjustedPageIndex = pageIndex % pagesPerSheet;
|
||||
int rowIndex = adjustedPageIndex / cols;
|
||||
int colIndex = adjustedPageIndex % cols;
|
||||
|
||||
if (destinationPageIndex >= newDocument.getNumberOfPages()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PDPage destinationPage = newDocument.getPage(destinationPageIndex);
|
||||
PDRectangle sourceRect = sourcePage.getMediaBox();
|
||||
|
||||
float scaleWidth = cellWidth / sourceRect.getWidth();
|
||||
float scaleHeight = cellHeight / sourceRect.getHeight();
|
||||
float scale = Math.min(scaleWidth, scaleHeight);
|
||||
|
||||
float x = colIndex * cellWidth + (cellWidth - sourceRect.getWidth() * scale) / 2;
|
||||
float y =
|
||||
destinationPage.getMediaBox().getHeight()
|
||||
- ((rowIndex + 1) * cellHeight
|
||||
- (cellHeight - sourceRect.getHeight() * scale) / 2);
|
||||
|
||||
copyBasicFormFields(
|
||||
sourceAcroForm,
|
||||
newAcroForm,
|
||||
sourcePage,
|
||||
destinationPage,
|
||||
x,
|
||||
y,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters,
|
||||
widgetFieldMap);
|
||||
}
|
||||
|
||||
// Refresh appearances to ensure widgets render correctly across viewers
|
||||
try {
|
||||
// Use reflection to avoid compile-time dependency on PDFBox version
|
||||
Method m = newAcroForm.getClass().getMethod("refreshAppearances");
|
||||
m.invoke(newAcroForm);
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
log.warn(
|
||||
"AcroForm.refreshAppearances() not available in this PDFBox version; relying on NeedAppearances.");
|
||||
} catch (Throwable t) {
|
||||
log.warn("Failed to refresh field appearances via AcroForm: {}", t.getMessage(), t);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyBasicFormFields(
|
||||
PDAcroForm sourceAcroForm,
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage sourcePage,
|
||||
PDPage destinationPage,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters,
|
||||
Map<PDAnnotationWidget, PDField> widgetFieldMap) {
|
||||
|
||||
try {
|
||||
List<PDAnnotation> sourceAnnotations = sourcePage.getAnnotations();
|
||||
List<PDAnnotation> destinationAnnotations = destinationPage.getAnnotations();
|
||||
|
||||
for (PDAnnotation annotation : sourceAnnotations) {
|
||||
if (annotation instanceof PDAnnotationWidget widgetAnnotation) {
|
||||
if (widgetAnnotation.getRectangle() == null) {
|
||||
continue;
|
||||
}
|
||||
PDField sourceField =
|
||||
widgetFieldMap != null ? widgetFieldMap.get(widgetAnnotation) : null;
|
||||
if (sourceField == null) {
|
||||
continue; // skip widgets without a matching field
|
||||
}
|
||||
if (sourceField instanceof PDTextField pdtextfield) {
|
||||
createSimpleTextField(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
pdtextfield,
|
||||
widgetAnnotation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
} else if (sourceField instanceof PDCheckBox pdCheckBox) {
|
||||
createSimpleCheckBoxField(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
pdCheckBox,
|
||||
widgetAnnotation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
} else if (sourceField instanceof PDRadioButton pdRadioButton) {
|
||||
createSimpleRadioButtonField(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
pdRadioButton,
|
||||
widgetAnnotation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
} else if (sourceField instanceof PDComboBox pdComboBox) {
|
||||
createSimpleComboBoxField(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
pdComboBox,
|
||||
widgetAnnotation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
} else if (sourceField instanceof PDListBox pdlistbox) {
|
||||
createSimpleListBoxField(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
pdlistbox,
|
||||
widgetAnnotation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
} else if (sourceField instanceof PDSignatureField pdSignatureField) {
|
||||
createSimpleSignatureField(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
pdSignatureField,
|
||||
widgetAnnotation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
} else if (sourceField instanceof PDPushButton pdPushButton) {
|
||||
createSimplePushButtonField(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
pdPushButton,
|
||||
widgetAnnotation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to copy basic form fields for page {}: {}",
|
||||
pageIndex,
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSimpleTextField(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
PDTextField sourceField,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
try {
|
||||
PDTextField newTextField = new PDTextField(newAcroForm);
|
||||
newTextField.setDefaultAppearance("/Helv 12 Tf 0 g");
|
||||
|
||||
boolean initialized =
|
||||
initializeFieldWithWidget(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
newTextField,
|
||||
sourceField.getPartialName(),
|
||||
"textField",
|
||||
sourceWidget,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceField.getValueAsString() != null) {
|
||||
newTextField.setValue(sourceField.getValueAsString());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to create text field '{}': {}",
|
||||
sourceField.getPartialName(),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSimpleCheckBoxField(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
PDCheckBox sourceField,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
try {
|
||||
PDCheckBox newCheckBox = new PDCheckBox(newAcroForm);
|
||||
|
||||
boolean initialized =
|
||||
initializeFieldWithWidget(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
newCheckBox,
|
||||
sourceField.getPartialName(),
|
||||
"checkBox",
|
||||
sourceWidget,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceField.isChecked()) {
|
||||
newCheckBox.check();
|
||||
} else {
|
||||
newCheckBox.unCheck();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to create checkbox field '{}': {}",
|
||||
sourceField.getPartialName(),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSimpleRadioButtonField(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
PDRadioButton sourceField,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
try {
|
||||
PDRadioButton newRadioButton = new PDRadioButton(newAcroForm);
|
||||
|
||||
boolean initialized =
|
||||
initializeFieldWithWidget(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
newRadioButton,
|
||||
sourceField.getPartialName(),
|
||||
"radioButton",
|
||||
sourceWidget,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceField.getExportValues() != null) {
|
||||
newRadioButton.setExportValues(sourceField.getExportValues());
|
||||
}
|
||||
if (sourceField.getValue() != null) {
|
||||
newRadioButton.setValue(sourceField.getValue());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to create radio button field '{}': {}",
|
||||
sourceField.getPartialName(),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSimpleComboBoxField(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
PDComboBox sourceField,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
try {
|
||||
PDComboBox newComboBox = new PDComboBox(newAcroForm);
|
||||
|
||||
boolean initialized =
|
||||
initializeFieldWithWidget(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
newComboBox,
|
||||
sourceField.getPartialName(),
|
||||
"comboBox",
|
||||
sourceWidget,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceField.getOptions() != null) {
|
||||
newComboBox.setOptions(sourceField.getOptions());
|
||||
}
|
||||
if (sourceField.getValue() != null && !sourceField.getValue().isEmpty()) {
|
||||
newComboBox.setValue(sourceField.getValue());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to create combo box field '{}': {}",
|
||||
sourceField.getPartialName(),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSimpleListBoxField(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
PDListBox sourceField,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
try {
|
||||
PDListBox newListBox = new PDListBox(newAcroForm);
|
||||
|
||||
boolean initialized =
|
||||
initializeFieldWithWidget(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
newListBox,
|
||||
sourceField.getPartialName(),
|
||||
"listBox",
|
||||
sourceWidget,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceField.getOptions() != null) {
|
||||
newListBox.setOptions(sourceField.getOptions());
|
||||
}
|
||||
if (sourceField.getValue() != null && !sourceField.getValue().isEmpty()) {
|
||||
newListBox.setValue(sourceField.getValue());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to create list box field '{}': {}",
|
||||
sourceField.getPartialName(),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSimpleSignatureField(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
PDSignatureField sourceField,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
try {
|
||||
PDSignatureField newSignatureField = new PDSignatureField(newAcroForm);
|
||||
|
||||
boolean initialized =
|
||||
initializeFieldWithWidget(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
newSignatureField,
|
||||
sourceField.getPartialName(),
|
||||
"signature",
|
||||
sourceWidget,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to create signature field '{}': {}",
|
||||
sourceField.getPartialName(),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSimplePushButtonField(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
PDPushButton sourceField,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
try {
|
||||
PDPushButton newPushButton = new PDPushButton(newAcroForm);
|
||||
|
||||
boolean initialized =
|
||||
initializeFieldWithWidget(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
newPushButton,
|
||||
sourceField.getPartialName(),
|
||||
"pushButton",
|
||||
sourceWidget,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to create push button field '{}': {}",
|
||||
sourceField.getPartialName(),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T extends PDTerminalField> boolean initializeFieldWithWidget(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
T newField,
|
||||
String originalName,
|
||||
String fallbackName,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
String baseName = (originalName != null) ? originalName : fallbackName;
|
||||
String newFieldName = generateUniqueFieldName(baseName, pageIndex, fieldNameCounters);
|
||||
newField.setPartialName(newFieldName);
|
||||
|
||||
PDAnnotationWidget newWidget = new PDAnnotationWidget();
|
||||
PDRectangle sourceRect = sourceWidget.getRectangle();
|
||||
if (sourceRect == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float newX = (sourceRect.getLowerLeftX() * scale) + offsetX;
|
||||
float newY = (sourceRect.getLowerLeftY() * scale) + offsetY;
|
||||
float newWidth = sourceRect.getWidth() * scale;
|
||||
float newHeight = sourceRect.getHeight() * scale;
|
||||
newWidget.setRectangle(new PDRectangle(newX, newY, newWidth, newHeight));
|
||||
newWidget.setPage(destinationPage);
|
||||
|
||||
newField.getWidgets().add(newWidget);
|
||||
newWidget.setParent(newField);
|
||||
newAcroForm.getFields().add(newField);
|
||||
destinationAnnotations.add(newWidget);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String generateUniqueFieldName(
|
||||
String originalName, int pageIndex, Map<String, Integer> fieldNameCounters) {
|
||||
String baseName = "page" + pageIndex + "_" + originalName;
|
||||
|
||||
Integer counter = fieldNameCounters.get(baseName);
|
||||
if (counter == null) {
|
||||
counter = 0;
|
||||
} else {
|
||||
counter++;
|
||||
}
|
||||
fieldNameCounters.put(baseName, counter);
|
||||
|
||||
return counter == 0 ? baseName : baseName + "_" + counter;
|
||||
}
|
||||
|
||||
private static Map<PDAnnotationWidget, PDField> buildWidgetFieldMap(PDAcroForm acroForm) {
|
||||
Map<PDAnnotationWidget, PDField> map = new HashMap<>();
|
||||
if (acroForm == null) {
|
||||
return map;
|
||||
}
|
||||
try {
|
||||
for (PDField field : acroForm.getFieldTree()) {
|
||||
List<PDAnnotationWidget> widgets = field.getWidgets();
|
||||
if (widgets != null) {
|
||||
for (PDAnnotationWidget w : widgets) {
|
||||
if (w != null) {
|
||||
map.put(w, field);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to build widget->field map: {}", e.getMessage(), e);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
@@ -242,11 +241,6 @@ public final class RegexPatternUtils {
|
||||
return getPattern("\\s+");
|
||||
}
|
||||
|
||||
/** Pattern for matching punctuation characters */
|
||||
public Pattern getPunctuationPattern() {
|
||||
return getPattern("[\\p{Punct}]+");
|
||||
}
|
||||
|
||||
/** Pattern for matching newlines (Windows and Unix style) */
|
||||
public Pattern getNewlinesPattern() {
|
||||
return getPattern("\\r?\\n");
|
||||
@@ -292,24 +286,6 @@ public final class RegexPatternUtils {
|
||||
return getPattern("[^a-zA-Z0-9 ]");
|
||||
}
|
||||
|
||||
/** Pattern for removing bracketed indices like [0], [Child], etc. in field names */
|
||||
public Pattern getFormFieldBracketPattern() {
|
||||
return getPattern("\\[[^\\]]*\\]");
|
||||
}
|
||||
|
||||
/** Pattern that replaces underscores or hyphens with spaces */
|
||||
public Pattern getUnderscoreHyphenPattern() {
|
||||
return getPattern("[-_]+");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pattern that matches camelCase or alpha-numeric boundaries to allow inserting spaces.
|
||||
* Examples: firstName -> first Name, field1 -> field 1, A5Size -> A5 Size
|
||||
*/
|
||||
public Pattern getCamelCaseBoundaryPattern() {
|
||||
return getPattern("(?<=[a-z])(?=[A-Z])|(?<=[A-Za-z])(?=\\d)|(?<=\\d)(?=[A-Za-z])");
|
||||
}
|
||||
|
||||
/** Pattern for removing angle brackets */
|
||||
public Pattern getAngleBracketsPattern() {
|
||||
return getPattern("[<>]");
|
||||
@@ -359,26 +335,6 @@ public final class RegexPatternUtils {
|
||||
return getPattern("[1-9][0-9]{0,2}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pattern for very simple generic field tokens such as "field", "text", "checkbox" with
|
||||
* optional numeric suffix (e.g. "field 1"). Case-insensitive.
|
||||
*/
|
||||
public Pattern getGenericFieldNamePattern() {
|
||||
return getPattern(
|
||||
"^(field|text|checkbox|radio|button|signature|name|value|option|select|choice)(\\s*\\d+)?$",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
/** Pattern for short identifiers like t1, f2, a10 etc. */
|
||||
public Pattern getSimpleFormFieldPattern() {
|
||||
return getPattern("^[A-Za-z]{1,2}\\s*\\d{1,3}$");
|
||||
}
|
||||
|
||||
/** Pattern for optional leading 't' followed by digits, e.g., t1, 1, t 12. */
|
||||
public Pattern getOptionalTNumericPattern() {
|
||||
return getPattern("^(?:t\\s*)?\\d+$", Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
/** Pattern for validating mathematical expressions */
|
||||
public Pattern getMathExpressionPattern() {
|
||||
return getPattern("[0-9n+\\-*/() ]+");
|
||||
@@ -511,11 +467,6 @@ public final class RegexPatternUtils {
|
||||
return getPattern("/");
|
||||
}
|
||||
|
||||
/** Supported logical types when creating new fields programmatically */
|
||||
public Set<String> getSupportedNewFieldTypes() {
|
||||
return Set.of("text", "checkbox", "combobox", "listbox", "radio", "button", "signature");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-compile commonly used patterns for immediate availability. This eliminates first-call
|
||||
* compilation overhead for frequent patterns.
|
||||
|
||||
@@ -75,9 +75,6 @@ public class RequestUriUtils {
|
||||
|| trimmedUri.startsWith("/api/v1/auth/login")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/refresh")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/logout")
|
||||
|| trimmedUri.startsWith(
|
||||
"/api/v1/proprietary/ui-data/login") // Login page config (SSO providers +
|
||||
// enableLogin)
|
||||
|| trimmedUri.startsWith("/v1/api-docs")
|
||||
|| trimmedUri.startsWith("/api/v1/invite/validate")
|
||||
|| trimmedUri.startsWith("/api/v1/invite/accept")
|
||||
|
||||
-9
@@ -5,7 +5,6 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.common.model.api.misc.HighContrastColorCombination;
|
||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
@@ -19,7 +18,6 @@ import stirling.software.common.util.misc.ReplaceAndInvertColorStrategy;
|
||||
public class ReplaceAndInvertColorFactory {
|
||||
|
||||
private final TempFileManager tempFileManager;
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
|
||||
public ReplaceAndInvertColorStrategy replaceAndInvert(
|
||||
MultipartFile file,
|
||||
@@ -28,13 +26,6 @@ public class ReplaceAndInvertColorFactory {
|
||||
String backGroundColor,
|
||||
String textColor) {
|
||||
|
||||
// Check Ghostscript availability for CMYK conversion
|
||||
if (replaceAndInvertOption == ReplaceAndInvert.COLOR_SPACE_CONVERSION
|
||||
&& !endpointConfiguration.isGroupEnabled("Ghostscript")) {
|
||||
throw new IllegalStateException(
|
||||
"CMYK color space conversion requires Ghostscript, which is not available on this system");
|
||||
}
|
||||
|
||||
return switch (replaceAndInvertOption) {
|
||||
case CUSTOM_COLOR, HIGH_CONTRAST_COLOR ->
|
||||
new CustomColorReplaceStrategy(
|
||||
|
||||
@@ -294,12 +294,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Other", "replace-and-invert-color-pdf");
|
||||
addEndpointToGroup("Other", "multi-tool");
|
||||
|
||||
// Adding form-related endpoints to "Other" group
|
||||
addEndpointToGroup("Other", "fields");
|
||||
addEndpointToGroup("Other", "modify-fields");
|
||||
addEndpointToGroup("Other", "delete-fields");
|
||||
addEndpointToGroup("Other", "fill");
|
||||
|
||||
// Adding endpoints to "Advance" group
|
||||
addEndpointToGroup("Advance", "adjust-contrast");
|
||||
addEndpointToGroup("Advance", "compress-pdf");
|
||||
@@ -409,6 +403,8 @@ public class EndpointConfiguration {
|
||||
/* Ghostscript */
|
||||
addEndpointToGroup("Ghostscript", "repair");
|
||||
addEndpointToGroup("Ghostscript", "compress-pdf");
|
||||
addEndpointToGroup("Ghostscript", "crop");
|
||||
addEndpointToGroup("Ghostscript", "replace-invert-pdf");
|
||||
|
||||
/* tesseract */
|
||||
addEndpointToGroup("tesseract", "ocr-pdf");
|
||||
|
||||
@@ -28,8 +28,6 @@ public class InitialSetup {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
private static boolean isNewServer = false;
|
||||
|
||||
@PostConstruct
|
||||
public void init() throws IOException {
|
||||
initUUIDKey();
|
||||
@@ -90,13 +88,6 @@ public class InitialSetup {
|
||||
}
|
||||
|
||||
public void initSetAppVersion() throws IOException {
|
||||
// Check if this is a new server before setting the version
|
||||
String existingVersion = applicationProperties.getAutomaticallyGenerated().getAppVersion();
|
||||
isNewServer =
|
||||
existingVersion == null
|
||||
|| existingVersion.isEmpty()
|
||||
|| existingVersion.equals("0.0.0");
|
||||
|
||||
String appVersion = "0.0.0";
|
||||
Resource resource = new ClassPathResource("version.properties");
|
||||
Properties props = new Properties();
|
||||
@@ -108,8 +99,4 @@ public class InitialSetup {
|
||||
GeneralUtils.saveKeyToSettings("AutomaticallyGenerated.appVersion", appVersion);
|
||||
applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion);
|
||||
}
|
||||
|
||||
public static boolean isNewServer() {
|
||||
return isNewServer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
@@ -38,11 +37,6 @@ import stirling.software.common.util.WebResponseUtils;
|
||||
public class CropController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
|
||||
private boolean isGhostscriptEnabled() {
|
||||
return endpointConfiguration.isGroupEnabled("Ghostscript");
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(value = "/crop", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@StandardPdfResponse
|
||||
@@ -52,13 +46,9 @@ public class CropController {
|
||||
"This operation takes an input PDF file and crops it according to the given"
|
||||
+ " coordinates. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm request) throws IOException {
|
||||
if (request.isRemoveDataOutsideCrop() && isGhostscriptEnabled()) {
|
||||
if (request.isRemoveDataOutsideCrop()) {
|
||||
return cropWithGhostscript(request);
|
||||
} else {
|
||||
if (request.isRemoveDataOutsideCrop()) {
|
||||
log.warn(
|
||||
"Ghostscript not available - 'removeDataOutsideCrop' option requires Ghostscript. Falling back to visual crop only.");
|
||||
}
|
||||
return cropWithPDFBox(request);
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -26,6 +26,7 @@ import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.FormUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@@ -136,6 +137,26 @@ public class MultiPageLayoutController {
|
||||
|
||||
contentStream.close();
|
||||
|
||||
// If any source page is rotated, skip form copying/transformation entirely
|
||||
boolean hasRotation = FormUtils.hasAnyRotatedPage(sourceDocument);
|
||||
if (hasRotation) {
|
||||
log.info("Source document has rotated pages; skipping form field copying.");
|
||||
} else {
|
||||
try {
|
||||
FormUtils.copyAndTransformFormFields(
|
||||
sourceDocument,
|
||||
newDocument,
|
||||
totalPages,
|
||||
pagesPerSheet,
|
||||
cols,
|
||||
rows,
|
||||
cellWidth,
|
||||
cellHeight);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to copy and transform form fields: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
sourceDocument.close();
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
+1
-27
@@ -10,10 +10,7 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.config.InitialSetup;
|
||||
import stirling.software.common.annotations.api.ConfigApi;
|
||||
import stirling.software.common.configuration.AppConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
@@ -22,7 +19,6 @@ import stirling.software.common.service.UserServiceInterface;
|
||||
|
||||
@ConfigApi
|
||||
@Hidden
|
||||
@Slf4j
|
||||
public class ConfigController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
@@ -62,15 +58,9 @@ public class ConfigController {
|
||||
// Extract values from ApplicationProperties
|
||||
configData.put("appNameNavbar", applicationProperties.getUi().getAppNameNavbar());
|
||||
configData.put("languages", applicationProperties.getUi().getLanguages());
|
||||
configData.put("logoStyle", applicationProperties.getUi().getLogoStyle());
|
||||
|
||||
// Security settings
|
||||
// enableLogin requires both the config flag AND proprietary features to be loaded
|
||||
// If userService is null, proprietary module isn't loaded
|
||||
// (DISABLE_ADDITIONAL_FEATURES=true or DOCKER_ENABLE_SECURITY=false)
|
||||
boolean enableLogin =
|
||||
applicationProperties.getSecurity().getEnableLogin() && userService != null;
|
||||
configData.put("enableLogin", enableLogin);
|
||||
configData.put("enableLogin", applicationProperties.getSecurity().getEnableLogin());
|
||||
|
||||
// Mail settings - check both SMTP enabled AND invites enabled
|
||||
boolean smtpEnabled = applicationProperties.getMail().isEnabled();
|
||||
@@ -88,22 +78,6 @@ public class ConfigController {
|
||||
}
|
||||
configData.put("isAdmin", isAdmin);
|
||||
|
||||
// Check if this is a new server (version was 0.0.0 before initialization)
|
||||
configData.put("isNewServer", InitialSetup.isNewServer());
|
||||
|
||||
// Check if the current user is a first-time user
|
||||
boolean isNewUser =
|
||||
false; // Default to false when security is disabled or user not found
|
||||
if (userService != null) {
|
||||
try {
|
||||
isNewUser = userService.isCurrentUserFirstLogin();
|
||||
} catch (Exception e) {
|
||||
// If there's an error, assume not new user for safety
|
||||
isNewUser = false;
|
||||
}
|
||||
}
|
||||
configData.put("isNewUser", isNewUser);
|
||||
|
||||
// System settings
|
||||
configData.put(
|
||||
"enableAlphaFunctionality",
|
||||
|
||||
@@ -176,7 +176,6 @@ system:
|
||||
|
||||
ui:
|
||||
appNameNavbar: '' # name displayed on the navigation bar
|
||||
logoStyle: classic # Options: 'classic' (default - classic S icon) or 'modern' (minimalist logo)
|
||||
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
|
||||
|
||||
endpoints:
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
--md-sys-color-surface-3: color-mix(in srgb, var(--md-sys-color-primary) 13%, rgba(0, 0, 255, 0.11) 5%);
|
||||
--md-sys-color-surface-4: color-mix(in srgb, var(--md-sys-color-primary) 13%, rgba(0, 0, 255, 0.12) 5%);
|
||||
--md-sys-color-surface-5: color-mix(in srgb, var(--md-sys-color-primary) 13%, rgba(0, 0, 255, 0.14) 5%);
|
||||
/* Clear button disabled text color (default/light) */
|
||||
--spdf-clear-disabled-text: var(--md-sys-color-primary);
|
||||
/* Icon fill */
|
||||
--md-sys-icon-fill-0: 'FILL' 0, 'wght' 500;
|
||||
--md-sys-icon-fill-1: 'FILL' 1, 'wght' 500;
|
||||
@@ -27,12 +25,6 @@
|
||||
--md-sys-elevation-5: 0px 8px 10px -6px rgb(var(--md-elevation-shadow-color), 0.2), 0px 16px 24px 2px rgb(var(--md-elevation-shadow-color), 0.14), 0px 6px 30px 5px rgb(var(--md-elevation-shadow-color), 0.12);
|
||||
}
|
||||
|
||||
/* Dark theme overrides */
|
||||
.dark-theme {
|
||||
/* In dark mode, use a neutral grey for disabled Clear button text */
|
||||
--spdf-clear-disabled-text: var(--mantine-color-gray-5, #9e9e9e);
|
||||
}
|
||||
|
||||
.fill {
|
||||
font-variation-settings: var(--md-sys-icon-fill-1);
|
||||
}
|
||||
|
||||
-5
@@ -116,10 +116,6 @@ public class ProprietaryUIDataController {
|
||||
LoginData data = new LoginData();
|
||||
Map<String, String> providerList = new HashMap<>();
|
||||
Security securityProps = applicationProperties.getSecurity();
|
||||
|
||||
// Add enableLogin flag so frontend doesn't need to call /app-config
|
||||
data.setEnableLogin(securityProps.getEnableLogin());
|
||||
|
||||
OAUTH2 oauth = securityProps.getOauth2();
|
||||
|
||||
if (oauth != null && oauth.getEnabled()) {
|
||||
@@ -452,7 +448,6 @@ public class ProprietaryUIDataController {
|
||||
|
||||
@Data
|
||||
public static class LoginData {
|
||||
private Boolean enableLogin;
|
||||
private Map<String, String> providerList;
|
||||
private String loginMethod;
|
||||
private boolean altLogin;
|
||||
|
||||
-215
@@ -1,215 +0,0 @@
|
||||
package stirling.software.proprietary.controller.api.form;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.proprietary.util.FormUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/form")
|
||||
@Tag(name = "Forms", description = "PDF form APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class FormFillController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private static ResponseEntity<byte[]> saveDocument(PDDocument document, String baseName)
|
||||
throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
document.save(baos);
|
||||
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), baseName + ".pdf");
|
||||
}
|
||||
|
||||
private static String buildBaseName(MultipartFile file, String suffix) {
|
||||
String original = Filenames.toSimpleFileName(file.getOriginalFilename());
|
||||
if (original == null || original.isBlank()) {
|
||||
original = "document";
|
||||
}
|
||||
if (!original.toLowerCase().endsWith(".pdf")) {
|
||||
return original + "_" + suffix;
|
||||
}
|
||||
String withoutExtension = original.substring(0, original.length() - 4);
|
||||
return withoutExtension + "_" + suffix;
|
||||
}
|
||||
|
||||
private static void requirePdf(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.fileFormatRequired", "{0} must be in PDF format", "file");
|
||||
}
|
||||
}
|
||||
|
||||
private static String decodePart(byte[] payload) {
|
||||
if (payload == null || payload.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return new String(payload, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/fields", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Inspect PDF form fields",
|
||||
description = "Returns metadata describing each field in the provided PDF form")
|
||||
public ResponseEntity<FormUtils.FormFieldExtraction> listFields(
|
||||
@Parameter(
|
||||
description = "The input PDF file",
|
||||
required = true,
|
||||
content =
|
||||
@Content(
|
||||
mediaType = MediaType.APPLICATION_PDF_VALUE,
|
||||
schema = @Schema(type = "string", format = "binary")))
|
||||
@RequestParam("file")
|
||||
MultipartFile file)
|
||||
throws IOException {
|
||||
|
||||
requirePdf(file);
|
||||
try (PDDocument document = pdfDocumentFactory.load(file, true)) {
|
||||
FormUtils.FormFieldExtraction extraction =
|
||||
FormUtils.extractFieldsWithTemplate(document);
|
||||
return ResponseEntity.ok(extraction);
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/modify-fields", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Modify existing form fields",
|
||||
description =
|
||||
"Updates existing fields in the provided PDF and returns the updated file")
|
||||
public ResponseEntity<byte[]> modifyFields(
|
||||
@Parameter(
|
||||
description = "The input PDF file",
|
||||
required = true,
|
||||
content =
|
||||
@Content(
|
||||
mediaType = MediaType.APPLICATION_PDF_VALUE,
|
||||
schema = @Schema(type = "string", format = "binary")))
|
||||
@RequestParam("file")
|
||||
MultipartFile file,
|
||||
@RequestPart(value = "updates", required = false) byte[] updatesPayload)
|
||||
throws IOException {
|
||||
|
||||
String rawUpdates = decodePart(updatesPayload);
|
||||
List<FormUtils.ModifyFormFieldDefinition> modifications =
|
||||
FormPayloadParser.parseModificationDefinitions(objectMapper, rawUpdates);
|
||||
if (modifications.isEmpty()) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.dataRequired",
|
||||
"{0} must contain at least one definition",
|
||||
"updates payload");
|
||||
}
|
||||
|
||||
return processSingleFile(
|
||||
file, "updated", document -> FormUtils.modifyFormFields(document, modifications));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/delete-fields", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Delete form fields",
|
||||
description = "Removes the specified fields from the PDF and returns the updated file")
|
||||
public ResponseEntity<byte[]> deleteFields(
|
||||
@Parameter(
|
||||
description = "The input PDF file",
|
||||
required = true,
|
||||
content =
|
||||
@Content(
|
||||
mediaType = MediaType.APPLICATION_PDF_VALUE,
|
||||
schema = @Schema(type = "string", format = "binary")))
|
||||
@RequestParam("file")
|
||||
MultipartFile file,
|
||||
@Parameter(
|
||||
description =
|
||||
"JSON array of field names or objects with a name property,"
|
||||
+ " matching the /fields response format",
|
||||
example = "[{\"name\":\"Field1\"}]")
|
||||
@RequestPart(value = "names", required = false)
|
||||
byte[] namesPayload)
|
||||
throws IOException {
|
||||
|
||||
String rawNames = decodePart(namesPayload);
|
||||
List<String> names = FormPayloadParser.parseNameList(objectMapper, rawNames);
|
||||
if (names.isEmpty()) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.dataRequired", "{0} must contain at least one value", "names payload");
|
||||
}
|
||||
|
||||
return processSingleFile(
|
||||
file, "updated", document -> FormUtils.deleteFormFields(document, names));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/fill", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Fill PDF form fields",
|
||||
description =
|
||||
"Populates the supplied PDF form using values from the provided JSON payload"
|
||||
+ " and returns the filled PDF")
|
||||
public ResponseEntity<byte[]> fillForm(
|
||||
@Parameter(
|
||||
description = "The input PDF file",
|
||||
required = true,
|
||||
content =
|
||||
@Content(
|
||||
mediaType = MediaType.APPLICATION_PDF_VALUE,
|
||||
schema = @Schema(type = "string", format = "binary")))
|
||||
@RequestParam("file")
|
||||
MultipartFile file,
|
||||
@Parameter(
|
||||
description = "JSON object of field-value pairs to apply",
|
||||
example = "{\"field\":\"value\"}")
|
||||
@RequestPart(value = "data", required = false)
|
||||
byte[] valuesPayload,
|
||||
@RequestParam(value = "flatten", defaultValue = "false") boolean flatten)
|
||||
throws IOException {
|
||||
|
||||
String rawValues = decodePart(valuesPayload);
|
||||
Map<String, Object> values = FormPayloadParser.parseValueMap(objectMapper, rawValues);
|
||||
|
||||
return processSingleFile(
|
||||
file,
|
||||
"filled",
|
||||
document -> FormUtils.applyFieldValues(document, values, flatten, true));
|
||||
}
|
||||
|
||||
private ResponseEntity<byte[]> processSingleFile(
|
||||
MultipartFile file, String suffix, DocumentProcessor processor) throws IOException {
|
||||
requirePdf(file);
|
||||
|
||||
String baseName = buildBaseName(file, suffix);
|
||||
try (PDDocument document = pdfDocumentFactory.load(file)) {
|
||||
processor.accept(document);
|
||||
return saveDocument(document, baseName);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface DocumentProcessor {
|
||||
void accept(PDDocument document) throws IOException;
|
||||
}
|
||||
}
|
||||
-295
@@ -1,295 +0,0 @@
|
||||
package stirling.software.proprietary.controller.api.form;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.proprietary.util.FormUtils;
|
||||
|
||||
final class FormPayloadParser {
|
||||
|
||||
private static final String KEY_FIELDS = "fields";
|
||||
private static final String KEY_NAME = "name";
|
||||
private static final String KEY_TARGET_NAME = "targetName";
|
||||
private static final String KEY_FIELD_NAME = "fieldName";
|
||||
private static final String KEY_FIELD = "field";
|
||||
private static final String KEY_VALUE = "value";
|
||||
private static final String KEY_DEFAULT_VALUE = "defaultValue";
|
||||
|
||||
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {};
|
||||
private static final TypeReference<List<FormUtils.ModifyFormFieldDefinition>>
|
||||
MODIFY_FIELD_LIST_TYPE = new TypeReference<>() {};
|
||||
private static final TypeReference<List<String>> STRING_LIST_TYPE = new TypeReference<>() {};
|
||||
|
||||
private FormPayloadParser() {}
|
||||
|
||||
static Map<String, Object> parseValueMap(ObjectMapper objectMapper, String json)
|
||||
throws IOException {
|
||||
if (json == null || json.isBlank()) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
JsonNode root;
|
||||
try {
|
||||
root = objectMapper.readTree(json);
|
||||
} catch (IOException e) {
|
||||
// Fallback to legacy direct map parse (will throw again if invalid)
|
||||
return objectMapper.readValue(json, MAP_TYPE);
|
||||
}
|
||||
if (root == null || root.isNull()) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
// 1. If payload already a flat object with no special wrapping, keep legacy behavior
|
||||
if (root.isObject()) {
|
||||
// a) Prefer explicit 'template' object if present (new combined /fields response)
|
||||
JsonNode templateNode = root.get("template");
|
||||
if (templateNode != null && templateNode.isObject()) {
|
||||
return objectToLinkedMap(templateNode);
|
||||
}
|
||||
// b) Accept an inline 'fields' array of field definitions (build map from them)
|
||||
JsonNode fieldsNode = root.get(KEY_FIELDS);
|
||||
if (fieldsNode != null && fieldsNode.isArray()) {
|
||||
Map<String, Object> record = extractFieldInfoArray(fieldsNode);
|
||||
if (!record.isEmpty()) {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
// c) Fallback: treat entire object as the value map (legacy behavior)
|
||||
return objectToLinkedMap(root);
|
||||
}
|
||||
|
||||
// 2. If an array was supplied to /fill (non-standard), treat first element as record
|
||||
if (root.isArray()) {
|
||||
if (root.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
JsonNode first = root.get(0);
|
||||
if (first != null && first.isObject()) {
|
||||
if (first.has(KEY_NAME) || first.has(KEY_VALUE) || first.has(KEY_DEFAULT_VALUE)) {
|
||||
return extractFieldInfoArray(root);
|
||||
}
|
||||
return objectToLinkedMap(first);
|
||||
}
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
// 3. Anything else: fallback to strict map parse
|
||||
return objectMapper.readValue(json, MAP_TYPE);
|
||||
}
|
||||
|
||||
static List<FormUtils.ModifyFormFieldDefinition> parseModificationDefinitions(
|
||||
ObjectMapper objectMapper, String json) throws IOException {
|
||||
if (json == null || json.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
return objectMapper.readValue(json, MODIFY_FIELD_LIST_TYPE);
|
||||
}
|
||||
|
||||
static List<String> parseNameList(ObjectMapper objectMapper, String json) throws IOException {
|
||||
if (json == null || json.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
final JsonNode root = objectMapper.readTree(json);
|
||||
if (root == null || root.isNull()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
final Set<String> names = new LinkedHashSet<>();
|
||||
|
||||
if (root.isArray()) {
|
||||
collectNames(root, names);
|
||||
} else if (root.isObject()) {
|
||||
if (root.has(KEY_FIELDS) && root.get(KEY_FIELDS).isArray()) {
|
||||
collectNames(root.get(KEY_FIELDS), names);
|
||||
} else {
|
||||
final String single = extractName(root);
|
||||
if (nonBlank(single)) {
|
||||
names.add(single);
|
||||
}
|
||||
}
|
||||
} else if (root.isTextual()) {
|
||||
final String single = trimToNull(root.asText());
|
||||
if (single != null) {
|
||||
names.add(single);
|
||||
}
|
||||
}
|
||||
|
||||
if (!names.isEmpty()) {
|
||||
return List.copyOf(names);
|
||||
}
|
||||
|
||||
try {
|
||||
return objectMapper.readValue(json, STRING_LIST_TYPE);
|
||||
} catch (IOException e) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidFormat",
|
||||
"Invalid {0} format: {1}",
|
||||
"names payload",
|
||||
"expected array of strings or objects with 'name'-like properties");
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> extractFieldInfoArray(JsonNode fieldsNode) {
|
||||
final Map<String, Object> record = new LinkedHashMap<>();
|
||||
if (fieldsNode == null || fieldsNode.isNull() || !fieldsNode.isArray()) {
|
||||
return record;
|
||||
}
|
||||
|
||||
for (JsonNode fieldNode : fieldsNode) {
|
||||
if (fieldNode == null || !fieldNode.isObject()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String name = extractName(fieldNode);
|
||||
if (!nonBlank(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonNode valueNode = fieldNode.get(KEY_VALUE);
|
||||
if ((valueNode == null || valueNode.isNull())
|
||||
&& fieldNode.hasNonNull(KEY_DEFAULT_VALUE)) {
|
||||
valueNode = fieldNode.get(KEY_DEFAULT_VALUE);
|
||||
}
|
||||
|
||||
final String normalized = normalizeFieldValue(valueNode);
|
||||
record.put(name, normalized == null ? "" : normalized);
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
private static String normalizeFieldValue(JsonNode valueNode) {
|
||||
if (valueNode == null || valueNode.isNull()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (valueNode.isArray()) {
|
||||
final List<String> values = new ArrayList<>();
|
||||
for (JsonNode element : valueNode) {
|
||||
final String text = coerceScalarToString(element);
|
||||
if (text != null) {
|
||||
values.add(text);
|
||||
}
|
||||
}
|
||||
return String.join(",", values);
|
||||
}
|
||||
|
||||
if (valueNode.isObject()) {
|
||||
// Preserve object as JSON string
|
||||
return valueNode.toString();
|
||||
}
|
||||
|
||||
// Scalar (text/number/boolean)
|
||||
return coerceScalarToString(valueNode);
|
||||
}
|
||||
|
||||
private static String coerceScalarToString(JsonNode node) {
|
||||
if (node == null || node.isNull()) {
|
||||
return null;
|
||||
}
|
||||
if (node.isTextual()) {
|
||||
return trimToEmpty(node.asText());
|
||||
}
|
||||
if (node.isNumber()) {
|
||||
return node.numberValue().toString();
|
||||
}
|
||||
if (node.isBoolean()) {
|
||||
return Boolean.toString(node.booleanValue());
|
||||
}
|
||||
// Fallback for other scalar-like nodes
|
||||
return trimToEmpty(node.asText());
|
||||
}
|
||||
|
||||
private static void collectNames(JsonNode arrayNode, Set<String> sink) {
|
||||
if (arrayNode == null || !arrayNode.isArray()) {
|
||||
return;
|
||||
}
|
||||
for (JsonNode node : arrayNode) {
|
||||
final String name = extractName(node);
|
||||
if (nonBlank(name)) {
|
||||
sink.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String extractName(JsonNode node) {
|
||||
if (node == null || node.isNull()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node.isTextual()) {
|
||||
return trimToNull(node.asText());
|
||||
}
|
||||
|
||||
if (node.isObject()) {
|
||||
final String direct = textProperty(node, KEY_NAME, KEY_TARGET_NAME, KEY_FIELD_NAME);
|
||||
if (nonBlank(direct)) {
|
||||
return direct;
|
||||
}
|
||||
final JsonNode field = node.get(KEY_FIELD);
|
||||
if (field != null && field.isObject()) {
|
||||
final String nested =
|
||||
textProperty(field, KEY_NAME, KEY_TARGET_NAME, KEY_FIELD_NAME);
|
||||
if (nonBlank(nested)) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String textProperty(JsonNode node, String... keys) {
|
||||
for (String key : keys) {
|
||||
final JsonNode valueNode = node.get(key);
|
||||
final String value = coerceScalarToString(valueNode);
|
||||
if (nonBlank(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Map<String, Object> objectToLinkedMap(JsonNode objectNode) {
|
||||
final Map<String, Object> result = new LinkedHashMap<>();
|
||||
objectNode
|
||||
.fieldNames()
|
||||
.forEachRemaining(
|
||||
key -> {
|
||||
final JsonNode v = objectNode.get(key);
|
||||
if (v == null || v.isNull()) {
|
||||
result.put(key, null);
|
||||
} else if (v.isTextual() || v.isNumber() || v.isBoolean()) {
|
||||
result.put(key, coerceScalarToString(v));
|
||||
} else {
|
||||
result.put(key, v.toString());
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean nonBlank(String s) {
|
||||
return s != null && !s.isBlank();
|
||||
}
|
||||
|
||||
private static String trimToNull(String s) {
|
||||
if (s == null) return null;
|
||||
final String t = s.trim();
|
||||
return t.isEmpty() ? null : t;
|
||||
}
|
||||
|
||||
private static String trimToEmpty(String s) {
|
||||
return s == null ? "" : s.trim();
|
||||
}
|
||||
}
|
||||
+49
-41
@@ -129,53 +129,61 @@ public class SecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
List<String> configuredOrigins = null;
|
||||
if (applicationProperties.getSystem() != null) {
|
||||
configuredOrigins = applicationProperties.getSystem().getCorsAllowedOrigins();
|
||||
}
|
||||
// Read CORS allowed origins from settings
|
||||
if (applicationProperties.getSystem() != null
|
||||
&& applicationProperties.getSystem().getCorsAllowedOrigins() != null
|
||||
&& !applicationProperties.getSystem().getCorsAllowedOrigins().isEmpty()) {
|
||||
|
||||
CorsConfiguration cfg = new CorsConfiguration();
|
||||
if (configuredOrigins != null && !configuredOrigins.isEmpty()) {
|
||||
cfg.setAllowedOriginPatterns(configuredOrigins);
|
||||
List<String> allowedOrigins = applicationProperties.getSystem().getCorsAllowedOrigins();
|
||||
|
||||
CorsConfiguration cfg = new CorsConfiguration();
|
||||
|
||||
// Use setAllowedOriginPatterns for better wildcard and port support
|
||||
cfg.setAllowedOriginPatterns(allowedOrigins);
|
||||
log.debug(
|
||||
"CORS configured with allowed origin patterns from settings.yml: {}",
|
||||
configuredOrigins);
|
||||
allowedOrigins);
|
||||
|
||||
// Set allowed methods explicitly (including OPTIONS for preflight)
|
||||
cfg.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
|
||||
// Set allowed headers explicitly
|
||||
cfg.setAllowedHeaders(
|
||||
List.of(
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"X-Requested-With",
|
||||
"Accept",
|
||||
"Origin",
|
||||
"X-API-KEY",
|
||||
"X-CSRF-TOKEN"));
|
||||
|
||||
// Set exposed headers (headers that the browser can access)
|
||||
cfg.setExposedHeaders(
|
||||
List.of(
|
||||
"WWW-Authenticate",
|
||||
"X-Total-Count",
|
||||
"X-Page-Number",
|
||||
"X-Page-Size",
|
||||
"Content-Disposition",
|
||||
"Content-Type"));
|
||||
|
||||
// Allow credentials (cookies, authorization headers)
|
||||
cfg.setAllowCredentials(true);
|
||||
|
||||
// Set max age for preflight cache
|
||||
cfg.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", cfg);
|
||||
return source;
|
||||
} else {
|
||||
// Default to allowing all origins when nothing is configured
|
||||
cfg.setAllowedOriginPatterns(List.of("*"));
|
||||
// No CORS origins configured - return null to disable CORS processing entirely
|
||||
// This avoids empty CORS policy that unexpectedly rejects preflights
|
||||
log.info(
|
||||
"No CORS allowed origins configured in settings.yml (system.corsAllowedOrigins); allowing all origins.");
|
||||
"CORS is disabled - no allowed origins configured in settings.yml (system.corsAllowedOrigins)");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Explicitly configure supported HTTP methods (include OPTIONS for preflight)
|
||||
cfg.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
|
||||
cfg.setAllowedHeaders(
|
||||
List.of(
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"X-Requested-With",
|
||||
"Accept",
|
||||
"Origin",
|
||||
"X-API-KEY",
|
||||
"X-CSRF-TOKEN",
|
||||
"X-XSRF-TOKEN"));
|
||||
|
||||
cfg.setExposedHeaders(
|
||||
List.of(
|
||||
"WWW-Authenticate",
|
||||
"X-Total-Count",
|
||||
"X-Page-Number",
|
||||
"X-Page-Size",
|
||||
"Content-Disposition",
|
||||
"Content-Type"));
|
||||
|
||||
cfg.setAllowCredentials(true);
|
||||
cfg.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", cfg);
|
||||
return source;
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
+4
-2
@@ -15,7 +15,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.api.InviteApi;
|
||||
import stirling.software.common.annotations.api.UserApi;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
@@ -26,9 +26,11 @@ import stirling.software.proprietary.security.service.EmailService;
|
||||
import stirling.software.proprietary.security.service.TeamService;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
|
||||
@InviteApi
|
||||
@UserApi
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/invite")
|
||||
public class InviteLinkController {
|
||||
|
||||
private final InviteTokenRepository inviteTokenRepository;
|
||||
|
||||
-27
@@ -742,31 +742,4 @@ public class UserController {
|
||||
return errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/complete-initial-setup")
|
||||
public ResponseEntity<?> completeInitialSetup() {
|
||||
try {
|
||||
String username = userService.getCurrentUsername();
|
||||
if (username == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body("User not authenticated");
|
||||
}
|
||||
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||
if (userOpt.isEmpty()) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found");
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
user.setHasCompletedInitialSetup(true);
|
||||
userRepository.save(user);
|
||||
|
||||
log.info("User {} completed initial setup", username);
|
||||
return ResponseEntity.ok().body(Map.of("success", true));
|
||||
} catch (Exception e) {
|
||||
log.error("Error completing initial setup", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body("Failed to complete initial setup");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-4
@@ -107,8 +107,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
}
|
||||
}
|
||||
|
||||
// If we still don't have any authentication, check if it's a public endpoint. If not, deny
|
||||
// the request
|
||||
// If we still don't have any authentication, check if it's a public endpoint. If not, deny the request
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
String method = request.getMethod();
|
||||
String contextPath = request.getContextPath();
|
||||
@@ -223,8 +222,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
|| trimmedUri.startsWith("/saml2")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/login")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/refresh")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/logout")
|
||||
|| trimmedUri.startsWith("/api/v1/proprietary/ui-data/login");
|
||||
|| trimmedUri.startsWith("/api/v1/auth/logout");
|
||||
}
|
||||
|
||||
private enum UserLoginType {
|
||||
|
||||
@@ -56,9 +56,6 @@ public class User implements UserDetails, Serializable {
|
||||
@Column(name = "isFirstLogin")
|
||||
private Boolean isFirstLogin = false;
|
||||
|
||||
@Column(name = "hasCompletedInitialSetup")
|
||||
private Boolean hasCompletedInitialSetup = false;
|
||||
|
||||
@Column(name = "roleName")
|
||||
private String roleName;
|
||||
|
||||
@@ -106,14 +103,6 @@ public class User implements UserDetails, Serializable {
|
||||
this.isFirstLogin = isFirstLogin;
|
||||
}
|
||||
|
||||
public boolean hasCompletedInitialSetup() {
|
||||
return hasCompletedInitialSetup != null && hasCompletedInitialSetup;
|
||||
}
|
||||
|
||||
public void setHasCompletedInitialSetup(boolean hasCompletedInitialSetup) {
|
||||
this.hasCompletedInitialSetup = hasCompletedInitialSetup;
|
||||
}
|
||||
|
||||
public void setAuthenticationType(AuthenticationType authenticationType) {
|
||||
this.authenticationType = authenticationType.toString().toLowerCase();
|
||||
}
|
||||
|
||||
-15
@@ -663,21 +663,6 @@ public class UserService implements UserServiceInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isCurrentUserFirstLogin() {
|
||||
try {
|
||||
String username = getCurrentUsername();
|
||||
if (username != null) {
|
||||
Optional<User> userOpt = findByUsernameIgnoreCase(username);
|
||||
if (userOpt.isPresent()) {
|
||||
return !userOpt.get().hasCompletedInitialSetup();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Error checking first login status", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void syncCustomApiUser(String customApiKey) {
|
||||
if (customApiKey == null || customApiKey.trim().isBlank()) {
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
package stirling.software.proprietary.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDResources;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class FormCopyUtils {
|
||||
|
||||
public boolean hasAnyRotatedPage(PDDocument document) {
|
||||
try {
|
||||
for (PDPage page : document.getPages()) {
|
||||
int rot = page.getRotation();
|
||||
int norm = ((rot % 360) + 360) % 360;
|
||||
if (norm != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to inspect page rotations: {}", e.getMessage(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void copyAndTransformFormFields(
|
||||
PDDocument sourceDocument,
|
||||
PDDocument newDocument,
|
||||
int totalPages,
|
||||
int pagesPerSheet,
|
||||
int cols,
|
||||
int rows,
|
||||
float cellWidth,
|
||||
float cellHeight)
|
||||
throws IOException {
|
||||
|
||||
PDDocumentCatalog sourceCatalog = sourceDocument.getDocumentCatalog();
|
||||
PDAcroForm sourceAcroForm = sourceCatalog.getAcroForm();
|
||||
|
||||
if (sourceAcroForm == null || sourceAcroForm.getFields().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PDDocumentCatalog newCatalog = newDocument.getDocumentCatalog();
|
||||
PDAcroForm newAcroForm = new PDAcroForm(newDocument);
|
||||
newCatalog.setAcroForm(newAcroForm);
|
||||
|
||||
PDResources dr = new PDResources();
|
||||
PDType1Font helvetica = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||
PDType1Font zapfDingbats = new PDType1Font(Standard14Fonts.FontName.ZAPF_DINGBATS);
|
||||
dr.put(COSName.getPDFName("Helv"), helvetica);
|
||||
dr.put(COSName.getPDFName("ZaDb"), zapfDingbats);
|
||||
newAcroForm.setDefaultResources(dr);
|
||||
newAcroForm.setDefaultAppearance("/Helv 12 Tf 0 g");
|
||||
|
||||
// Temporarily set NeedAppearances to true during field creation
|
||||
newAcroForm.setNeedAppearances(true);
|
||||
|
||||
Map<String, Integer> fieldNameCounters = new HashMap<>();
|
||||
|
||||
// Build widget -> field map once for efficient lookups
|
||||
Map<PDAnnotationWidget, PDField> widgetFieldMap = buildWidgetFieldMap(sourceAcroForm);
|
||||
|
||||
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||
PDPage sourcePage = sourceDocument.getPage(pageIndex);
|
||||
List<PDAnnotation> annotations = sourcePage.getAnnotations();
|
||||
|
||||
if (annotations.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int destinationPageIndex = pageIndex / pagesPerSheet;
|
||||
int adjustedPageIndex = pageIndex % pagesPerSheet;
|
||||
int rowIndex = adjustedPageIndex / cols;
|
||||
int colIndex = adjustedPageIndex % cols;
|
||||
|
||||
if (rowIndex >= rows) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (destinationPageIndex >= newDocument.getNumberOfPages()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PDPage destinationPage = newDocument.getPage(destinationPageIndex);
|
||||
PDRectangle sourceRect = sourcePage.getMediaBox();
|
||||
|
||||
float scaleWidth = cellWidth / sourceRect.getWidth();
|
||||
float scaleHeight = cellHeight / sourceRect.getHeight();
|
||||
float scale = Math.min(scaleWidth, scaleHeight);
|
||||
|
||||
float x = colIndex * cellWidth + (cellWidth - sourceRect.getWidth() * scale) / 2;
|
||||
float y =
|
||||
destinationPage.getMediaBox().getHeight()
|
||||
- ((rowIndex + 1) * cellHeight
|
||||
- (cellHeight - sourceRect.getHeight() * scale) / 2);
|
||||
|
||||
copyBasicFormFields(
|
||||
sourceAcroForm,
|
||||
newAcroForm,
|
||||
sourcePage,
|
||||
destinationPage,
|
||||
x,
|
||||
y,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters,
|
||||
widgetFieldMap);
|
||||
}
|
||||
|
||||
// Generate appearance streams and embed them authoritatively
|
||||
boolean appearancesGenerated = false;
|
||||
try {
|
||||
newAcroForm.refreshAppearances();
|
||||
appearancesGenerated = true;
|
||||
} catch (NoSuchMethodError nsme) {
|
||||
log.warn(
|
||||
"AcroForm.refreshAppearances() not available in this PDFBox version; "
|
||||
+ "leaving NeedAppearances=true for viewer-side rendering.");
|
||||
} catch (Exception t) {
|
||||
log.warn(
|
||||
"Failed to refresh field appearances via AcroForm: {}. "
|
||||
+ "Leaving NeedAppearances=true as fallback.",
|
||||
t.getMessage(),
|
||||
t);
|
||||
}
|
||||
|
||||
// After successful appearance generation, set NeedAppearances to false
|
||||
// to signal that appearance streams are now embedded authoritatively
|
||||
if (appearancesGenerated) {
|
||||
try {
|
||||
newAcroForm.setNeedAppearances(false);
|
||||
} catch (Exception e) {
|
||||
log.debug(
|
||||
"Failed to set NeedAppearances to false: {}. "
|
||||
+ "Appearances were generated but flag could not be updated.",
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void copyBasicFormFields(
|
||||
PDAcroForm sourceAcroForm,
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage sourcePage,
|
||||
PDPage destinationPage,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters,
|
||||
Map<PDAnnotationWidget, PDField> widgetFieldMap) {
|
||||
|
||||
try {
|
||||
List<PDAnnotation> sourceAnnotations = sourcePage.getAnnotations();
|
||||
List<PDAnnotation> destinationAnnotations = destinationPage.getAnnotations();
|
||||
|
||||
for (PDAnnotation annotation : sourceAnnotations) {
|
||||
if (annotation instanceof PDAnnotationWidget widgetAnnotation) {
|
||||
if (widgetAnnotation.getRectangle() == null) {
|
||||
continue;
|
||||
}
|
||||
PDField sourceField =
|
||||
widgetFieldMap != null ? widgetFieldMap.get(widgetAnnotation) : null;
|
||||
if (sourceField == null) {
|
||||
continue; // skip widgets without a matching field
|
||||
}
|
||||
if (!(sourceField instanceof PDTerminalField terminalField)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
FormFieldTypeSupport handler = FormFieldTypeSupport.forField(terminalField);
|
||||
if (handler == null) {
|
||||
log.debug(
|
||||
"Skipping unsupported field type '{}' for widget '{}'",
|
||||
sourceField.getClass().getSimpleName(),
|
||||
Optional.ofNullable(sourceField.getFullyQualifiedName())
|
||||
.orElseGet(sourceField::getPartialName));
|
||||
continue;
|
||||
}
|
||||
|
||||
copyFieldUsingHandler(
|
||||
handler,
|
||||
terminalField,
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
widgetAnnotation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to copy basic form fields for page {}: {}",
|
||||
pageIndex,
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyFieldUsingHandler(
|
||||
FormFieldTypeSupport handler,
|
||||
PDTerminalField sourceField,
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
try {
|
||||
PDTerminalField newField = handler.createField(newAcroForm);
|
||||
boolean initialized =
|
||||
initializeFieldWithWidget(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
newField,
|
||||
sourceField.getPartialName(),
|
||||
handler.fallbackWidgetName(),
|
||||
sourceWidget,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
handler.copyFromOriginal(sourceField, newField);
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to copy {} field '{}': {}",
|
||||
handler.typeName(),
|
||||
Optional.ofNullable(sourceField.getFullyQualifiedName())
|
||||
.orElseGet(sourceField::getPartialName),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends PDTerminalField> boolean initializeFieldWithWidget(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
T newField,
|
||||
String originalName,
|
||||
String fallbackName,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
String baseName = (originalName != null) ? originalName : fallbackName;
|
||||
String newFieldName = generateUniqueFieldName(baseName, pageIndex, fieldNameCounters);
|
||||
newField.setPartialName(newFieldName);
|
||||
|
||||
PDAnnotationWidget newWidget = new PDAnnotationWidget();
|
||||
PDRectangle sourceRect = sourceWidget.getRectangle();
|
||||
if (sourceRect == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float newX = (sourceRect.getLowerLeftX() * scale) + offsetX;
|
||||
float newY = (sourceRect.getLowerLeftY() * scale) + offsetY;
|
||||
float newWidth = sourceRect.getWidth() * scale;
|
||||
float newHeight = sourceRect.getHeight() * scale;
|
||||
newWidget.setRectangle(new PDRectangle(newX, newY, newWidth, newHeight));
|
||||
newWidget.setPage(destinationPage);
|
||||
|
||||
newField.getWidgets().add(newWidget);
|
||||
newWidget.setParent(newField);
|
||||
newAcroForm.getFields().add(newField);
|
||||
destinationAnnotations.add(newWidget);
|
||||
return true;
|
||||
}
|
||||
|
||||
private String generateUniqueFieldName(
|
||||
String originalName, int pageIndex, Map<String, Integer> fieldNameCounters) {
|
||||
String baseName = "page" + pageIndex + "_" + originalName;
|
||||
|
||||
Integer counter = fieldNameCounters.get(baseName);
|
||||
if (counter == null) {
|
||||
counter = 0;
|
||||
} else {
|
||||
counter++;
|
||||
}
|
||||
fieldNameCounters.put(baseName, counter);
|
||||
|
||||
return counter == 0 ? baseName : baseName + "_" + counter;
|
||||
}
|
||||
|
||||
private Map<PDAnnotationWidget, PDField> buildWidgetFieldMap(PDAcroForm acroForm) {
|
||||
Map<PDAnnotationWidget, PDField> map = new HashMap<>();
|
||||
if (acroForm == null) {
|
||||
return map;
|
||||
}
|
||||
try {
|
||||
for (PDField field : acroForm.getFieldTree()) {
|
||||
List<PDAnnotationWidget> widgets = field.getWidgets();
|
||||
if (widgets == null) {
|
||||
continue;
|
||||
}
|
||||
for (PDAnnotationWidget widget : widgets) {
|
||||
if (widget != null) {
|
||||
map.put(widget, field);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to build widget->field map: {}", e.getMessage(), e);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
-368
@@ -1,368 +0,0 @@
|
||||
package stirling.software.proprietary.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
|
||||
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceCharacteristicsDictionary;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDCheckBox;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDChoice;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDComboBox;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDListBox;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDPushButton;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDRadioButton;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDTextField;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public enum FormFieldTypeSupport {
|
||||
TEXT("text", "textField", PDTextField.class) {
|
||||
@Override
|
||||
PDTerminalField createField(PDAcroForm acroForm) {
|
||||
PDTextField textField = new PDTextField(acroForm);
|
||||
textField.setDefaultAppearance("/Helv 12 Tf 0 g");
|
||||
return textField;
|
||||
}
|
||||
|
||||
@Override
|
||||
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||
PDTextField src = (PDTextField) source;
|
||||
PDTextField dst = (PDTextField) target;
|
||||
String value = src.getValueAsString();
|
||||
if (value != null) {
|
||||
dst.setValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean doesNotsupportsDefinitionCreation() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
void applyNewFieldDefinition(
|
||||
PDTerminalField field,
|
||||
FormUtils.NewFormFieldDefinition definition,
|
||||
List<String> options)
|
||||
throws IOException {
|
||||
PDTextField textField = (PDTextField) field;
|
||||
String defaultValue = Optional.ofNullable(definition.defaultValue()).orElse("");
|
||||
if (!defaultValue.isBlank()) {
|
||||
FormUtils.setTextValue(textField, defaultValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
CHECKBOX("checkbox", "checkBox", PDCheckBox.class) {
|
||||
@Override
|
||||
PDTerminalField createField(PDAcroForm acroForm) {
|
||||
return new PDCheckBox(acroForm);
|
||||
}
|
||||
|
||||
@Override
|
||||
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||
PDCheckBox src = (PDCheckBox) source;
|
||||
PDCheckBox dst = (PDCheckBox) target;
|
||||
if (src.isChecked()) {
|
||||
dst.check();
|
||||
} else {
|
||||
dst.unCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean doesNotsupportsDefinitionCreation() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
void applyNewFieldDefinition(
|
||||
PDTerminalField field,
|
||||
FormUtils.NewFormFieldDefinition definition,
|
||||
List<String> options)
|
||||
throws IOException {
|
||||
PDCheckBox checkBox = (PDCheckBox) field;
|
||||
|
||||
if (!options.isEmpty()) {
|
||||
checkBox.setExportValues(options);
|
||||
}
|
||||
|
||||
ensureCheckBoxAppearance(checkBox);
|
||||
|
||||
if (FormUtils.isChecked(definition.defaultValue())) {
|
||||
checkBox.check();
|
||||
} else {
|
||||
checkBox.unCheck();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureCheckBoxAppearance(PDCheckBox checkBox) {
|
||||
try {
|
||||
if (checkBox.getWidgets().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PDAnnotationWidget widget = checkBox.getWidgets().get(0);
|
||||
|
||||
PDAppearanceCharacteristicsDictionary appearanceChars =
|
||||
widget.getAppearanceCharacteristics();
|
||||
if (appearanceChars == null) {
|
||||
appearanceChars =
|
||||
new PDAppearanceCharacteristicsDictionary(widget.getCOSObject());
|
||||
widget.setAppearanceCharacteristics(appearanceChars);
|
||||
}
|
||||
|
||||
appearanceChars.setBorderColour(
|
||||
new PDColor(new float[] {0, 0, 0}, PDDeviceRGB.INSTANCE));
|
||||
appearanceChars.setBackground(
|
||||
new PDColor(new float[] {1, 1, 1}, PDDeviceRGB.INSTANCE));
|
||||
|
||||
appearanceChars.setNormalCaption("4");
|
||||
|
||||
widget.setPrinted(true);
|
||||
widget.setReadOnly(false);
|
||||
widget.setHidden(false);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("Unable to set checkbox appearance characteristics: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
},
|
||||
RADIO("radio", "radioButton", PDRadioButton.class) {
|
||||
@Override
|
||||
PDTerminalField createField(PDAcroForm acroForm) {
|
||||
return new PDRadioButton(acroForm);
|
||||
}
|
||||
|
||||
@Override
|
||||
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||
PDRadioButton src = (PDRadioButton) source;
|
||||
PDRadioButton dst = (PDRadioButton) target;
|
||||
if (src.getExportValues() != null) {
|
||||
dst.setExportValues(src.getExportValues());
|
||||
}
|
||||
if (src.getValue() != null) {
|
||||
dst.setValue(src.getValue());
|
||||
}
|
||||
}
|
||||
},
|
||||
COMBOBOX("combobox", "comboBox", PDComboBox.class) {
|
||||
@Override
|
||||
PDTerminalField createField(PDAcroForm acroForm) {
|
||||
return new PDComboBox(acroForm);
|
||||
}
|
||||
|
||||
@Override
|
||||
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||
PDComboBox src = (PDComboBox) source;
|
||||
PDComboBox dst = (PDComboBox) target;
|
||||
copyChoiceCharacteristics(src, dst);
|
||||
if (src.getOptions() != null) {
|
||||
dst.setOptions(src.getOptions());
|
||||
}
|
||||
if (src.getValue() != null && !src.getValue().isEmpty()) {
|
||||
dst.setValue(src.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean doesNotsupportsDefinitionCreation() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
void applyNewFieldDefinition(
|
||||
PDTerminalField field,
|
||||
FormUtils.NewFormFieldDefinition definition,
|
||||
List<String> options)
|
||||
throws IOException {
|
||||
PDComboBox comboBox = (PDComboBox) field;
|
||||
if (!options.isEmpty()) {
|
||||
comboBox.setOptions(options);
|
||||
}
|
||||
List<String> allowedOptions = FormUtils.resolveOptions(comboBox);
|
||||
String comboName =
|
||||
Optional.ofNullable(comboBox.getFullyQualifiedName())
|
||||
.orElseGet(comboBox::getPartialName);
|
||||
String defaultValue = definition.defaultValue();
|
||||
if (defaultValue != null && !defaultValue.isBlank()) {
|
||||
String filtered =
|
||||
FormUtils.filterSingleChoiceSelection(
|
||||
defaultValue, allowedOptions, comboName);
|
||||
if (filtered != null) {
|
||||
comboBox.setValue(filtered);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
LISTBOX("listbox", "listBox", PDListBox.class) {
|
||||
@Override
|
||||
PDTerminalField createField(PDAcroForm acroForm) {
|
||||
return new PDListBox(acroForm);
|
||||
}
|
||||
|
||||
@Override
|
||||
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||
PDListBox src = (PDListBox) source;
|
||||
PDListBox dst = (PDListBox) target;
|
||||
copyChoiceCharacteristics(src, dst);
|
||||
if (src.getOptions() != null) {
|
||||
dst.setOptions(src.getOptions());
|
||||
}
|
||||
if (src.getValue() != null && !src.getValue().isEmpty()) {
|
||||
dst.setValue(src.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean doesNotsupportsDefinitionCreation() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
void applyNewFieldDefinition(
|
||||
PDTerminalField field,
|
||||
FormUtils.NewFormFieldDefinition definition,
|
||||
List<String> options)
|
||||
throws IOException {
|
||||
PDListBox listBox = (PDListBox) field;
|
||||
listBox.setMultiSelect(Boolean.TRUE.equals(definition.multiSelect()));
|
||||
if (!options.isEmpty()) {
|
||||
listBox.setOptions(options);
|
||||
}
|
||||
List<String> allowedOptions = FormUtils.collectChoiceAllowedValues(listBox);
|
||||
String listBoxName =
|
||||
Optional.ofNullable(listBox.getFullyQualifiedName())
|
||||
.orElseGet(listBox::getPartialName);
|
||||
String defaultValue = definition.defaultValue();
|
||||
if (defaultValue != null && !defaultValue.isBlank()) {
|
||||
if (Boolean.TRUE.equals(definition.multiSelect())) {
|
||||
List<String> selections = FormUtils.parseMultiChoiceSelections(defaultValue);
|
||||
List<String> filtered =
|
||||
FormUtils.filterChoiceSelections(
|
||||
selections, allowedOptions, listBoxName);
|
||||
if (!filtered.isEmpty()) {
|
||||
listBox.setValue(filtered);
|
||||
}
|
||||
} else {
|
||||
String filtered =
|
||||
FormUtils.filterSingleChoiceSelection(
|
||||
defaultValue, allowedOptions, listBoxName);
|
||||
if (filtered != null) {
|
||||
listBox.setValue(filtered);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
SIGNATURE("signature", "signature", PDSignatureField.class) {
|
||||
@Override
|
||||
PDTerminalField createField(PDAcroForm acroForm) {
|
||||
return new PDSignatureField(acroForm);
|
||||
}
|
||||
},
|
||||
BUTTON("button", "pushButton", PDPushButton.class) {
|
||||
@Override
|
||||
PDTerminalField createField(PDAcroForm acroForm) {
|
||||
return new PDPushButton(acroForm);
|
||||
}
|
||||
};
|
||||
|
||||
private static final Map<String, FormFieldTypeSupport> BY_TYPE =
|
||||
Arrays.stream(values())
|
||||
.collect(
|
||||
Collectors.toUnmodifiableMap(
|
||||
FormFieldTypeSupport::typeName, Function.identity()));
|
||||
|
||||
private final String typeName;
|
||||
private final String fallbackWidgetName;
|
||||
private final Class<? extends PDTerminalField> fieldClass;
|
||||
|
||||
FormFieldTypeSupport(
|
||||
String typeName,
|
||||
String fallbackWidgetName,
|
||||
Class<? extends PDTerminalField> fieldClass) {
|
||||
this.typeName = typeName;
|
||||
this.fallbackWidgetName = fallbackWidgetName;
|
||||
this.fieldClass = fieldClass;
|
||||
}
|
||||
|
||||
public static FormFieldTypeSupport forField(PDField field) {
|
||||
if (field == null) {
|
||||
return null;
|
||||
}
|
||||
for (FormFieldTypeSupport handler : values()) {
|
||||
if (handler.fieldClass.isInstance(field)) {
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static FormFieldTypeSupport forTypeName(String typeName) {
|
||||
if (typeName == null) {
|
||||
return null;
|
||||
}
|
||||
return BY_TYPE.get(typeName);
|
||||
}
|
||||
|
||||
private static void copyChoiceCharacteristics(PDChoice sourceField, PDChoice targetField) {
|
||||
if (sourceField == null || targetField == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
int flags = sourceField.getCOSObject().getInt(COSName.FF);
|
||||
targetField.getCOSObject().setInt(COSName.FF, flags);
|
||||
} catch (Exception e) {
|
||||
// ignore and continue
|
||||
}
|
||||
|
||||
if (sourceField instanceof PDListBox sourceList
|
||||
&& targetField instanceof PDListBox targetList) {
|
||||
try {
|
||||
targetList.setMultiSelect(sourceList.isMultiSelect());
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String typeName() {
|
||||
return typeName;
|
||||
}
|
||||
|
||||
String fallbackWidgetName() {
|
||||
return fallbackWidgetName;
|
||||
}
|
||||
|
||||
abstract PDTerminalField createField(PDAcroForm acroForm);
|
||||
|
||||
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||
// default no-op
|
||||
}
|
||||
|
||||
boolean doesNotsupportsDefinitionCreation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void applyNewFieldDefinition(
|
||||
PDTerminalField field,
|
||||
FormUtils.NewFormFieldDefinition definition,
|
||||
List<String> options)
|
||||
throws IOException {
|
||||
// default no-op
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,114 +0,0 @@
|
||||
package stirling.software.proprietary.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDResources;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDCheckBox;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDTextField;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Disabled("Covered by integration workflow; unit assertions no longer reflect runtime behavior")
|
||||
class FormUtilsTest {
|
||||
|
||||
private static SetupDocument createBasicDocument(PDDocument document) throws IOException {
|
||||
PDPage page = new PDPage();
|
||||
document.addPage(page);
|
||||
|
||||
PDAcroForm acroForm = new PDAcroForm(document);
|
||||
acroForm.setDefaultResources(new PDResources());
|
||||
acroForm.setNeedAppearances(true);
|
||||
document.getDocumentCatalog().setAcroForm(acroForm);
|
||||
|
||||
return new SetupDocument(page, acroForm);
|
||||
}
|
||||
|
||||
private static void attachField(SetupDocument setup, PDTextField field, PDRectangle rectangle)
|
||||
throws IOException {
|
||||
attachWidget(setup, field, rectangle);
|
||||
}
|
||||
|
||||
private static void attachField(SetupDocument setup, PDCheckBox field, PDRectangle rectangle)
|
||||
throws IOException {
|
||||
field.setExportValues(List.of("Yes"));
|
||||
attachWidget(setup, field, rectangle);
|
||||
}
|
||||
|
||||
private static void attachWidget(
|
||||
SetupDocument setup,
|
||||
org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField field,
|
||||
PDRectangle rectangle)
|
||||
throws IOException {
|
||||
PDAnnotationWidget widget = new PDAnnotationWidget();
|
||||
widget.setRectangle(rectangle);
|
||||
widget.setPage(setup.page);
|
||||
List<PDAnnotationWidget> widgets = field.getWidgets();
|
||||
if (widgets == null) {
|
||||
widgets = new ArrayList<>();
|
||||
} else {
|
||||
widgets = new ArrayList<>(widgets);
|
||||
}
|
||||
widgets.add(widget);
|
||||
field.setWidgets(widgets);
|
||||
setup.acroForm.getFields().add(field);
|
||||
setup.page.getAnnotations().add(widget);
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractFormFieldsReturnsFieldMetadata() throws IOException {
|
||||
try (PDDocument document = new PDDocument()) {
|
||||
SetupDocument setup = createBasicDocument(document);
|
||||
|
||||
PDTextField textField = new PDTextField(setup.acroForm);
|
||||
textField.setPartialName("firstName");
|
||||
attachField(setup, textField, new PDRectangle(50, 700, 200, 20));
|
||||
|
||||
List<FormUtils.FormFieldInfo> fields = FormUtils.extractFormFields(document);
|
||||
assertEquals(1, fields.size());
|
||||
FormUtils.FormFieldInfo info = fields.get(0);
|
||||
assertEquals("firstName", info.name());
|
||||
assertEquals("text", info.type());
|
||||
assertEquals(0, info.pageIndex());
|
||||
assertEquals("", info.value());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyFieldValuesPopulatesTextAndCheckbox() throws IOException {
|
||||
try (PDDocument document = new PDDocument()) {
|
||||
SetupDocument setup = createBasicDocument(document);
|
||||
|
||||
PDTextField textField = new PDTextField(setup.acroForm);
|
||||
textField.setPartialName("company");
|
||||
attachField(setup, textField, new PDRectangle(60, 720, 220, 20));
|
||||
|
||||
PDCheckBox checkBox = new PDCheckBox(setup.acroForm);
|
||||
checkBox.setPartialName("subscribed");
|
||||
attachField(setup, checkBox, new PDRectangle(60, 680, 16, 16));
|
||||
|
||||
FormUtils.applyFieldValues(
|
||||
document, Map.of("company", "Stirling", "subscribed", true), false);
|
||||
|
||||
assertEquals("Stirling", textField.getValueAsString());
|
||||
assertTrue(checkBox.isChecked());
|
||||
|
||||
FormUtils.applyFieldValues(document, Map.of("subscribed", false), false);
|
||||
assertFalse(checkBox.isChecked());
|
||||
assertEquals("Off", checkBox.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private record SetupDocument(PDPage page, PDAcroForm acroForm) {}
|
||||
}
|
||||
+1
-1
@@ -57,7 +57,7 @@ repositories {
|
||||
|
||||
allprojects {
|
||||
group = 'stirling.software'
|
||||
version = '2.0.0'
|
||||
version = '1.4.0'
|
||||
|
||||
configurations.configureEach {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# Run nginx as non-root user
|
||||
pid /tmp/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
Generated
+58
-84
@@ -10,25 +10,24 @@
|
||||
"license": "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@embedpdf/core": "^1.4.1",
|
||||
"@embedpdf/engines": "^1.4.1",
|
||||
"@embedpdf/plugin-annotation": "^1.4.1",
|
||||
"@embedpdf/plugin-export": "^1.4.1",
|
||||
"@embedpdf/plugin-history": "^1.4.1",
|
||||
"@embedpdf/plugin-interaction-manager": "^1.4.1",
|
||||
"@embedpdf/plugin-loader": "^1.4.1",
|
||||
"@embedpdf/plugin-pan": "^1.4.1",
|
||||
"@embedpdf/plugin-render": "^1.4.1",
|
||||
"@embedpdf/plugin-rotate": "^1.4.1",
|
||||
"@embedpdf/plugin-scroll": "^1.4.1",
|
||||
"@embedpdf/plugin-search": "^1.4.1",
|
||||
"@embedpdf/plugin-selection": "^1.4.1",
|
||||
"@embedpdf/plugin-spread": "^1.4.1",
|
||||
"@embedpdf/plugin-thumbnail": "^1.4.1",
|
||||
"@embedpdf/plugin-tiling": "^1.4.1",
|
||||
"@embedpdf/plugin-viewport": "^1.4.1",
|
||||
"@embedpdf/plugin-zoom": "^1.4.1",
|
||||
"@embedpdf/core": "^1.3.14",
|
||||
"@embedpdf/engines": "^1.3.14",
|
||||
"@embedpdf/plugin-annotation": "^1.3.14",
|
||||
"@embedpdf/plugin-export": "^1.3.14",
|
||||
"@embedpdf/plugin-history": "^1.3.14",
|
||||
"@embedpdf/plugin-interaction-manager": "^1.3.14",
|
||||
"@embedpdf/plugin-loader": "^1.3.14",
|
||||
"@embedpdf/plugin-pan": "^1.3.14",
|
||||
"@embedpdf/plugin-render": "^1.3.14",
|
||||
"@embedpdf/plugin-rotate": "^1.3.14",
|
||||
"@embedpdf/plugin-scroll": "^1.3.14",
|
||||
"@embedpdf/plugin-search": "^1.3.14",
|
||||
"@embedpdf/plugin-selection": "^1.3.14",
|
||||
"@embedpdf/plugin-spread": "^1.3.14",
|
||||
"@embedpdf/plugin-thumbnail": "^1.3.14",
|
||||
"@embedpdf/plugin-tiling": "^1.3.14",
|
||||
"@embedpdf/plugin-viewport": "^1.3.14",
|
||||
"@embedpdf/plugin-zoom": "^1.3.14",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@iconify/react": "^6.0.2",
|
||||
@@ -442,6 +441,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -488,6 +488,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -506,68 +507,12 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@dnd-kit/core": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/core/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@dnd-kit/utilities": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/utilities/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@embedpdf/core": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.4.1.tgz",
|
||||
"integrity": "sha512-TGpxn2CvAKRnOJWJ3bsK+dKBiCp75ehxftRUmv7wAmPomhnG5XrDfoWJungvO+zbbqAwso6PocdeXINVt3hlAw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@embedpdf/engines": "1.4.1",
|
||||
"@embedpdf/models": "1.4.1"
|
||||
@@ -651,6 +596,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.4.1.tgz",
|
||||
"integrity": "sha512-5WLDiNMH6tACkLGGv/lJtNsDeozOhSbrh0mjD1btHun8u7Yscu/Vf8tdJRUOsd+nULivo2nQ2NFNKu0OTbVo8w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.4.1"
|
||||
},
|
||||
@@ -667,6 +613,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.4.1.tgz",
|
||||
"integrity": "sha512-Ng02S9SFIAi9JZS5rI+NXSnZZ1Yk9YYRw4MlN2pig49qOyivZdz0oScZaYxQPewo8ccJkLeghjdeWswOBW/6cA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.4.1"
|
||||
},
|
||||
@@ -684,6 +631,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.4.1.tgz",
|
||||
"integrity": "sha512-m3ZOk8JygsLxoa4cZ+0BVB5pfRWuBCg2/gPqjhoFZNKTqAFw4J6HGUrhYKg94GRYe+w1cTJl/NbTBYuU5DOrsA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.4.1"
|
||||
},
|
||||
@@ -720,6 +668,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.4.1.tgz",
|
||||
"integrity": "sha512-gKCdNKw6WBHBEpTc2DLBWIWOxzsNnaNbpfeY6C4f2Bum0EO+XW3Hl2oIx1uaRHjIhhnXso1J3QweqelsPwDGwg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.4.1"
|
||||
},
|
||||
@@ -754,6 +703,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.4.1.tgz",
|
||||
"integrity": "sha512-Y9O+matB4j4fLim5s/jn7qIi+lMC9vmDJRpJhiWe8bvD9oYLP2xfD/DdhFgAjRKcNhPoxC+j8q8QN5BMeGAv2Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.4.1"
|
||||
},
|
||||
@@ -790,6 +740,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.4.1.tgz",
|
||||
"integrity": "sha512-lo5Ytk1PH0PrRKv6zKVupm4t02VGsqIrnSIeP6NO8Ujx0wfqEhj//sqIuO/EwfFVJD8lcQIP9UUo9y8baCrEog==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.4.1"
|
||||
},
|
||||
@@ -865,6 +816,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.4.1.tgz",
|
||||
"integrity": "sha512-+TgFHKPCLTBiDYe2DdsmTS37hwQgcZ3dYIc7bE0l5cp+GVwouu1h0MTmjL+90loizeWwCiu10E/zXR6hz+CUaQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.4.1"
|
||||
},
|
||||
@@ -1020,6 +972,7 @@
|
||||
"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",
|
||||
@@ -1063,6 +1016,7 @@
|
||||
"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",
|
||||
@@ -2093,6 +2047,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.5.tgz",
|
||||
"integrity": "sha512-PdVNLMgOS2vFhOujRi6/VC9ic8w3UDyKX7ftwDeJ7yQT8CiepUxfbWWYpVpnq23bdWh/7fIT2Pn1EY8r8GOk7g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.16",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -2143,6 +2098,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.5.tgz",
|
||||
"integrity": "sha512-0Wf08eWLKi3WkKlxnV1W5vfuN6wcvAV2VbhQlOy0R9nrWorGTtonQF6qqBE3PnJFYF1/ZE+HkYZQ/Dr7DmYSMQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react": "^18.x || ^19.x"
|
||||
}
|
||||
@@ -2210,6 +2166,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz",
|
||||
"integrity": "sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@mui/core-downloads-tracker": "^7.3.4",
|
||||
@@ -3893,6 +3850,7 @@
|
||||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@@ -4216,6 +4174,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -4226,6 +4185,7 @@
|
||||
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -4286,6 +4246,7 @@
|
||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
@@ -4999,7 +4960,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz",
|
||||
"integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
@@ -5009,7 +4969,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.22.tgz",
|
||||
"integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
@@ -5020,7 +4979,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz",
|
||||
"integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.22",
|
||||
"@vue/runtime-core": "3.5.22",
|
||||
@@ -5033,7 +4991,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.22.tgz",
|
||||
"integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
@@ -5060,6 +5017,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -5744,6 +5702,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
@@ -6789,7 +6748,8 @@
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz",
|
||||
"integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
@@ -7184,6 +7144,7 @@
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -7354,6 +7315,7 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -8676,6 +8638,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
@@ -9483,6 +9446,7 @@
|
||||
"integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@asamuzakjp/dom-selector": "^6.7.2",
|
||||
"cssstyle": "^5.3.1",
|
||||
@@ -11259,6 +11223,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -11538,6 +11503,7 @@
|
||||
"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"
|
||||
@@ -11910,6 +11876,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -11919,6 +11886,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -13353,9 +13321,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.43.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.0.tgz",
|
||||
"integrity": "sha512-1sRxVbgJAB+UGzwkc3GUoiBSzEOf0jqzccMaVoI2+pI+kASUe9qubslxace8+Mzhqw19k4syTA5niCIJwfXpOA==",
|
||||
"version": "5.42.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.42.3.tgz",
|
||||
"integrity": "sha512-+8dUmdJGvKSWEfbAgIaUmpD97s1bBAGxEf6s7wQonk+HNdMmrBZtpStzRypRqrYBFUmmhaUgBHUjraE8gLqWAw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -13589,6 +13557,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -13890,6 +13859,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -13972,6 +13942,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"napi-postinstall": "^0.3.0"
|
||||
},
|
||||
@@ -14176,6 +14147,7 @@
|
||||
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -14327,6 +14299,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -14340,6 +14313,7 @@
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
|
||||
+18
-20
@@ -6,25 +6,24 @@
|
||||
"proxy": "http://localhost:8080",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@embedpdf/core": "^1.4.1",
|
||||
"@embedpdf/engines": "^1.4.1",
|
||||
"@embedpdf/plugin-annotation": "^1.4.1",
|
||||
"@embedpdf/plugin-export": "^1.4.1",
|
||||
"@embedpdf/plugin-history": "^1.4.1",
|
||||
"@embedpdf/plugin-interaction-manager": "^1.4.1",
|
||||
"@embedpdf/plugin-loader": "^1.4.1",
|
||||
"@embedpdf/plugin-pan": "^1.4.1",
|
||||
"@embedpdf/plugin-render": "^1.4.1",
|
||||
"@embedpdf/plugin-rotate": "^1.4.1",
|
||||
"@embedpdf/plugin-scroll": "^1.4.1",
|
||||
"@embedpdf/plugin-search": "^1.4.1",
|
||||
"@embedpdf/plugin-selection": "^1.4.1",
|
||||
"@embedpdf/plugin-spread": "^1.4.1",
|
||||
"@embedpdf/plugin-thumbnail": "^1.4.1",
|
||||
"@embedpdf/plugin-tiling": "^1.4.1",
|
||||
"@embedpdf/plugin-viewport": "^1.4.1",
|
||||
"@embedpdf/plugin-zoom": "^1.4.1",
|
||||
"@embedpdf/core": "^1.3.14",
|
||||
"@embedpdf/engines": "^1.3.14",
|
||||
"@embedpdf/plugin-annotation": "^1.3.14",
|
||||
"@embedpdf/plugin-export": "^1.3.14",
|
||||
"@embedpdf/plugin-history": "^1.3.14",
|
||||
"@embedpdf/plugin-interaction-manager": "^1.3.14",
|
||||
"@embedpdf/plugin-loader": "^1.3.14",
|
||||
"@embedpdf/plugin-pan": "^1.3.14",
|
||||
"@embedpdf/plugin-render": "^1.3.14",
|
||||
"@embedpdf/plugin-rotate": "^1.3.14",
|
||||
"@embedpdf/plugin-scroll": "^1.3.14",
|
||||
"@embedpdf/plugin-search": "^1.3.14",
|
||||
"@embedpdf/plugin-selection": "^1.3.14",
|
||||
"@embedpdf/plugin-spread": "^1.3.14",
|
||||
"@embedpdf/plugin-thumbnail": "^1.3.14",
|
||||
"@embedpdf/plugin-tiling": "^1.3.14",
|
||||
"@embedpdf/plugin-viewport": "^1.3.14",
|
||||
"@embedpdf/plugin-zoom": "^1.3.14",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.0",
|
||||
@@ -67,7 +66,6 @@
|
||||
"preview": "vite preview",
|
||||
"tauri-dev": "tauri dev --no-watch",
|
||||
"tauri-build": "tauri build",
|
||||
"tauri-clean": "cd src-tauri && cargo clean && cd .. && rm -rf dist build",
|
||||
"typecheck": "npm run typecheck:proprietary",
|
||||
"typecheck:core": "tsc --noEmit --project tsconfig.core.json",
|
||||
"typecheck:proprietary": "tsc --noEmit --project tsconfig.proprietary.json",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve"><defs id="defs173"><linearGradient id="XMLID_5_" x1="304.496" x2="316.036" y1="422.91" y2="326.263" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#dcf1f3" id="stop156"/><stop offset="1" style="stop-color:#c2c2c9" id="stop158"/></linearGradient></defs><style id="style150" type="text/css">.st1{fill:#c02223}.st2{fill:#882425}.st3{fill:url(#XMLID_5_)}.st4{fill:url(#XMLID_7_)}</style><g id="XMLID_4_"><path id="XMLID_131_" d="M 347.01402,14.355825 98.978019,69.02261 C 73.825483,74.547445 55.942464,96.792175 55.942464,122.52628 v 315.06096 c 0,22.39012 16.719895,41.14548 38.819234,43.76251 L 224.8861,498.36042 339.48636,384.26465 455.76603,265.15425 453.73057,84.870162 C 453.43979,62.916214 433.08513,46.632491 411.71274,51.284984 l -28.78729,6.251786 0.14539,-13.666697 C 383.36162,24.678542 365.62399,10.284894 347.01402,14.355825 Z" class="st1" style="stroke-width:1.45391"/><path id="XMLID_117_" d="m 383.21622,57.53677 v 285.8375 L 456.05681,265.00885 454.02135,78.763767 C 453.87595,59.863016 436.28372,45.905539 417.81914,49.97647 Z" class="st2" style="stroke-width:1.45391"/><polygon id="XMLID_18_" points="234.7 422.6 368.5 387.7 393.5 262.2" class="st3" style="fill:url(#XMLID_5_)" transform="matrix(1.4556308,0,0,1.4548265,-116.73161,-116.45231)"/><linearGradient id="XMLID_7_" x1="223.084" x2="241.417" y1="372.756" y2="114.557" gradientTransform="matrix(1.4539039,0,0,1.4539039,-116.19976,-116.20474)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#dcf1f3" id="stop163"/><stop offset="1" style="stop-color:#c2c2c9" id="stop165"/></linearGradient><path id="XMLID_6_" d="m 282.89686,214.84917 c 0,0 -22.24473,-28.93269 -38.67384,-36.78377 -10.46811,-4.94327 -26.02489,-6.83335 -38.23768,-0.72695 -18.02841,9.0142 -19.91848,34.31213 -3.34397,44.34406 3.92553,2.47165 9.15959,4.50711 15.99294,6.10641 36.63838,8.43264 97.12077,25.87949 89.70587,96.10304 0,0 -4.21633,65.86185 -73.56753,73.42215 -12.2128,1.30851 -24.57098,0.43617 -36.493,-2.32625 -16.42911,-3.63476 -45.50719,-11.04967 -59.75545,-19.91849 l -2.61703,-75.16682 h 6.97875 c 0,0 13.81208,33.43978 53.06749,49.57812 7.26952,2.90781 15.26599,4.07093 22.97168,2.90781 9.74116,-1.45391 21.22699,-6.68796 25.87949,-22.53551 0,0 7.85108,-23.11707 -32.85823,-35.76604 -32.56744,-10.17733 -63.24481,-20.64543 -75.89378,-54.95757 -5.961,-16.28371 -6.97874,-34.31212 -2.90781,-51.61358 5.37944,-22.53551 20.79082,-54.23062 64.40794,-67.89732 0,0 57.28381,-15.55677 96.53922,5.52484 l -1.74468,89.70587 z" class="st4" style="fill:url(#XMLID_7_);stroke-width:1.45391"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Generated
+84
-434
@@ -81,137 +81,6 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"parking",
|
||||
"polling",
|
||||
"rustix",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-signal"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"atomic-waker",
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atk"
|
||||
version = "0.18.2"
|
||||
@@ -286,6 +155,12 @@ dependencies = [
|
||||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -313,19 +188,6 @@ dependencies = [
|
||||
"objc2 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh"
|
||||
version = "1.5.7"
|
||||
@@ -558,6 +420,36 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
"cocoa-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics 0.22.3",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa-foundation"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics-types 0.1.3",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -568,15 +460,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
@@ -619,6 +502,19 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics"
|
||||
version = "0.22.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics-types 0.1.3",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics"
|
||||
version = "0.24.0"
|
||||
@@ -627,11 +523,22 @@ checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics-types",
|
||||
"core-graphics-types 0.2.0",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics-types"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics-types"
|
||||
version = "0.2.0"
|
||||
@@ -643,15 +550,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-services"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0aa845ab21b847ee46954be761815f18f16469b29ef3ba250241b1b8bab659a"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -936,33 +834,6 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.4"
|
||||
@@ -1000,27 +871,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
@@ -1176,19 +1026,6 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
@@ -1575,12 +1412,6 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
@@ -2161,6 +1992,15 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.14.1"
|
||||
@@ -2308,19 +2148,6 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodrop"
|
||||
version = "0.1.14"
|
||||
@@ -2373,6 +2200,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
|
||||
dependencies = [
|
||||
"malloc_buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-sys"
|
||||
version = "0.3.5"
|
||||
@@ -2705,16 +2541,6 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.2.3"
|
||||
@@ -2750,12 +2576,6 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -2937,17 +2757,6 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
@@ -2980,20 +2789,6 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.3"
|
||||
@@ -3894,7 +3689,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"core-graphics",
|
||||
"core-graphics 0.24.0",
|
||||
"foreign-types 0.5.0",
|
||||
"js-sys",
|
||||
"log",
|
||||
@@ -3940,19 +3735,14 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "stirling-pdf"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-services",
|
||||
"cocoa",
|
||||
"log",
|
||||
"objc",
|
||||
"once_cell",
|
||||
"reqwest 0.11.27",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3961,7 +3751,6 @@ dependencies = [
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-log",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-single-instance",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -4098,7 +3887,7 @@ dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.6.2",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics",
|
||||
"core-graphics 0.24.0",
|
||||
"crossbeam-channel",
|
||||
"dispatch",
|
||||
"dlopen2",
|
||||
@@ -4348,21 +4137,6 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-single-instance"
|
||||
version = "2.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd707f8c86b4e3004e2c141fa24351f1909ba40ce1b8437e30d5ed5277dd3710"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"thiserror 2.0.17",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.9.1"
|
||||
@@ -4770,21 +4544,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.34"
|
||||
@@ -4834,17 +4596,6 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-property"
|
||||
version = "0.9.0"
|
||||
@@ -5860,67 +5611,6 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"nix",
|
||||
"ordered-stream",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
"winnow 0.7.13",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "5.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.4.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_names"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"winnow 0.7.13",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.27"
|
||||
@@ -5994,43 +5684,3 @@ dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c"
|
||||
dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"winnow 0.7.13",
|
||||
"zvariant_derive",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "5.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.4.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.108",
|
||||
"winnow 0.7.13",
|
||||
]
|
||||
|
||||
@@ -10,9 +10,6 @@ rust-version = "1.77.2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lints.rust]
|
||||
warnings = "deny"
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
@@ -28,10 +25,11 @@ tauri = { version = "2.9.0", features = [ "devtools"] }
|
||||
tauri-plugin-log = "2.0.0-rc"
|
||||
tauri-plugin-shell = "2.1.0"
|
||||
tauri-plugin-fs = "2.4.4"
|
||||
tauri-plugin-single-instance = "2.0.1"
|
||||
tokio = { version = "1.0", features = ["time"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
|
||||
# macOS-specific dependencies for native file opening
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.10"
|
||||
core-services = "1.0"
|
||||
objc = "0.2"
|
||||
cocoa = "0.24"
|
||||
once_cell = "1.19"
|
||||
|
||||
@@ -234,8 +234,6 @@ fn monitor_backend_output(mut rx: tauri::async_runtime::Receiver<tauri_plugin_sh
|
||||
match event {
|
||||
tauri_plugin_shell::process::CommandEvent::Stdout(output) => {
|
||||
let output_str = String::from_utf8_lossy(&output);
|
||||
// Strip exactly one trailing newline to avoid double newlines
|
||||
let output_str = output_str.strip_suffix('\n').unwrap_or(&output_str);
|
||||
add_log(format!("📤 Backend: {}", output_str));
|
||||
|
||||
// Look for startup indicators
|
||||
@@ -252,8 +250,6 @@ fn monitor_backend_output(mut rx: tauri::async_runtime::Receiver<tauri_plugin_sh
|
||||
}
|
||||
tauri_plugin_shell::process::CommandEvent::Stderr(output) => {
|
||||
let output_str = String::from_utf8_lossy(&output);
|
||||
// Strip exactly one trailing newline to avoid double newlines
|
||||
let output_str = output_str.strip_suffix('\n').unwrap_or(&output_str);
|
||||
add_log(format!("📥 Backend Error: {}", output_str));
|
||||
|
||||
// Look for error indicators
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
use crate::utils::add_log;
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use std::process::Command;
|
||||
|
||||
/// Check if Stirling PDF is the default PDF handler
|
||||
#[tauri::command]
|
||||
pub fn is_default_pdf_handler() -> Result<bool, String> {
|
||||
add_log("🔍 Checking if app is default PDF handler".to_string());
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
check_default_windows()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
check_default_macos()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
check_default_linux()
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to set/prompt for Stirling PDF as default PDF handler
|
||||
#[tauri::command]
|
||||
pub fn set_as_default_pdf_handler() -> Result<String, String> {
|
||||
add_log("⚙️ Attempting to set as default PDF handler".to_string());
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
set_default_windows()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
set_default_macos()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
set_default_linux()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Windows Implementation
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn check_default_windows() -> Result<bool, String> {
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
// Query the default handler for .pdf extension
|
||||
let output = Command::new("cmd")
|
||||
.args(["/C", "assoc .pdf"])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to check default app: {}", e))?;
|
||||
|
||||
let assoc = String::from_utf8_lossy(&output.stdout);
|
||||
add_log(format!("Windows PDF association: {}", assoc.trim()));
|
||||
|
||||
// Get the ProgID for .pdf files
|
||||
if let Some(prog_id) = assoc.trim().strip_prefix(".pdf=") {
|
||||
// Query what application handles this ProgID
|
||||
let output = Command::new("cmd")
|
||||
.args(["/C", &format!("ftype {}", prog_id)])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to query file type: {}", e))?;
|
||||
|
||||
let ftype = String::from_utf8_lossy(&output.stdout);
|
||||
add_log(format!("Windows file type: {}", ftype.trim()));
|
||||
|
||||
// Check if it contains "Stirling" or our app name
|
||||
let is_default = ftype.to_lowercase().contains("stirling");
|
||||
Ok(is_default)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn set_default_windows() -> Result<String, String> {
|
||||
// On Windows 10+, we need to open the Default Apps settings
|
||||
// as programmatic setting requires a signed installer
|
||||
Command::new("cmd")
|
||||
.args(["/C", "start", "ms-settings:defaultapps"])
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open default apps settings: {}", e))?;
|
||||
|
||||
add_log("Opened Windows Default Apps settings".to_string());
|
||||
Ok("opened_settings".to_string())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// macOS Implementation (using LaunchServices framework)
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn check_default_macos() -> Result<bool, String> {
|
||||
use core_foundation::base::TCFType;
|
||||
use core_foundation::string::{CFString, CFStringRef};
|
||||
use std::os::raw::c_int;
|
||||
|
||||
// Define the LSCopyDefaultRoleHandlerForContentType function
|
||||
#[link(name = "CoreServices", kind = "framework")]
|
||||
extern "C" {
|
||||
fn LSCopyDefaultRoleHandlerForContentType(
|
||||
content_type: CFStringRef,
|
||||
role: c_int,
|
||||
) -> CFStringRef;
|
||||
}
|
||||
|
||||
const K_LS_ROLES_ALL: c_int = 0xFFFFFFFF_u32 as c_int;
|
||||
|
||||
unsafe {
|
||||
// Query the default handler for "com.adobe.pdf" (PDF UTI - standard macOS identifier)
|
||||
let pdf_uti = CFString::new("com.adobe.pdf");
|
||||
let handler_ref = LSCopyDefaultRoleHandlerForContentType(pdf_uti.as_concrete_TypeRef(), K_LS_ROLES_ALL);
|
||||
|
||||
if handler_ref.is_null() {
|
||||
add_log("No default PDF handler found".to_string());
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let handler = CFString::wrap_under_create_rule(handler_ref);
|
||||
let handler_str = handler.to_string();
|
||||
add_log(format!("macOS PDF handler: {}", handler_str));
|
||||
|
||||
// Check if it's our bundle identifier
|
||||
let is_default = handler_str == "stirling.pdf.dev";
|
||||
Ok(is_default)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn set_default_macos() -> Result<String, String> {
|
||||
use core_foundation::base::TCFType;
|
||||
use core_foundation::string::{CFString, CFStringRef};
|
||||
use std::os::raw::c_int;
|
||||
|
||||
// Define the LSSetDefaultRoleHandlerForContentType function
|
||||
#[link(name = "CoreServices", kind = "framework")]
|
||||
extern "C" {
|
||||
fn LSSetDefaultRoleHandlerForContentType(
|
||||
content_type: CFStringRef,
|
||||
role: c_int,
|
||||
handler_bundle_id: CFStringRef,
|
||||
) -> c_int; // OSStatus
|
||||
}
|
||||
|
||||
const K_LS_ROLES_ALL: c_int = 0xFFFFFFFF_u32 as c_int;
|
||||
|
||||
unsafe {
|
||||
// Set our app as the default handler for PDF files
|
||||
let pdf_uti = CFString::new("com.adobe.pdf");
|
||||
let our_bundle_id = CFString::new("stirling.pdf.dev");
|
||||
|
||||
let status = LSSetDefaultRoleHandlerForContentType(
|
||||
pdf_uti.as_concrete_TypeRef(),
|
||||
K_LS_ROLES_ALL,
|
||||
our_bundle_id.as_concrete_TypeRef(),
|
||||
);
|
||||
|
||||
if status == 0 {
|
||||
add_log("Successfully triggered default app dialog".to_string());
|
||||
Ok("set_successfully".to_string())
|
||||
} else {
|
||||
let error_msg = format!("LaunchServices returned status: {}", status);
|
||||
add_log(error_msg.clone());
|
||||
Err(error_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Linux Implementation
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn check_default_linux() -> Result<bool, String> {
|
||||
// Use xdg-mime to check the default application for PDF files
|
||||
let output = Command::new("xdg-mime")
|
||||
.args(["query", "default", "application/pdf"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to check default app: {}", e))?;
|
||||
|
||||
let handler = String::from_utf8_lossy(&output.stdout);
|
||||
add_log(format!("Linux PDF handler: {}", handler.trim()));
|
||||
|
||||
// Check if it's our .desktop file
|
||||
let is_default = handler.trim() == "stirling-pdf.desktop";
|
||||
Ok(is_default)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn set_default_linux() -> Result<String, String> {
|
||||
// Use xdg-mime to set the default application for PDF files
|
||||
let result = Command::new("xdg-mime")
|
||||
.args(["default", "stirling-pdf.desktop", "application/pdf"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to set default app: {}", e))?;
|
||||
|
||||
if result.status.success() {
|
||||
add_log("Set as default PDF handler on Linux".to_string());
|
||||
Ok("set_successfully".to_string())
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&result.stderr);
|
||||
add_log(format!("Failed to set default: {}", error));
|
||||
Err(format!("Failed to set as default: {}", error))
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,48 @@
|
||||
use crate::utils::add_log;
|
||||
use std::sync::Mutex;
|
||||
|
||||
// Store the opened file paths globally (supports multiple files)
|
||||
static OPENED_FILES: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
// Store the opened file path globally
|
||||
static OPENED_FILE: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
// Add an opened file path
|
||||
pub fn add_opened_file(file_path: String) {
|
||||
let mut opened_files = OPENED_FILES.lock().unwrap();
|
||||
opened_files.push(file_path.clone());
|
||||
add_log(format!("📂 File stored for later retrieval: {}", file_path));
|
||||
// Set the opened file path (called by macOS file open events)
|
||||
pub fn set_opened_file(file_path: String) {
|
||||
let mut opened_file = OPENED_FILE.lock().unwrap();
|
||||
*opened_file = Some(file_path.clone());
|
||||
add_log(format!("📂 File opened via file open event: {}", file_path));
|
||||
}
|
||||
|
||||
// Command to get opened file paths (if app was launched with files)
|
||||
// Command to get opened file path (if app was launched with a file)
|
||||
#[tauri::command]
|
||||
pub async fn get_opened_files() -> Result<Vec<String>, String> {
|
||||
// Get all files from the OPENED_FILES store
|
||||
// Command line args are processed in setup() callback and added to this store
|
||||
// Additional files from second instances or events are also added here
|
||||
let opened_files = OPENED_FILES.lock().unwrap();
|
||||
let all_files = opened_files.clone();
|
||||
|
||||
add_log(format!("📂 Returning {} opened file(s)", all_files.len()));
|
||||
Ok(all_files)
|
||||
pub async fn get_opened_file() -> Result<Option<String>, String> {
|
||||
// First check if we have a file from macOS file open events
|
||||
{
|
||||
let opened_file = OPENED_FILE.lock().unwrap();
|
||||
if let Some(ref file_path) = *opened_file {
|
||||
add_log(format!("📂 Returning stored opened file: {}", file_path));
|
||||
return Ok(Some(file_path.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to command line arguments (Windows/Linux)
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
// Look for a PDF file argument (skip the first arg which is the executable)
|
||||
for arg in args.iter().skip(1) {
|
||||
if arg.ends_with(".pdf") && std::path::Path::new(arg).exists() {
|
||||
add_log(format!("📂 PDF file opened via command line: {}", arg));
|
||||
return Ok(Some(arg.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// Command to clear the opened files (after processing)
|
||||
// Command to clear the opened file (after processing)
|
||||
#[tauri::command]
|
||||
pub async fn clear_opened_files() -> Result<(), String> {
|
||||
let mut opened_files = OPENED_FILES.lock().unwrap();
|
||||
opened_files.clear();
|
||||
add_log("📂 Cleared opened files".to_string());
|
||||
pub async fn clear_opened_file() -> Result<(), String> {
|
||||
let mut opened_file = OPENED_FILE.lock().unwrap();
|
||||
*opened_file = None;
|
||||
add_log("📂 Cleared opened file".to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
pub mod backend;
|
||||
pub mod health;
|
||||
pub mod files;
|
||||
pub mod default_app;
|
||||
|
||||
pub use backend::{start_backend, cleanup_backend};
|
||||
pub use health::check_backend_health;
|
||||
pub use files::{get_opened_files, clear_opened_files, add_opened_file};
|
||||
pub use default_app::{is_default_pdf_handler, set_as_default_pdf_handler};
|
||||
pub use files::{get_opened_file, clear_opened_file, set_opened_file};
|
||||
@@ -0,0 +1,189 @@
|
||||
/// Multi-platform file opening handler
|
||||
///
|
||||
/// This module provides unified file opening support across platforms:
|
||||
/// - macOS: Uses native NSApplication delegate (proper Apple Events)
|
||||
/// - Windows/Linux: Uses command line arguments (fallback approach)
|
||||
/// - All platforms: Runtime event handling via Tauri events
|
||||
|
||||
use crate::utils::add_log;
|
||||
use crate::commands::set_opened_file;
|
||||
use tauri::AppHandle;
|
||||
|
||||
|
||||
/// Initialize file handling for the current platform
|
||||
pub fn initialize_file_handler(app: &AppHandle<tauri::Wry>) {
|
||||
add_log("🔧 Initializing file handler...".to_string());
|
||||
|
||||
// Platform-specific initialization
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
add_log("🍎 Using macOS native file handler".to_string());
|
||||
macos_native::register_open_file_handler(app);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
add_log("🖥️ Using command line argument file handler".to_string());
|
||||
let _ = app; // Suppress unused variable warning
|
||||
}
|
||||
|
||||
// Universal: Check command line arguments (works on all platforms)
|
||||
check_command_line_args();
|
||||
}
|
||||
|
||||
/// Early initialization for macOS delegate registration
|
||||
pub fn early_init() {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
add_log("🔄 Early macOS initialization...".to_string());
|
||||
macos_native::register_delegate_early();
|
||||
}
|
||||
}
|
||||
|
||||
/// Check command line arguments for file paths (universal fallback)
|
||||
fn check_command_line_args() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
add_log(format!("🔍 DEBUG: All command line args: {:?}", args));
|
||||
|
||||
// Check command line arguments for file opening
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
add_log(format!("🔍 DEBUG: Arg {}: {}", i, arg));
|
||||
if i > 0 && arg.ends_with(".pdf") && std::path::Path::new(arg).exists() {
|
||||
add_log(format!("📂 File argument detected: {}", arg));
|
||||
set_opened_file(arg.clone());
|
||||
break; // Only handle the first PDF file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle runtime file open events (for future single-instance support)
|
||||
#[allow(dead_code)]
|
||||
pub fn handle_runtime_file_open(file_path: String) {
|
||||
if file_path.ends_with(".pdf") && std::path::Path::new(&file_path).exists() {
|
||||
add_log(format!("📂 Runtime file open: {}", file_path));
|
||||
set_opened_file(file_path);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos_native {
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
use objc::runtime::{Class, Object, Sel};
|
||||
use cocoa::appkit::NSApplication;
|
||||
use cocoa::base::{id, nil};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::Mutex;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
use crate::utils::add_log;
|
||||
use crate::commands::set_opened_file;
|
||||
|
||||
// Static app handle storage
|
||||
static APP_HANDLE: Lazy<Mutex<Option<AppHandle<tauri::Wry>>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
// Store files opened during launch
|
||||
static LAUNCH_FILES: Lazy<Mutex<Vec<String>>> = Lazy::new(|| Mutex::new(Vec::new()));
|
||||
|
||||
|
||||
extern "C" fn open_files(_self: &Object, _cmd: Sel, _sender: id, filenames: id) {
|
||||
unsafe {
|
||||
add_log(format!("📂 macOS native openFiles event called"));
|
||||
|
||||
// filenames is an NSArray of NSString objects
|
||||
let count: usize = msg_send![filenames, count];
|
||||
add_log(format!("📂 Number of files to open: {}", count));
|
||||
|
||||
for i in 0..count {
|
||||
let filename: id = msg_send![filenames, objectAtIndex: i];
|
||||
let cstr = {
|
||||
let bytes: *const std::os::raw::c_char = msg_send![filename, UTF8String];
|
||||
std::ffi::CStr::from_ptr(bytes)
|
||||
};
|
||||
|
||||
if let Ok(path) = cstr.to_str() {
|
||||
add_log(format!("📂 macOS file open: {}", path));
|
||||
if path.ends_with(".pdf") {
|
||||
// Always set the opened file for command-line interface
|
||||
set_opened_file(path.to_string());
|
||||
|
||||
if let Some(app) = APP_HANDLE.lock().unwrap().as_ref() {
|
||||
// App is running, emit event immediately
|
||||
add_log(format!("✅ App running, emitting file event: {}", path));
|
||||
let _ = app.emit("macos://open-file", path.to_string());
|
||||
} else {
|
||||
// App not ready yet, store for later processing
|
||||
add_log(format!("🚀 App not ready, storing file for later: {}", path));
|
||||
LAUNCH_FILES.lock().unwrap().push(path.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register the delegate immediately when the module loads
|
||||
pub fn register_delegate_early() {
|
||||
add_log("🔧 Registering macOS delegate early...".to_string());
|
||||
|
||||
unsafe {
|
||||
let ns_app = NSApplication::sharedApplication(nil);
|
||||
|
||||
// Check if there's already a delegate
|
||||
let existing_delegate: id = msg_send![ns_app, delegate];
|
||||
if existing_delegate != nil {
|
||||
add_log("⚠️ Tauri already has an NSApplication delegate, trying to extend it...".to_string());
|
||||
|
||||
// Try to add our method to the existing delegate's class
|
||||
let delegate_class: id = msg_send![existing_delegate, class];
|
||||
let class_name: *const std::os::raw::c_char = msg_send![delegate_class, name];
|
||||
let class_name_str = std::ffi::CStr::from_ptr(class_name).to_string_lossy();
|
||||
add_log(format!("🔍 Existing delegate class: {}", class_name_str));
|
||||
|
||||
// This approach won't work with existing classes, so let's try a different method
|
||||
// We'll use method swizzling or create a new delegate that forwards to the old one
|
||||
add_log("🔄 Will try alternative approach...".to_string());
|
||||
}
|
||||
|
||||
let delegate_class = Class::get("StirlingAppDelegate").unwrap_or_else(|| {
|
||||
let superclass = class!(NSObject);
|
||||
let mut decl = objc::declare::ClassDecl::new("StirlingAppDelegate", superclass).unwrap();
|
||||
|
||||
// Add file opening delegate method (modern plural version)
|
||||
decl.add_method(
|
||||
sel!(application:openFiles:),
|
||||
open_files as extern "C" fn(&Object, Sel, id, id)
|
||||
);
|
||||
|
||||
decl.register()
|
||||
});
|
||||
|
||||
let delegate: id = msg_send![delegate_class, new];
|
||||
let _: () = msg_send![ns_app, setDelegate:delegate];
|
||||
}
|
||||
|
||||
add_log("✅ macOS delegate registered early".to_string());
|
||||
}
|
||||
|
||||
pub fn register_open_file_handler(app: &AppHandle<tauri::Wry>) {
|
||||
add_log("🔧 Connecting app handle to file handler...".to_string());
|
||||
|
||||
// Store the app handle
|
||||
*APP_HANDLE.lock().unwrap() = Some(app.clone());
|
||||
|
||||
// Process any files that were opened during launch
|
||||
let launch_files = {
|
||||
let mut files = LAUNCH_FILES.lock().unwrap();
|
||||
let result = files.clone();
|
||||
files.clear();
|
||||
result
|
||||
};
|
||||
|
||||
for file_path in launch_files {
|
||||
add_log(format!("📂 Processing stored launch file: {}", file_path));
|
||||
set_opened_file(file_path.clone());
|
||||
let _ = app.emit("macos://open-file", file_path);
|
||||
}
|
||||
|
||||
add_log("✅ macOS file handler connected successfully".to_string());
|
||||
}
|
||||
}
|
||||
@@ -1,72 +1,30 @@
|
||||
use tauri::{RunEvent, WindowEvent, Emitter, Manager};
|
||||
use tauri::{RunEvent, WindowEvent, Emitter};
|
||||
|
||||
mod utils;
|
||||
mod commands;
|
||||
mod file_handler;
|
||||
|
||||
use commands::{
|
||||
start_backend,
|
||||
check_backend_health,
|
||||
get_opened_files,
|
||||
clear_opened_files,
|
||||
cleanup_backend,
|
||||
add_opened_file,
|
||||
is_default_pdf_handler,
|
||||
set_as_default_pdf_handler,
|
||||
};
|
||||
use commands::{start_backend, check_backend_health, get_opened_file, clear_opened_file, cleanup_backend, set_opened_file};
|
||||
use utils::{add_log, get_tauri_logs};
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// Initialize file handler early for macOS
|
||||
file_handler::early_init();
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
||||
// This callback runs when a second instance tries to start
|
||||
add_log(format!("📂 Second instance detected with args: {:?}", args));
|
||||
|
||||
// Scan args for PDF files (skip first arg which is the executable)
|
||||
for arg in args.iter().skip(1) {
|
||||
if std::path::Path::new(arg).exists() {
|
||||
add_log(format!("📂 Forwarding file to existing instance: {}", arg));
|
||||
|
||||
// Store file for later retrieval (in case frontend isn't ready yet)
|
||||
add_opened_file(arg.clone());
|
||||
|
||||
// Bring the existing window to front
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.set_focus();
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emit a generic notification that files were added (frontend will re-read storage)
|
||||
let _ = app.emit("files-changed", ());
|
||||
}))
|
||||
.setup(|_app| {
|
||||
.setup(|app| {
|
||||
add_log("🚀 Tauri app setup started".to_string());
|
||||
|
||||
// Process command line arguments on first launch
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
for arg in args.iter().skip(1) {
|
||||
if std::path::Path::new(arg).exists() {
|
||||
add_log(format!("📂 Initial file from command line: {}", arg));
|
||||
add_opened_file(arg.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Initialize platform-specific file handler
|
||||
file_handler::initialize_file_handler(&app.handle());
|
||||
|
||||
add_log("🔍 DEBUG: Setup completed".to_string());
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
start_backend,
|
||||
check_backend_health,
|
||||
get_opened_files,
|
||||
clear_opened_files,
|
||||
get_tauri_logs,
|
||||
is_default_pdf_handler,
|
||||
set_as_default_pdf_handler,
|
||||
])
|
||||
.invoke_handler(tauri::generate_handler![start_backend, check_backend_health, get_opened_file, clear_opened_file, get_tauri_logs])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
.run(|app_handle, event| {
|
||||
@@ -85,22 +43,17 @@ pub fn run() {
|
||||
#[cfg(target_os = "macos")]
|
||||
RunEvent::Opened { urls } => {
|
||||
add_log(format!("📂 Tauri file opened event: {:?}", urls));
|
||||
let mut added_files = false;
|
||||
for url in urls {
|
||||
let url_str = url.as_str();
|
||||
if url_str.starts_with("file://") {
|
||||
let file_path = url_str.strip_prefix("file://").unwrap_or(url_str);
|
||||
if file_path.ends_with(".pdf") {
|
||||
add_log(format!("📂 Processing opened PDF: {}", file_path));
|
||||
add_opened_file(file_path.to_string());
|
||||
added_files = true;
|
||||
set_opened_file(file_path.to_string());
|
||||
let _ = app_handle.emit("macos://open-file", file_path.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Emit a generic notification that files were added (frontend will re-read storage)
|
||||
if added_files {
|
||||
let _ = app_handle.emit("files-changed", ());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Only log unhandled events in debug mode to reduce noise
|
||||
@@ -109,4 +62,4 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["deb", "rpm", "dmg", "app", "msi"],
|
||||
"targets": ["deb", "rpm", "dmg", "msi"],
|
||||
"icon": [
|
||||
"icons/icon.png",
|
||||
"icons/icon.icns",
|
||||
@@ -50,12 +50,6 @@
|
||||
"deb": {
|
||||
"desktopTemplate": "stirling-pdf.desktop"
|
||||
}
|
||||
},
|
||||
"macOS": {
|
||||
"minimumSystemVersion": "10.15",
|
||||
"signingIdentity": null,
|
||||
"entitlements": null,
|
||||
"providerShortName": null
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Suspense } from "react";
|
||||
import { AppProviders } from "@app/components/AppProviders";
|
||||
import { AppLayout } from "@app/components/AppLayout";
|
||||
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
|
||||
import HomePage from "@app/pages/HomePage";
|
||||
import OnboardingTour from "@app/components/onboarding/OnboardingTour";
|
||||
@@ -17,10 +16,8 @@ export default function App() {
|
||||
return (
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<AppProviders>
|
||||
<AppLayout>
|
||||
<HomePage />
|
||||
<OnboardingTour />
|
||||
</AppLayout>
|
||||
<HomePage />
|
||||
<OnboardingTour />
|
||||
</AppProviders>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { useBanner } from '@app/contexts/BannerContext';
|
||||
|
||||
interface AppLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* App layout wrapper that handles banner rendering and viewport sizing
|
||||
* Automatically adjusts child components to fit remaining space after banner
|
||||
*/
|
||||
export function AppLayout({ children }: AppLayoutProps) {
|
||||
const { banner } = useBanner();
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
.h-screen,
|
||||
.right-rail {
|
||||
height: 100% !important;
|
||||
}
|
||||
`}</style>
|
||||
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||
{banner}
|
||||
<div style={{ flex: 1, minHeight: 0, height: 0 }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -8,18 +8,14 @@ import { ToolWorkflowProvider } from "@app/contexts/ToolWorkflowContext";
|
||||
import { HotkeyProvider } from "@app/contexts/HotkeyContext";
|
||||
import { SidebarProvider } from "@app/contexts/SidebarContext";
|
||||
import { PreferencesProvider } from "@app/contexts/PreferencesContext";
|
||||
import { AppConfigProvider, AppConfigProviderProps, AppConfigRetryOptions } from "@app/contexts/AppConfigContext";
|
||||
import { AppConfigProvider } from "@app/contexts/AppConfigContext";
|
||||
import { RightRailProvider } from "@app/contexts/RightRailContext";
|
||||
import { ViewerProvider } from "@app/contexts/ViewerContext";
|
||||
import { SignatureProvider } from "@app/contexts/SignatureContext";
|
||||
import { OnboardingProvider } from "@app/contexts/OnboardingContext";
|
||||
import { TourOrchestrationProvider } from "@app/contexts/TourOrchestrationContext";
|
||||
import { AdminTourOrchestrationProvider } from "@app/contexts/AdminTourOrchestrationContext";
|
||||
import { PageEditorProvider } from "@app/contexts/PageEditorContext";
|
||||
import { BannerProvider } from "@app/contexts/BannerContext";
|
||||
import ErrorBoundary from "@app/components/shared/ErrorBoundary";
|
||||
import { useScarfTracking } from "@app/hooks/useScarfTracking";
|
||||
import { useAppInitialization } from "@app/hooks/useAppInitialization";
|
||||
|
||||
// Component to initialize scarf tracking (must be inside AppConfigProvider)
|
||||
function ScarfTrackingInitializer() {
|
||||
@@ -27,68 +23,43 @@ function ScarfTrackingInitializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Component to run app-level initialization (must be inside AppProviders for context access)
|
||||
function AppInitializer() {
|
||||
useAppInitialization();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Avoid requirement to have props which are required in app providers anyway
|
||||
type AppConfigProviderOverrides = Omit<AppConfigProviderProps, 'children' | 'retryOptions'>;
|
||||
|
||||
export interface AppProvidersProps {
|
||||
children: ReactNode;
|
||||
appConfigRetryOptions?: AppConfigRetryOptions;
|
||||
appConfigProviderProps?: Partial<AppConfigProviderOverrides>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core application providers
|
||||
* Contains all providers needed for the core
|
||||
*/
|
||||
export function AppProviders({ children, appConfigRetryOptions, appConfigProviderProps }: AppProvidersProps) {
|
||||
export function AppProviders({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<PreferencesProvider>
|
||||
<RainbowThemeProvider>
|
||||
<ErrorBoundary>
|
||||
<BannerProvider>
|
||||
<OnboardingProvider>
|
||||
<AppConfigProvider
|
||||
retryOptions={appConfigRetryOptions}
|
||||
{...appConfigProviderProps}
|
||||
>
|
||||
<ScarfTrackingInitializer />
|
||||
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
||||
<AppInitializer />
|
||||
<ToolRegistryProvider>
|
||||
<NavigationProvider>
|
||||
<FilesModalProvider>
|
||||
<ToolWorkflowProvider>
|
||||
<HotkeyProvider>
|
||||
<SidebarProvider>
|
||||
<ViewerProvider>
|
||||
<PageEditorProvider>
|
||||
<SignatureProvider>
|
||||
<RightRailProvider>
|
||||
<TourOrchestrationProvider>
|
||||
<AdminTourOrchestrationProvider>
|
||||
{children}
|
||||
</AdminTourOrchestrationProvider>
|
||||
</TourOrchestrationProvider>
|
||||
</RightRailProvider>
|
||||
</SignatureProvider>
|
||||
</PageEditorProvider>
|
||||
</ViewerProvider>
|
||||
</SidebarProvider>
|
||||
</HotkeyProvider>
|
||||
</ToolWorkflowProvider>
|
||||
</FilesModalProvider>
|
||||
</NavigationProvider>
|
||||
</ToolRegistryProvider>
|
||||
</FileContextProvider>
|
||||
</AppConfigProvider>
|
||||
</OnboardingProvider>
|
||||
</BannerProvider>
|
||||
<OnboardingProvider>
|
||||
<AppConfigProvider>
|
||||
<ScarfTrackingInitializer />
|
||||
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
||||
<ToolRegistryProvider>
|
||||
<NavigationProvider>
|
||||
<FilesModalProvider>
|
||||
<ToolWorkflowProvider>
|
||||
<HotkeyProvider>
|
||||
<SidebarProvider>
|
||||
<ViewerProvider>
|
||||
<SignatureProvider>
|
||||
<RightRailProvider>
|
||||
<TourOrchestrationProvider>
|
||||
{children}
|
||||
</TourOrchestrationProvider>
|
||||
</RightRailProvider>
|
||||
</SignatureProvider>
|
||||
</ViewerProvider>
|
||||
</SidebarProvider>
|
||||
</HotkeyProvider>
|
||||
</ToolWorkflowProvider>
|
||||
</FilesModalProvider>
|
||||
</NavigationProvider>
|
||||
</ToolRegistryProvider>
|
||||
</FileContextProvider>
|
||||
</AppConfigProvider>
|
||||
</OnboardingProvider>
|
||||
</ErrorBoundary>
|
||||
</RainbowThemeProvider>
|
||||
</PreferencesProvider>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Paper, Button, Modal, Stack, Text, Popover, ColorPicker as MantineColor
|
||||
import { ColorSwatchButton } from '@app/components/annotation/shared/ColorPicker';
|
||||
import PenSizeSelector from '@app/components/tools/sign/PenSizeSelector';
|
||||
import SignaturePad from 'signature_pad';
|
||||
import { PrivateContent } from '@app/components/shared/PrivateContent';
|
||||
|
||||
interface DrawingCanvasProps {
|
||||
selectedColor: string;
|
||||
@@ -178,21 +177,19 @@ export const DrawingCanvas: React.FC<DrawingCanvasProps> = ({
|
||||
<Paper withBorder p="md">
|
||||
<Stack gap="sm">
|
||||
<Text fw={500}>Draw your signature</Text>
|
||||
<PrivateContent>
|
||||
<canvas
|
||||
ref={previewCanvasRef}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
cursor: disabled ? 'default' : 'pointer',
|
||||
backgroundColor: '#ffffff',
|
||||
width: '100%',
|
||||
}}
|
||||
onClick={disabled ? undefined : openModal}
|
||||
/>
|
||||
</PrivateContent>
|
||||
<canvas
|
||||
ref={previewCanvasRef}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
cursor: disabled ? 'default' : 'pointer',
|
||||
backgroundColor: '#ffffff',
|
||||
width: '100%',
|
||||
}}
|
||||
onClick={disabled ? undefined : openModal}
|
||||
/>
|
||||
<Text size="sm" c="dimmed" ta="center">
|
||||
Click to open drawing canvas
|
||||
</Text>
|
||||
@@ -249,25 +246,23 @@ export const DrawingCanvas: React.FC<DrawingCanvasProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PrivateContent>
|
||||
<canvas
|
||||
ref={(el) => {
|
||||
modalCanvasRef.current = el;
|
||||
if (el) initPad(el);
|
||||
}}
|
||||
style={{
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
display: 'block',
|
||||
touchAction: 'none',
|
||||
backgroundColor: 'white',
|
||||
width: '100%',
|
||||
maxWidth: '800px',
|
||||
height: '400px',
|
||||
cursor: 'crosshair',
|
||||
}}
|
||||
/>
|
||||
</PrivateContent>
|
||||
<canvas
|
||||
ref={(el) => {
|
||||
modalCanvasRef.current = el;
|
||||
if (el) initPad(el);
|
||||
}}
|
||||
style={{
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
display: 'block',
|
||||
touchAction: 'none',
|
||||
backgroundColor: 'white',
|
||||
width: '100%',
|
||||
maxWidth: '800px',
|
||||
height: '400px',
|
||||
cursor: 'crosshair',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Button variant="subtle" color="red" onClick={clear}>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { FileInput, Text, Stack } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PrivateContent } from '@app/components/shared/PrivateContent';
|
||||
|
||||
interface ImageUploaderProps {
|
||||
onImageChange: (file: File | null) => void;
|
||||
@@ -41,15 +40,13 @@ export const ImageUploader: React.FC<ImageUploaderProps> = ({
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<PrivateContent>
|
||||
<FileInput
|
||||
label={label || t('sign.image.label', 'Upload signature image')}
|
||||
placeholder={placeholder || t('sign.image.placeholder', 'Select image file')}
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</PrivateContent>
|
||||
<FileInput
|
||||
label={label || t('sign.image.label', 'Upload signature image')}
|
||||
placeholder={placeholder || t('sign.image.placeholder', 'Select image file')}
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Text size="sm" c="dimmed">
|
||||
{hint || t('sign.image.hint', 'Upload an image of your signature')}
|
||||
</Text>
|
||||
|
||||
@@ -14,7 +14,6 @@ import { FileId, StirlingFile } from '@app/types/fileContext';
|
||||
import { alert } from '@app/components/toast';
|
||||
import { downloadBlob } from '@app/utils/downloadUtils';
|
||||
import { useFileEditorRightRailButtons } from '@app/components/fileEditor/fileEditorRightRailButtons';
|
||||
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
||||
|
||||
|
||||
interface FileEditorProps {
|
||||
@@ -66,15 +65,6 @@ const FileEditor = ({
|
||||
}, []);
|
||||
const [selectionMode, setSelectionMode] = useState(toolMode);
|
||||
|
||||
// Current tool (for enforcing maxFiles limits)
|
||||
const { selectedTool } = useToolWorkflow();
|
||||
|
||||
// Compute effective max allowed files based on the active tool and mode
|
||||
const maxAllowed = useMemo<number>(() => {
|
||||
const rawMax = selectedTool?.maxFiles;
|
||||
return (!toolMode || rawMax == null || rawMax < 0) ? Infinity : rawMax;
|
||||
}, [selectedTool?.maxFiles, toolMode]);
|
||||
|
||||
// Enable selection mode automatically in tool mode
|
||||
useEffect(() => {
|
||||
if (toolMode) {
|
||||
@@ -93,10 +83,7 @@ const FileEditor = ({
|
||||
const localSelectedIds = contextSelectedIds;
|
||||
|
||||
const handleSelectAllFiles = useCallback(() => {
|
||||
// Respect maxAllowed: if limited, select the last N files
|
||||
const allIds = state.files.ids;
|
||||
const idsToSelect = Number.isFinite(maxAllowed) ? allIds.slice(-maxAllowed) : allIds;
|
||||
setSelectedFiles(idsToSelect);
|
||||
setSelectedFiles(state.files.ids);
|
||||
try {
|
||||
clearAllFileErrors();
|
||||
} catch (error) {
|
||||
@@ -104,7 +91,7 @@ const FileEditor = ({
|
||||
console.warn('Failed to clear file errors on select all:', error);
|
||||
}
|
||||
}
|
||||
}, [state.files.ids, setSelectedFiles, clearAllFileErrors, maxAllowed]);
|
||||
}, [state.files.ids, setSelectedFiles, clearAllFileErrors]);
|
||||
|
||||
const handleDeselectAllFiles = useCallback(() => {
|
||||
setSelectedFiles([]);
|
||||
@@ -144,13 +131,6 @@ const FileEditor = ({
|
||||
// - HTML ZIPs stay intact
|
||||
// - Non-ZIP files pass through unchanged
|
||||
await addFiles(uploadedFiles, { selectFiles: true });
|
||||
// After auto-selection, enforce maxAllowed if needed
|
||||
if (Number.isFinite(maxAllowed)) {
|
||||
const nowSelectedIds = selectors.getSelectedStirlingFileStubs().map(r => r.id);
|
||||
if (nowSelectedIds.length > maxAllowed) {
|
||||
setSelectedFiles(nowSelectedIds.slice(-maxAllowed));
|
||||
}
|
||||
}
|
||||
showStatus(`Added ${uploadedFiles.length} file(s)`, 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -158,7 +138,7 @@ const FileEditor = ({
|
||||
showError(errorMessage);
|
||||
console.error('File processing error:', err);
|
||||
}
|
||||
}, [addFiles, showStatus, showError, selectors, maxAllowed, setSelectedFiles]);
|
||||
}, [addFiles, showStatus, showError]);
|
||||
|
||||
const toggleFile = useCallback((fileId: FileId) => {
|
||||
const currentSelectedIds = contextSelectedIdsRef.current;
|
||||
@@ -176,33 +156,24 @@ const FileEditor = ({
|
||||
newSelection = currentSelectedIds.filter(id => id !== contextFileId);
|
||||
} else {
|
||||
// Add file to selection
|
||||
// Determine max files allowed from the active tool (negative or undefined means unlimited)
|
||||
const rawMax = selectedTool?.maxFiles;
|
||||
const maxAllowed = (!toolMode || rawMax == null || rawMax < 0) ? Infinity : rawMax;
|
||||
// In tool mode, typically allow multiple files unless specified otherwise
|
||||
const maxAllowed = toolMode ? 10 : Infinity; // Default max for tools
|
||||
|
||||
if (maxAllowed === 1) {
|
||||
// Only one file allowed -> replace selection with the new file
|
||||
newSelection = [contextFileId];
|
||||
} else {
|
||||
// If at capacity, drop the oldest selected and append the new one
|
||||
if (Number.isFinite(maxAllowed) && currentSelectedIds.length >= maxAllowed) {
|
||||
newSelection = [...currentSelectedIds.slice(1), contextFileId];
|
||||
} else {
|
||||
newSelection = [...currentSelectedIds, contextFileId];
|
||||
// Check if we've hit the selection limit
|
||||
if (maxAllowed > 1 && currentSelectedIds.length >= maxAllowed) {
|
||||
showStatus(`Maximum ${maxAllowed} files can be selected`, 'warning');
|
||||
return;
|
||||
}
|
||||
newSelection = [...currentSelectedIds, contextFileId];
|
||||
}
|
||||
}
|
||||
|
||||
// Update context (this automatically updates tool selection since they use the same action)
|
||||
setSelectedFiles(newSelection);
|
||||
}, [setSelectedFiles, toolMode, _setStatus, activeStirlingFileStubs, selectedTool?.maxFiles]);
|
||||
|
||||
// Enforce maxAllowed when tool changes or when an external action sets too many selected files
|
||||
useEffect(() => {
|
||||
if (Number.isFinite(maxAllowed) && selectedFileIds.length > maxAllowed) {
|
||||
setSelectedFiles(selectedFileIds.slice(-maxAllowed));
|
||||
}
|
||||
}, [maxAllowed, selectedFileIds, setSelectedFiles]);
|
||||
}, [setSelectedFiles, toolMode, _setStatus, activeStirlingFileStubs]);
|
||||
|
||||
|
||||
// File reordering handler for drag and drop
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useCallback, useRef, useMemo } from 'react';
|
||||
import { Text, ActionIcon, CheckboxIndicator, Tooltip, Modal, Button, Group, Stack } from '@mantine/core';
|
||||
import { useIsMobile } from '@app/hooks/useIsMobile';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { alert } from '@app/components/toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
|
||||
@@ -21,7 +21,6 @@ import { FileId } from '@app/types/file';
|
||||
import { formatFileSize } from '@app/utils/fileUtils';
|
||||
import ToolChain from '@app/components/shared/ToolChain';
|
||||
import HoverActionMenu, { HoverAction } from '@app/components/shared/HoverActionMenu';
|
||||
import { PrivateContent } from '@app/components/shared/PrivateContent';
|
||||
|
||||
|
||||
|
||||
@@ -64,7 +63,7 @@ const FileEditorThumbnail = ({
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const dragElementRef = useRef<HTMLDivElement | null>(null);
|
||||
const [showHoverMenu, setShowHoverMenu] = useState(false);
|
||||
const isMobile = useIsMobile();
|
||||
const isMobile = useMediaQuery('(max-width: 1024px)');
|
||||
const [showCloseModal, setShowCloseModal] = useState(false);
|
||||
|
||||
// Resolve the actual File object for pin/unpin operations
|
||||
@@ -93,13 +92,6 @@ const FileEditorThumbnail = ({
|
||||
return (m?.[1] || '').toUpperCase();
|
||||
}, [file.name]);
|
||||
|
||||
const extLower = useMemo(() => {
|
||||
const m = /\.([a-z0-9]+)$/i.exec(file.name ?? '');
|
||||
return (m?.[1] || '').toLowerCase();
|
||||
}, [file.name]);
|
||||
|
||||
const isCBZ = extLower === 'cbz';
|
||||
|
||||
const pageLabel = useMemo(
|
||||
() =>
|
||||
pageCount > 0
|
||||
@@ -213,7 +205,7 @@ const FileEditorThumbnail = ({
|
||||
alert({ alertType: 'success', title: `Unzipping ${file.name}`, expandable: false, durationMs: 2500 });
|
||||
}
|
||||
},
|
||||
hidden: !isZipFile || !onUnzipFile || isCBZ,
|
||||
hidden: !isZipFile || !onUnzipFile,
|
||||
},
|
||||
{
|
||||
id: 'close',
|
||||
@@ -336,8 +328,8 @@ const FileEditorThumbnail = ({
|
||||
marginTop: '0.5rem',
|
||||
marginBottom: '0.5rem',
|
||||
}}>
|
||||
<Text size="lg" fw={700} className={styles.title} lineClamp={2}>
|
||||
<PrivateContent>{file.name}</PrivateContent>
|
||||
<Text size="lg" fw={700} className={`${styles.title} ph-no-capture `} lineClamp={2}>
|
||||
{file.name}
|
||||
</Text>
|
||||
<Text
|
||||
size="sm"
|
||||
@@ -361,20 +353,20 @@ const FileEditorThumbnail = ({
|
||||
>
|
||||
<div className={styles.previewPaper}>
|
||||
{file.thumbnailUrl && (
|
||||
<PrivateContent>
|
||||
<img
|
||||
src={file.thumbnailUrl}
|
||||
alt={file.name}
|
||||
draggable={false}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onError={(e) => {
|
||||
const img = e.currentTarget;
|
||||
img.style.display = 'none';
|
||||
img.parentElement?.setAttribute('data-thumb-missing', 'true');
|
||||
}}
|
||||
style={{
|
||||
maxWidth: '80%',
|
||||
<img
|
||||
className="ph-no-capture"
|
||||
src={file.thumbnailUrl}
|
||||
alt={file.name}
|
||||
draggable={false}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onError={(e) => {
|
||||
const img = e.currentTarget;
|
||||
img.style.display = 'none';
|
||||
img.parentElement?.setAttribute('data-thumb-missing', 'true');
|
||||
}}
|
||||
style={{
|
||||
maxWidth: '80%',
|
||||
maxHeight: '80%',
|
||||
objectFit: 'contain',
|
||||
borderRadius: 0,
|
||||
@@ -386,7 +378,6 @@ const FileEditorThumbnail = ({
|
||||
alignSelf: 'start'
|
||||
}}
|
||||
/>
|
||||
</PrivateContent>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getFileSize } from '@app/utils/fileUtils';
|
||||
import { StirlingFileStub } from '@app/types/fileContext';
|
||||
import { PrivateContent } from '@app/components/shared/PrivateContent';
|
||||
|
||||
interface CompactFileDetailsProps {
|
||||
currentFile: StirlingFileStub | null;
|
||||
@@ -42,19 +41,18 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
|
||||
{/* Small preview */}
|
||||
<Box style={{ width: '7.5rem', height: '9.375rem', flexShrink: 0, position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{currentFile && thumbnail ? (
|
||||
<PrivateContent>
|
||||
<img
|
||||
src={thumbnail}
|
||||
alt={currentFile.name}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
objectFit: 'contain',
|
||||
borderRadius: '0.25rem',
|
||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
/>
|
||||
</PrivateContent>
|
||||
<img
|
||||
className='ph-no-capture'
|
||||
src={thumbnail}
|
||||
alt={currentFile.name}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
objectFit: 'contain',
|
||||
borderRadius: '0.25rem',
|
||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
/>
|
||||
) : currentFile ? (
|
||||
<Center style={{
|
||||
width: '100%',
|
||||
@@ -69,8 +67,8 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
|
||||
|
||||
{/* File info */}
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text size="sm" fw={500} truncate>
|
||||
<PrivateContent>{currentFile ? currentFile.name : 'No file selected'}</PrivateContent>
|
||||
<Text className='ph-no-capture' size="sm" fw={500} truncate>
|
||||
{currentFile ? currentFile.name : 'No file selected'}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{currentFile ? getFileSize(currentFile) : ''}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { detectFileExtension, getFileSize } from '@app/utils/fileUtils';
|
||||
import { StirlingFileStub } from '@app/types/fileContext';
|
||||
import ToolChain from '@app/components/shared/ToolChain';
|
||||
import { PrivateContent } from '@app/components/shared/PrivateContent';
|
||||
|
||||
interface FileInfoCardProps {
|
||||
currentFile: StirlingFileStub | null;
|
||||
@@ -27,9 +26,7 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
|
||||
<ScrollArea style={{ flex: 1 }} p="md">
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" py="xs">
|
||||
<Text size="sm" c="dimmed">
|
||||
<PrivateContent>{t('fileManager.fileName', 'Name')}</PrivateContent>
|
||||
</Text>
|
||||
<Text className='ph-no-capture' size="sm" c="dimmed">{t('fileManager.fileName', 'Name')}</Text>
|
||||
<Text size="sm" fw={500} style={{ maxWidth: '60%', textAlign: 'right' }} truncate>
|
||||
{currentFile ? currentFile.name : ''}
|
||||
</Text>
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useFileManagerContext } from '@app/contexts/FileManagerContext';
|
||||
import { zipFileService } from '@app/services/zipFileService';
|
||||
import ToolChain from '@app/components/shared/ToolChain';
|
||||
import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex';
|
||||
import { PrivateContent } from '@app/components/shared/PrivateContent';
|
||||
|
||||
interface FileListItemProps {
|
||||
file: StirlingFileStub;
|
||||
@@ -100,9 +99,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
||||
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Group gap="xs" align="center">
|
||||
<Text size="sm" fw={500} truncate style={{ flex: 1 }}>
|
||||
<PrivateContent>{file.name}</PrivateContent>
|
||||
</Text>
|
||||
<Text size="sm" fw={500} className='ph-no-capture' truncate style={{ flex: 1 }}>{file.name}</Text>
|
||||
<Badge size="xs" variant="light" color={"blue"}>
|
||||
v{currentVersion}
|
||||
</Badge>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
.workbenchScrollable {
|
||||
.workbench-scrollable {
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.workbenchScrollable::-webkit-scrollbar {
|
||||
.workbench-scrollable::-webkit-scrollbar {
|
||||
width: 0.375rem;
|
||||
}
|
||||
|
||||
.workbenchScrollable::-webkit-scrollbar-track {
|
||||
.workbench-scrollable::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.workbenchScrollable::-webkit-scrollbar-thumb {
|
||||
.workbench-scrollable::-webkit-scrollbar-thumb {
|
||||
background-color: var(--mantine-color-gray-4);
|
||||
border-radius: 0.1875rem;
|
||||
}
|
||||
|
||||
.workbenchScrollable::-webkit-scrollbar-thumb:hover {
|
||||
.workbench-scrollable::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--mantine-color-gray-5);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mantine/core';
|
||||
import { useRainbowThemeContext } from '@app/components/shared/RainbowThemeProvider';
|
||||
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
||||
@@ -80,7 +81,6 @@ export default function Workbench() {
|
||||
|
||||
switch (currentView) {
|
||||
case "fileEditor":
|
||||
|
||||
return (
|
||||
<FileEditor
|
||||
toolMode={!!selectedToolId}
|
||||
@@ -98,7 +98,6 @@ export default function Workbench() {
|
||||
);
|
||||
|
||||
case "viewer":
|
||||
|
||||
return (
|
||||
<Viewer
|
||||
sidebarsVisible={sidebarsVisible}
|
||||
@@ -111,7 +110,6 @@ export default function Workbench() {
|
||||
);
|
||||
|
||||
case "pageEditor":
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageEditor
|
||||
@@ -145,8 +143,6 @@ export default function Workbench() {
|
||||
default:
|
||||
if (!isBaseWorkbench(currentView)) {
|
||||
const customView = customWorkbenchViews.find((view) => view.workbenchId === currentView && view.data != null);
|
||||
|
||||
|
||||
if (customView) {
|
||||
const CustomComponent = customView.component;
|
||||
return <CustomComponent data={customView.data} />;
|
||||
@@ -158,7 +154,7 @@ export default function Workbench() {
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="flex-1 h-full min-w-0 relative flex flex-col"
|
||||
className="flex-1 h-full min-w-80 relative flex flex-col"
|
||||
data-tour="workbench"
|
||||
style={
|
||||
isRainbowMode
|
||||
@@ -189,13 +185,14 @@ export default function Workbench() {
|
||||
className={`flex-1 min-h-0 relative z-10 ${styles.workbenchScrollable}`}
|
||||
style={{
|
||||
transition: 'opacity 0.15s ease-in-out',
|
||||
paddingTop: currentView === 'viewer' ? '0' : (activeFiles.length > 0 ? '3.5rem' : '0'),
|
||||
}}
|
||||
>
|
||||
{renderMainContent()}
|
||||
</Box>
|
||||
|
||||
<Footer
|
||||
analyticsEnabled={config?.enableAnalytics === true}
|
||||
analyticsEnabled={config?.enableAnalytics ?? undefined}
|
||||
termsAndConditions={config?.termsAndConditions}
|
||||
privacyPolicy={config?.privacyPolicy}
|
||||
cookiePolicy={config?.cookiePolicy}
|
||||
|
||||
@@ -6,28 +6,3 @@
|
||||
ry: 8px;
|
||||
filter: drop-shadow(0 0 10px var(--mantine-primary-color-filled));
|
||||
}
|
||||
|
||||
/* Add glowing border to navigation items during admin tour */
|
||||
.modal-nav-item.tour-nav-glow {
|
||||
position: relative;
|
||||
box-shadow:
|
||||
0 0 0 2px var(--mantine-primary-color-filled),
|
||||
0 0 15px var(--mantine-primary-color-filled),
|
||||
inset 0 0 15px rgba(59, 130, 246, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
box-shadow:
|
||||
0 0 0 3px var(--mantine-primary-color-filled),
|
||||
0 0 20px var(--mantine-primary-color-filled),
|
||||
inset 0 0 20px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
0 0 0 3px var(--mantine-primary-color-filled),
|
||||
0 0 30px var(--mantine-primary-color-filled),
|
||||
inset 0 0 30px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user