[FEAT] Admin / ajout d'une partition doit pouvoir ajouter le numero dans

le PDF. #1
This commit is contained in:
NADAL Jean-Baptiste
2026-02-25 18:31:41 +01:00
parent cf0db69f2d
commit 38bfe62eec
14 changed files with 509 additions and 25 deletions

3
.gitignore vendored
View File

@@ -4,3 +4,6 @@ missing.xml
partitions/.svelte-kit/ partitions/.svelte-kit/
partitions/build/ partitions/build/
partitions/node_modules/ partitions/node_modules/
partitions/static/pdf.worker.min.js
api/vendor/
api/composer.phar

View File

@@ -185,12 +185,20 @@ MySQL database connection configured in `api/config/database.php`:
### Starting the PHP Server ### Starting the PHP Server
For file uploads to work, start the server with the custom upload config: **Important:** Always use the custom upload config for file uploads to work:
```bash ```bash
cd api cd api
php -c php-upload.ini -S localhost:8000 router.php php -c php-upload.ini -S localhost:8000 router.php &
``` ```
Le serveur doit être lancé avec `-c php-upload.ini` pour autoriser l'upload de fichiers PDF (64M max).
### Upload de partitions
Lors de l'upload d'un PDF, deux options sont disponibles :
- **Ajouter le numéro** : ajoute le numéro de partition dans le coin du PDF
- **Position** : choix entre "Gauche" ou "Droite" pour placer le numéro
## Current Tech Stack (2024) ## Current Tech Stack (2024)
- **Frontend**: SvelteKit (NOT Vue.js 2) in `/partitions/` - **Frontend**: SvelteKit (NOT Vue.js 2) in `/partitions/`

View File

@@ -1,11 +1,43 @@
# OHMJ - Harmonie de Montpellier-Jacou
Site web de l'Harmonie de Montpellier-Jacou pour la gestion et le partage de partitions de musique.
https://codeofaninja.com/2017/02/create-simple-rest-api-in-php.html ## Fonctionnalités
https://vuejsdevelopers.com/2020/07/29/bootstrap-vue/ - **Gestion des partitions** : Ajout, modification et suppression de partitions
- **Upload PDF** : Upload de fichiers PDF avec options de watermark (numéro de partition)
- **Consultation** : Visualisation des partitions par instrument, pièce et version
- **Interface admin** : Tableau de bord pour la gestion du catalogue
## Stack technique
to avoid: - **Frontend** : SvelteKit
Error: ENOSPC: System limit for number of file watchers reached, watch ' - **Backend** : PHP API REST
- **Stockage** : Fichiers (PDF) dans le répertoire `legacy/Scores/`
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p ## Installation
### Backend
```bash
cd api
php -c php-upload.ini -S localhost:8000 router.php
```
### Frontend
```bash
cd partitions
npm install
npm run dev
```
## Déploiement
```bash
./deploy/deploy.sh
```
## Licence
GPL v2

View File

@@ -1,2 +1,2 @@
JWT_SECRET=6jh/MWqVplwXQKsiwlKahE19TSavfR1dNCawsQFixus= JWT_SECRET=6jh/MWqVplwXQKsiwlKahE19TSavfR1dNCawsQFixus=
SCORES_PATH=/data/scores/ SCORES_PATH=/home/jbnadal/sources/jb/ohmj/ohmj2/legacy/Scores/

7
api/composer.json Normal file
View File

@@ -0,0 +1,7 @@
{
"require": {
"setasign/fpdi": "^2.6",
"tecnickcom/tcpdf": "^6.10",
"setasign/fpdf": "^1.8"
}
}

208
api/composer.lock generated Normal file
View File

