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:
@@ -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<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({
|
||||
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() {
|
||||
<div className="space-y-4">
|
||||
{/* Drop zone */}
|
||||
<div
|
||||
className="upload-zone cursor-pointer"
|
||||
onClick={() => 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() {
|
||||
>
|
||||
<input
|
||||
id="images-file-input"
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept=".png,.jpg,.jpeg,.webp,.bmp"
|
||||
multiple
|
||||
accept={acceptValue}
|
||||
multiple={!useSinglePickerFlow}
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
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" />
|
||||
<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 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">
|
||||
{t('common.maxSize', { size: 10 })}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openPicker}
|
||||
className="btn-secondary mt-4"
|
||||
>
|
||||
{files.length > 0 ? t('tools.imagesToPdf.addMore') : t('tools.imagesToPdf.selectImages')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* File list */}
|
||||
|
||||
@@ -564,7 +564,8 @@
|
||||
"addMore": "أضف صور أخرى",
|
||||
"imagesSelected": "{{count}} صور مختارة",
|
||||
"invalidFiles": "يرجى اختيار ملفات صور صالحة (JPG أو PNG أو WebP).",
|
||||
"minFiles": "يرجى اختيار صورة واحدة على الأقل."
|
||||
"minFiles": "يرجى اختيار صورة واحدة على الأقل.",
|
||||
"mobilePickerHint": "في بعض الهواتف يفضَّل اختيار صورة واحدة كل مرة ثم الضغط على حفظ لتأكيدها قبل إضافة الصورة التالية."
|
||||
},
|
||||
"watermarkPdf": {
|
||||
"title": "علامة مائية PDF",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user