feat: enhance ImagesToPdf component with mobile-friendly file picker and unique file selection logic; update translations for mobile picker guidance

This commit is contained in:
Your Name
2026-03-29 21:04:34 +02:00
parent f82a77febe
commit 5ac1d58742
4 changed files with 54 additions and 10 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Helmet } from 'react-helmet-async'; import { Helmet } from 'react-helmet-async';
import { FileImage } from 'lucide-react'; import { FileImage } from 'lucide-react';
@@ -18,6 +18,8 @@ export default function ImagesToPdf() {
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [taskId, setTaskId] = useState<string | null>(null); const [taskId, setTaskId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [useSinglePickerFlow, setUseSinglePickerFlow] = useState(false);
const inputRef = useRef<HTMLInputElement | null>(null);
const { status, result, error: taskError } = useTaskPolling({ const { status, result, error: taskError } = useTaskPolling({
taskId, taskId,
@@ -35,7 +37,22 @@ export default function ImagesToPdf() {
} }
}, []); // eslint-disable-line react-hooks/exhaustive-deps }, []); // 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 acceptedTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/bmp'];
const acceptValue = acceptedTypes.join(',');
const openPicker = () => {
inputRef.current?.click();
};
const handleFilesSelect = (newFiles: FileList | File[]) => { const handleFilesSelect = (newFiles: FileList | File[]) => {
const fileArray = Array.from(newFiles).filter((f) => const fileArray = Array.from(newFiles).filter((f) =>
@@ -45,7 +62,19 @@ export default function ImagesToPdf() {
setError(t('tools.imagesToPdf.invalidFiles')); setError(t('tools.imagesToPdf.invalidFiles'));
return; 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); setError(null);
}; };
@@ -112,8 +141,7 @@ export default function ImagesToPdf() {
<div className="space-y-4"> <div className="space-y-4">
{/* Drop zone */} {/* Drop zone */}
<div <div
className="upload-zone cursor-pointer" className="upload-zone"
onClick={() => document.getElementById('images-file-input')?.click()}
onDragOver={(e) => e.preventDefault()} onDragOver={(e) => e.preventDefault()}
onDrop={(e) => { onDrop={(e) => {
e.preventDefault(); e.preventDefault();
@@ -122,9 +150,10 @@ export default function ImagesToPdf() {
> >
<input <input
id="images-file-input" id="images-file-input"
ref={inputRef}
type="file" type="file"
accept=".png,.jpg,.jpeg,.webp,.bmp" accept={acceptValue}
multiple multiple={!useSinglePickerFlow}
className="hidden" className="hidden"
onChange={(e) => { onChange={(e) => {
if (e.target.files) handleFilesSelect(e.target.files); if (e.target.files) handleFilesSelect(e.target.files);
@@ -133,12 +162,24 @@ export default function ImagesToPdf() {
/> />
<FileImage className="mb-4 h-12 w-12 text-slate-400" /> <FileImage className="mb-4 h-12 w-12 text-slate-400" />
<p className="mb-2 text-base font-medium text-slate-700"> <p className="mb-2 text-base font-medium text-slate-700">
{t('common.dragDrop')} {files.length > 0 ? t('tools.imagesToPdf.addMore') : t('tools.imagesToPdf.selectImages')}
</p> </p>
<p className="text-sm text-slate-500">PNG, JPG, WebP, BMP</p> <p className="text-sm text-slate-500">PNG, JPG, WebP, BMP</p>
{useSinglePickerFlow && (
<p className="mt-2 text-xs text-slate-500">
{t('tools.imagesToPdf.mobilePickerHint')}
</p>
)}
<p className="mt-1 text-xs text-slate-400"> <p className="mt-1 text-xs text-slate-400">
{t('common.maxSize', { size: 10 })} {t('common.maxSize', { size: 10 })}
</p> </p>
<button
type="button"
onClick={openPicker}
className="btn-secondary mt-4"
>
{files.length > 0 ? t('tools.imagesToPdf.addMore') : t('tools.imagesToPdf.selectImages')}
</button>
</div> </div>
{/* File list */} {/* File list */}

View File

@@ -564,7 +564,8 @@
"addMore": "أضف صور أخرى", "addMore": "أضف صور أخرى",
"imagesSelected": "{{count}} صور مختارة", "imagesSelected": "{{count}} صور مختارة",
"invalidFiles": "يرجى اختيار ملفات صور صالحة (JPG أو PNG أو WebP).", "invalidFiles": "يرجى اختيار ملفات صور صالحة (JPG أو PNG أو WebP).",
"minFiles": "يرجى اختيار صورة واحدة على الأقل." "minFiles": "يرجى اختيار صورة واحدة على الأقل.",
"mobilePickerHint": "في بعض الهواتف يفضَّل اختيار صورة واحدة كل مرة ثم الضغط على حفظ لتأكيدها قبل إضافة الصورة التالية."
}, },
"watermarkPdf": { "watermarkPdf": {
"title": "علامة مائية PDF", "title": "علامة مائية PDF",

View File

@@ -564,7 +564,8 @@
"addMore": "Add More Images", "addMore": "Add More Images",
"imagesSelected": "{{count}} images selected", "imagesSelected": "{{count}} images selected",
"invalidFiles": "Please select valid image files (JPG, PNG, WebP).", "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": { "watermarkPdf": {
"title": "Watermark PDF", "title": "Watermark PDF",

View File

@@ -564,7 +564,8 @@
"addMore": "Ajouter plus d'images", "addMore": "Ajouter plus d'images",
"imagesSelected": "{{count}} images sélectionnées", "imagesSelected": "{{count}} images sélectionnées",
"invalidFiles": "Veuillez sélectionner des fichiers images valides (JPG, PNG, WebP).", "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": { "watermarkPdf": {
"title": "Filigrane PDF", "title": "Filigrane PDF",