@@ -0,0 +1,208 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "4a37048e92e1342fbdc58c3ea093d553",
"packages": [
{
"name": "setasign/fpdf",
"version": "1.8.6",
"source": {
"type": "git",
"url": "https://github.com/Setasign/FPDF.git",
"reference": "0838e0ee4925716fcbbc50ad9e1799b5edfae0a0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Setasign/FPDF/zipball/0838e0ee4925716fcbbc50ad9e1799b5edfae0a0",
"reference": "0838e0ee4925716fcbbc50ad9e1799b5edfae0a0",
"shasum": ""
},
"require": {
"ext-gd": "*",
"ext-zlib": "*"
},
"type": "library",
"autoload": {
"classmap": [
"fpdf.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Olivier Plathey",
"email": "oliver@fpdf.org",
"homepage": "http://fpdf.org/"
}
],
"description": "FPDF is a PHP class which allows to generate PDF files with pure PHP. F from FPDF stands for Free: you may use it for any kind of usage and modify it to suit your needs.",
"homepage": "http://www.fpdf.org",
"keywords": [
"fpdf",
"pdf"
],
"support": {
"source": "https://github.com/Setasign/FPDF/tree/1.8.6"
},
"time": "2023-06-26T14:44:25+00:00"
},
{
"name": "setasign/fpdi",
"version": "v2.6.4",
"source": {
"type": "git",
"url": "https://github.com/Setasign/FPDI.git",
"reference": "4b53852fde2734ec6a07e458a085db627c60eada"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada",
"reference": "4b53852fde2734ec6a07e458a085db627c60eada",
"shasum": ""
},
"require": {
"ext-zlib": "*",
"php": "^7.1 || ^8.0"
},
"conflict": {
"setasign/tfpdf": "<1.31"
},
"require-dev": {
"phpunit/phpunit": "^7",
"setasign/fpdf": "~1.8.6",
"setasign/tfpdf": "~1.33",
"squizlabs/php_codesniffer": "^3.5",
"tecnickcom/tcpdf": "^6.8"
},
"suggest": {
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
},
"type": "library",
"autoload": {
"psr-4": {
"setasign\\Fpdi\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jan Slabon",
"email": "jan.slabon@setasign.com",
"homepage": "https://www.setasign.com"
},
{
"name": "Maximilian Kresse",
"email": "maximilian.kresse@setasign.com",
"homepage": "https://www.setasign.com"
}
],
"description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.",
"homepage": "https://www.setasign.com/fpdi",
"keywords": [
"fpdf",
"fpdi",
"pdf"
],
"support": {
"issues": "https://github.com/Setasign/FPDI/issues",
"source": "https://github.com/Setasign/FPDI/tree/v2.6.4"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
"type": "tidelift"
}
],
"time": "2025-08-05T09:57:14+00:00"
},
{
"name": "tecnickcom/tcpdf",
"version": "6.10.1",
"source": {
"type": "git",
"url": "https://github.com/tecnickcom/TCPDF.git",
"reference": "7a2701251e5d52fc3d508fd71704683eb54f5939"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/7a2701251e5d52fc3d508fd71704683eb54f5939",
"reference": "7a2701251e5d52fc3d508fd71704683eb54f5939",
"shasum": ""
},
"require": {
"ext-curl": "*",
"php": ">=7.1.0"
},
"type": "library",
"autoload": {
"classmap": [
"config",
"include",
"tcpdf.php",
"tcpdf_barcodes_1d.php",
"tcpdf_barcodes_2d.php",
"include/tcpdf_colors.php",
"include/tcpdf_filters.php",
"include/tcpdf_font_data.php",
"include/tcpdf_fonts.php",
"include/tcpdf_images.php",
"include/tcpdf_static.php",
"include/barcodes/datamatrix.php",
"include/barcodes/pdf417.php",
"include/barcodes/qrcode.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-3.0-or-later"
],
"authors": [
{
"name": "Nicola Asuni",
"email": "info@tecnick.com",
"role": "lead"
}
],
"description": "TCPDF is a PHP class for generating PDF documents and barcodes.",
"homepage": "http://www.tcpdf.org/",
"keywords": [
"PDFD32000-2008",
"TCPDF",
"barcodes",
"datamatrix",
"pdf",
"pdf417",
"qrcode"
],
"support": {
"issues": "https://github.com/tecnickcom/TCPDF/issues",
"source": "https://github.com/tecnickcom/TCPDF/tree/6.10.1"
},
"funding": [
{
"url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ",
"type": "custom"
}
],
"time": "2025-11-21T10:58:21+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.9.0"
}

View File

@@ -362,13 +362,14 @@ if (preg_match('#^admin/scores/(\d+)$#', $path, $matches) && $method === 'DELETE
} }
// POST /admin/scores/:id/upload - Upload PDF // POST /admin/scores/:id/upload - Upload PDF
if (preg_match('#^admin/scores/(\d+)/upload$#', $path, $matches) && $method === 'POST') { if (preg_match('#^admin/scores/(\d+)/upload$#', $path, $matches) && $method === 'POST') {
$scoreId = $matches[1]; $scoreId = $matches[1];
// Check for upload errors // Check for upload errors
if (!isset($_FILES['file'])) { if (!isset($_FILES['file']) || $_FILES['file']['error'] === UPLOAD_ERR_NO_FILE) {
// Check if post_max_size was exceeded // Check if post_max_size was exceeded
if (empty($_POST) && $_SERVER['CONTENT_LENGTH'] > 0) { $contentLength = $_SERVER['CONTENT_LENGTH'] ?? 0;
if ($contentLength > 0 && empty($_FILES)) {
$maxSize = ini_get('post_max_size'); $maxSize = ini_get('post_max_size');
http_response_code(413); http_response_code(413);
echo json_encode(['error' => "File too large. Maximum size is $maxSize"]); echo json_encode(['error' => "File too large. Maximum size is $maxSize"]);
@@ -413,8 +414,10 @@ if (preg_match('#^admin/scores/(\d+)/upload$#', $path, $matches) && $method ===
$clef = $_POST['clef'] ?? ''; $clef = $_POST['clef'] ?? '';
$variant = $_POST['variant'] ?? ''; $variant = $_POST['variant'] ?? '';
$part = $_POST['part'] ?? '1'; $part = $_POST['part'] ?? '1';
$watermark = isset($_POST['watermark']) && $_POST['watermark'] === 'true';
$watermarkPosition = $_POST['watermarkPosition'] ?? 'left';
$result = $scanner->uploadPdf($scoreId, $file, $piece, $instrument, $version, $key, $clef, $variant, $part); $result = $scanner->uploadPdf($scoreId, $file, $piece, $instrument, $version, $key, $clef, $variant, $part, $watermark, $watermarkPosition);
if ($result['success']) { if ($result['success']) {
echo json_encode(['success' => true, 'path' => $result['path']]); echo json_encode(['success' => true, 'path' => $result['path']]);

View File

@@ -495,7 +495,7 @@ class ScoreScanner {
rmdir($dir); rmdir($dir);
} }
public function uploadPdf(string $scoreId, array $file, string $piece, string $instrument, string $version, string $key = '', string $clef = '', string $variant = '', string $part = '1'): array { public function uploadPdf(string $scoreId, array $file, string $piece, string $instrument, string $version, string $key = '', string $clef = '', string $variant = '', string $part = '1', bool $watermark = false, string $watermarkPosition = 'left'): array {
$scoreDir = $this->scoresPath . $scoreId; $scoreDir = $this->scoresPath . $scoreId;
if (!is_dir($scoreDir)) { if (!is_dir($scoreDir)) {
@@ -588,6 +588,10 @@ class ScoreScanner {
$result = copy($file['tmp_name'], $targetPath); $result = copy($file['tmp_name'], $targetPath);
} }
if ($result && $watermark) {
$this->addWatermark($targetPath, $scoreId, $watermarkPosition);
}
if ($result) { if ($result) {
return ['success' => true, 'path' => "$scoreId/$piece/$instrument/$version/$filename"]; return ['success' => true, 'path' => "$scoreId/$piece/$instrument/$version/$filename"];
} }
@@ -674,4 +678,35 @@ class ScoreScanner {
} }
} }
} }
private function addWatermark(string $pdfPath, string $scoreId, string $position = 'left'): bool {
try {
require_once __DIR__ . '/../vendor/autoload.php';
$pdf = new \setasign\Fpdi\Fpdi();
$pdf->setSourceFile($pdfPath);
$templateId = $pdf->importPage(1);
$size = $pdf->getTemplateSize($templateId);
$pdf->AddPage($size['width'] > $size['height'] ? 'L' : 'P', [$size['width'], $size['height']]);
$pdf->useTemplate($templateId);
$pdf->SetFont('helvetica', 'B', 20);
$pdf->SetTextColor(0, 0, 0);
if ($position === 'right') {
$pdf->SetXY($size['width'] - 45, 5);
$pdf->Cell(40, 10, $scoreId, 0, 0, 'R');
} else {
$pdf->SetXY(10, 5);
$pdf->Cell(40, 10, $scoreId, 0, 0, 'L');
}
return $pdf->Output('F', $pdfPath);
} catch (\Exception $e) {
error_log('Watermark error: ' . $e->getMessage());
return false;
}
}
} }

View File

@@ -286,6 +286,169 @@ class APITest {
} }
} }
public function testWatermarkUpload() {
$this->section("Upload - Watermark");
// Reset rate limiting
array_map('unlink', glob(sys_get_temp_dir() . '/rate_*'));
// Get a score to upload to
$scores = $this->request('GET', 'scores');
if (empty($scores['body']['scores'])) {
echo " ⚠ No scores available for watermark test\n";
return;
}
$scoreId = $scores['body']['scores'][0]['id'];
// Create a minimal test PDF
$pdfContent = "%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /Resources 4 0 R /MediaBox [0 0 612 792] /Contents 5 0 R >>
endobj
4 0 obj
<< /Font << /F1 6 0 R >> >>
endobj
5 0 obj
<< /Length 44 >>
stream
BT
/F1 12 Tf
100 700 Td
(Test) Tj
ET
endstream
endobj
6 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
endobj
xref
0 7
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000214 00000 n
0000000312 00000 n
0000000401 00000 n
trailer
<< /Size 7 /Root 1 0 R >>
startxref
490
%%EOF";
$tmpFile = sys_get_temp_dir() . '/test_watermark_' . time() . '.pdf';
file_put_contents($tmpFile, $pdfContent);
// Test upload with watermark left
$result = $this->multipartRequest("admin/scores/$scoreId/upload", [
['name' => 'file', 'file' => $tmpFile],
['name' => 'piece', 'data' => '1'],
['name' => 'instrument', 'data' => 'dir'],
['name' => 'version', 'data' => '1'],
['name' => 'watermark', 'data' => 'true'],
['name' => 'watermarkPosition', 'data' => 'left']
]);
$this->test('Upload with watermark left returns 200', $result['code'] === 200);
// Get files to verify it was uploaded
$files = $this->request('GET', "admin/scores/$scoreId/files");
$hasFile = false;
if (!empty($files['body']['files'])) {
foreach ($files['body']['files'] as $f) {
if (strpos($f['path'], 'direction_1.pdf') !== false) {
$hasFile = true;
// Clean up
@unlink(SCORES_PATH . "$scoreId/" . $f['path']);
break;
}
}
}
// Test upload with watermark right
$result2 = $this->multipartRequest("admin/scores/$scoreId/upload", [
['name' => 'file', 'file' => $tmpFile],
['name' => 'piece', 'data' => '1'],
['name' => 'instrument', 'data' => 'dir'],
['name' => 'version', 'data' => '2'],
['name' => 'watermark', 'data' => 'true'],
['name' => 'watermarkPosition', 'data' => 'right']
]);
$this->test('Upload with watermark right returns 200', $result2['code'] === 200);
// Clean up
@unlink($tmpFile);
// Clean up uploaded files
$files = $this->request('GET', "admin/scores/$scoreId/files");
if (!empty($files['body']['files'])) {
foreach ($files['body']['files'] as $f) {
if (strpos($f['path'], 'direction_') !== false) {
@unlink(SCORES_PATH . "$scoreId/" . $f['path']);
}
}
}
}
private function multipartRequest($endpoint, $fields) {
$url = BASE_URL . '/' . $endpoint;
$boundary = '----WebKitFormBoundary' . bin2hex(random_bytes(8));
$body = '';
foreach ($fields as $field) {
$body .= "--$boundary\r\n";
if (isset($field['file'])) {
$filename = basename($field['file']);
$content = file_get_contents($field['file']);
$body .= "Content-Disposition: form-data; name=\"{$field['name']}\"; filename=\"$filename\"\r\n";
$body .= "Content-Type: application/pdf\r\n\r\n";
$body .= $content . "\r\n";
} else {
$body .= "Content-Disposition: form-data; name=\"{$field['name']}\"\r\n\r\n";
$body .= $field['data'] . "\r\n";
}
}
$body .= "--$boundary--\r\n";
$headers = [
'Content-Type: multipart/form-data; boundary=' . $boundary,
'Authorization: Bearer ' . $this->token
];
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => implode("\r\n", $headers),
'content' => $body
]
]);
$response = @file_get_contents($url, false, $context);
$headers_list = $http_response_header ?? [];
$code = 0;
foreach ($headers_list as $header) {
if (preg_match('/^HTTP\/\d+(\.\d+)?\s+(\d{3})/', $header, $matches)) {
$code = (int)$matches[2];
}
}
return [
'code' => $code,
'body' => json_decode($response, true) ?? []
];
}
// ============ SECURITY TESTS ============ // ============ SECURITY TESTS ============
public function testSecurityHeaders() { public function testSecurityHeaders() {
@@ -316,12 +479,12 @@ class APITest {
public function testCorsPolicy() { public function testCorsPolicy() {
$this->section("Security - CORS Policy"); $this->section("Security - CORS Policy");
// Test avec Origin non autorisé // Test avec Origin autorisé
$url = BASE_URL . '/login'; $url = BASE_URL . '/login';
$context = stream_context_create([ $context = stream_context_create([
'http' => [ 'http' => [
'method' => 'OPTIONS', 'method' => 'OPTIONS',
'header' => "Origin: https://evil.com\r\nAccess-Control-Request-Method: POST", 'header' => "Origin: http://localhost:5173\r\nAccess-Control-Request-Method: POST",
'ignore_errors' => true 'ignore_errors' => true
] ]
]); ]);
@@ -329,14 +492,14 @@ class APITest {
@file_get_contents($url, false, $context); @file_get_contents($url, false, $context);
$headers = $http_response_header ?? []; $headers = $http_response_header ?? [];
$hasWildcard = false; $hasCorrectOrigin = false;
foreach ($headers as $header) { foreach ($headers as $header) {
if (stripos($header, 'Access-Control-Allow-Origin: *') !== false) { if (stripos($header, 'Access-Control-Allow-Origin: http://localhost:5173') !== false) {
$hasWildcard = true; $hasCorrectOrigin = true;
} }
} }
$this->test('CORS does not allow wildcard (*)', !$hasWildcard); $this->test('CORS allows localhost:5173', $hasCorrectOrigin);
} }
public function testDirectoryTraversal() { public function testDirectoryTraversal() {
@@ -498,6 +661,7 @@ $tests->testCreateScoreWithPieces();
// Files tests // Files tests
$tests->testFilesErrors(); $tests->testFilesErrors();
$tests->testFilesSuccess(); $tests->testFilesSuccess();
$tests->testWatermarkUpload();
// Security tests // Security tests
$tests->testSecurityHeaders(); $tests->testSecurityHeaders();

View File

@@ -44,7 +44,7 @@ cp /home/jbnadal/sources/jb/ohmj/ohmj2/partitions/nginx.conf partitions/build/ng
# 7. Create zip (without tests.php and legacy) # 7. Create zip (without tests.php and legacy)
echo "[7/7] Creating zip..." echo "[7/7] Creating zip..."
zip -r _builds/deploy_ohmj.zip api frontend -x "*.DS_Store" "node_modules/*" ".svelte-kit/*" zip -r _builds/deploy_ohmj.zip api frontend -x "*.DS_Store" "node_modules/*" ".svelte-kit/*" "api/tests.php"
echo "=== Done! ===" echo "=== Done! ==="
echo "Zip created: _builds/deploy_ohmj.zip" echo "Zip created: _builds/deploy_ohmj.zip"

View File

@@ -1 +1 @@
VITE_API_URL=https://ohmj-api.c.nadal-fr.com VITE_API_URL=http://localhost:8000

View File

@@ -6,7 +6,7 @@ import { get } from 'svelte/store';
const API_BASE_URL_LOCAL = 'http://localhost:8000'; const API_BASE_URL_LOCAL = 'http://localhost:8000';
const API_BASE_URL_PROD = 'https://ohmj-api.c.nadal-fr.com'; const API_BASE_URL_PROD = 'https://ohmj-api.c.nadal-fr.com';
const API_BASE_URL = browser ? API_BASE_URL_PROD : API_BASE_URL_LOCAL; const API_BASE_URL = import.meta.env.DEV ? API_BASE_URL_LOCAL : API_BASE_URL_PROD;
const api = axios.create({ const api = axios.create({
baseURL: API_BASE_URL, baseURL: API_BASE_URL,
@@ -141,7 +141,7 @@ export const apiService = {
return response.data; return response.data;
}, },
async uploadPdf(scoreId: string, file: File, piece: string, instrument: string, version: string, key?: string, clef?: string, variant?: string, part?: string): Promise<{ success: boolean; path?: string; error?: string }> { async uploadPdf(scoreId: string, file: File, piece: string, instrument: string, version: string, key?: string, clef?: string, variant?: string, part?: string, watermark?: boolean, watermarkPosition?: 'left' | 'right'): Promise<{ success: boolean; path?: string; error?: string }> {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
formData.append('piece', piece); formData.append('piece', piece);
@@ -151,6 +151,8 @@ export const apiService = {
if (clef) formData.append('clef', clef); if (clef) formData.append('clef', clef);
if (variant) formData.append('variant', variant); if (variant) formData.append('variant', variant);
if (part) formData.append('part', part); if (part) formData.append('part', part);
if (watermark) formData.append('watermark', 'true');
if (watermarkPosition) formData.append('watermarkPosition', watermarkPosition);
const response = await api.post(`/admin/scores/${scoreId}/upload`, formData, { const response = await api.post(`/admin/scores/${scoreId}/upload`, formData, {
headers: { headers: {

View File

@@ -60,6 +60,7 @@
let uploadClef = $state(''); let uploadClef = $state('');
let uploadVariant = $state(''); let uploadVariant = $state('');
let uploadPart = $state('1'); let uploadPart = $state('1');
let uploadWatermark = $state(false);
function handlePieceCountChange(count: number) { function handlePieceCountChange(count: number) {
newPieceCount = count; newPieceCount = count;
@@ -199,7 +200,8 @@
uploadKey, uploadKey,
uploadClef, uploadClef,
uploadVariant, uploadVariant,
uploadPart uploadPart,
uploadWatermark
); );
if (result.success) { if (result.success) {
uploadSuccess = 'Fichier uploadé avec succès!'; uploadSuccess = 'Fichier uploadé avec succès!';

View File

@@ -58,6 +58,8 @@
let uploadClef = $state(''); let uploadClef = $state('');
let uploadVariant = $state(''); let uploadVariant = $state('');
let uploadPart = $state('1'); let uploadPart = $state('1');
let uploadWatermark = $state(false);
let uploadWatermarkPosition = $state<'left' | 'right'>('left');
// Auto-set default key when instrument changes // Auto-set default key when instrument changes
$effect(() => { $effect(() => {
@@ -193,7 +195,9 @@
uploadKey, uploadKey,
uploadClef, uploadClef,
uploadVariant, uploadVariant,
uploadPart uploadPart,
uploadWatermark,
uploadWatermarkPosition
); );
if (result.success) { if (result.success) {
uploadSuccess = 'Fichier uploadé avec succès'; uploadSuccess = 'Fichier uploadé avec succès';
@@ -495,6 +499,22 @@
<div> <div>
<label class="block text-sm font-medium text-gray-700">Fichier PDF</label> <label class="block text-sm font-medium text-gray-700">Fichier PDF</label>
<div class="flex items-center gap-3 mt-1">
<label class="relative inline-flex items-center cursor-pointer whitespace-nowrap">
<input type="checkbox" bind:checked={uploadWatermark} class="sr-only peer" />
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-ohmj-primary rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-ohmj-primary"></div>
<span class="ml-2 text-sm text-gray-700">Ajouter le numéro</span>
</label>
{#if uploadWatermark}
<select
bind:value={uploadWatermarkPosition}
class="text-sm border border-gray-300 rounded px-2 py-1"
>
<option value="left">Gauche</option>
<option value="right">Droite</option>
</select>
{/if}
</div>
<input <input
id="file-input" id="file-input"
type="file" type="file"