diff --git a/frontend/src/components/tools/ImagesToPdf.tsx b/frontend/src/components/tools/ImagesToPdf.tsx index cd45889..c393ebb 100644 --- a/frontend/src/components/tools/ImagesToPdf.tsx +++ b/frontend/src/components/tools/ImagesToPdf.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Helmet } from 'react-helmet-async'; import { FileImage } from 'lucide-react'; @@ -18,6 +18,8 @@ export default function ImagesToPdf() { const [isUploading, setIsUploading] = useState(false); const [taskId, setTaskId] = useState(null); const [error, setError] = useState(null); + const [useSinglePickerFlow, setUseSinglePickerFlow] = useState(false); + const inputRef = useRef(null); const { status, result, error: taskError } = useTaskPolling({ taskId, @@ -35,7 +37,22 @@ export default function ImagesToPdf() { } }, []); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { + if (typeof window === 'undefined') { + return; + } + + const coarsePointer = window.matchMedia?.('(pointer: coarse)').matches ?? false; + const mobileUserAgent = /android|iphone|ipad|ipod|mobile/i.test(navigator.userAgent); + setUseSinglePickerFlow(coarsePointer || mobileUserAgent); + }, []); + const acceptedTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/bmp']; + const acceptValue = acceptedTypes.join(','); + + const openPicker = () => { + inputRef.current?.click(); + }; const handleFilesSelect = (newFiles: FileList | File[]) => { const fileArray = Array.from(newFiles).filter((f) => @@ -45,7 +62,19 @@ export default function ImagesToPdf() { setError(t('tools.imagesToPdf.invalidFiles')); return; } - setFiles((prev) => [...prev, ...fileArray]); + setFiles((prev) => { + const seen = new Set(prev.map((file) => `${file.name}:${file.size}:${file.lastModified}`)); + const uniqueNewFiles = fileArray.filter((file) => { + const key = `${file.name}:${file.size}:${file.lastModified}`; + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); + + return [...prev, ...uniqueNewFiles]; + }); setError(null); }; @@ -112,8 +141,7 @@ export default function ImagesToPdf() {
{/* Drop zone */}
document.getElementById('images-file-input')?.click()} + className="upload-zone" onDragOver={(e) => e.preventDefault()} onDrop={(e) => { e.preventDefault(); @@ -122,9 +150,10 @@ export default function ImagesToPdf() { > { if (e.target.files) handleFilesSelect(e.target.files); @@ -133,12 +162,24 @@ export default function ImagesToPdf() { />

- {t('common.dragDrop')} + {files.length > 0 ? t('tools.imagesToPdf.addMore') : t('tools.imagesToPdf.selectImages')}

PNG, JPG, WebP, BMP

+ {useSinglePickerFlow && ( +

+ {t('tools.imagesToPdf.mobilePickerHint')} +

+ )}

{t('common.maxSize', { size: 10 })}

+
{/* File list */} diff --git a/frontend/src/i18n/ar.json b/frontend/src/i18n/ar.json index 4f00bdc..74d94a7 100644 --- a/frontend/src/i18n/ar.json +++ b/frontend/src/i18n/ar.json @@ -564,7 +564,8 @@ "addMore": "أضف صور أخرى", "imagesSelected": "{{count}} صور مختارة", "invalidFiles": "يرجى اختيار ملفات صور صالحة (JPG أو PNG أو WebP).", - "minFiles": "يرجى اختيار صورة واحدة على الأقل." + "minFiles": "يرجى اختيار صورة واحدة على الأقل.", + "mobilePickerHint": "في بعض الهواتف يفضَّل اختيار صورة واحدة كل مرة ثم الضغط على حفظ لتأكيدها قبل إضافة الصورة التالية." }, "watermarkPdf": { "title": "علامة مائية PDF", diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 3440a4e..ba03ae4 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -564,7 +564,8 @@ "addMore": "Add More Images", "imagesSelected": "{{count}} images selected", "invalidFiles": "Please select valid image files (JPG, PNG, WebP).", - "minFiles": "Please select at least one image." + "minFiles": "Please select at least one image.", + "mobilePickerHint": "On some phones, select one image at a time and tap Save to confirm it before adding the next image." }, "watermarkPdf": { "title": "Watermark PDF", diff --git a/frontend/src/i18n/fr.json b/frontend/src/i18n/fr.json index 81e155a..24f1dc9 100644 --- a/frontend/src/i18n/fr.json +++ b/frontend/src/i18n/fr.json @@ -564,7 +564,8 @@ "addMore": "Ajouter plus d'images", "imagesSelected": "{{count}} images sélectionnées", "invalidFiles": "Veuillez sélectionner des fichiers images valides (JPG, PNG, WebP).", - "minFiles": "Veuillez sélectionner au moins une image." + "minFiles": "Veuillez sélectionner au moins une image.", + "mobilePickerHint": "Sur certains téléphones, sélectionnez une image à la fois puis appuyez sur Enregistrer avant d'ajouter la suivante." }, "watermarkPdf": { "title": "Filigrane PDF",