add file search and some fixes and bugs

This commit is contained in:
smanylov
2026-01-27 19:52:01 +07:00
parent 1db333a21b
commit 9a6491b76b
14 changed files with 1205 additions and 127 deletions
+186 -113
View File
@@ -1,20 +1,20 @@
'use client'
import { useRef, useState, useCallback, ChangeEvent, DragEvent, useEffect, useMemo } from 'react';
import { IconShieldAdd } from '@/app/ui/icons/icons';
import { IconSearch } from '@/app/ui/icons/icons';
import { fileUpload, cancelUpload, chunkUpload, checkChunkStatus } from '@/app/actions/fileUpload';
import { useTranslations } from 'next-intl';
import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker';
import { useQueryClient } from '@tanstack/react-query';
import { searchUserFiles, removeUserFile, searchGlobalFiles } from '@/app/actions/fileEntity';
import { SearchedUserFilesList } from '@/app/ui/search/searched-user-files-list';
import { SearchedGlobalFilesList } from '@/app/ui/search/searched-global-files-list';
import { useRouter } from 'next/navigation';
interface SelectedFile {
file: File;
preview: string | undefined;
name: string;
size: string;
dimensions?: {
width: number;
height: number;
};
}
interface FileUploadInitResponse {
@@ -39,6 +39,10 @@ export default function SectionSearchFile({ fileType, allowedExtensions, maxFile
const [uploadId, setUploadId] = useState<string | null>(null);
const [isFileUploaded, setIsFileUploaded] = useState<boolean>(false);
const [uploadProgress, setUploadProgress] = useState<number>(0);
const [fileId, setFileId] = useState<string | null>(null);
const [searchedUserFiles, setSearchedUserFiles] = useState<string[]>([]);
const [searchedGlobalFiles, setSearchedGlobalFiles] = useState<string[]>([]);
const queryClient = useQueryClient();
const isCancelledRef = useRef(false);
@@ -74,37 +78,22 @@ export default function SectionSearchFile({ fileType, allowedExtensions, maxFile
return { isValid: true };
};
const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => {
return new Promise((resolve, reject) => {
const img = new Image();
const objectUrl = URL.createObjectURL(file);
img.onload = () => {
const dimensions = {
width: img.width,
height: img.height
};
URL.revokeObjectURL(objectUrl);
resolve(dimensions);
};
img.onerror = () => {
URL.revokeObjectURL(objectUrl);
reject(new Error(t('error-uploading-file')));
};
img.src = objectUrl;
});
};
const handleFileSelect = useCallback(async (file: File | null): Promise<void> => {
if (!file) {
setError(null);
return;
}
if (fileId) {
await removeUserFile(fileId, 1);
setFileId(null);
}
setError(null);
setIsFileUploaded(false);
setSearchedUserFiles([]);
setSearchedGlobalFiles([]);
setUploadProgress(0);
const validation = validateFile(file);
@@ -114,30 +103,20 @@ export default function SectionSearchFile({ fileType, allowedExtensions, maxFile
return;
}
let dimensions = undefined;
if (fileType === "image") {
try {
dimensions = await getImageDimensions(file) as { width: number, height: number };
if (dimensions.width < 100 || dimensions.height < 100 ||
dimensions.width > 10000 || dimensions.height > 10000) {
setError(`${t('image-size')}: ${dimensions.width}x${dimensions.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.`);
return;
}
} catch (err) {
setError(t('error-reading-image'));
return;
}
}
setSelectedFile({
file,
name: file.name,
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
dimensions,
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined
});
console.log('Файл выбран:', file.name, file.size);
handlerFileUpload({
file,
name: file.name,
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined
});
}, [fileType, t]);
const handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
@@ -221,6 +200,7 @@ export default function SectionSearchFile({ fileType, allowedExtensions, maxFile
return;
}
const isLastChunk = chunkIndex === totalChunks - 1;
const start = chunkIndex * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
@@ -229,7 +209,7 @@ export default function SectionSearchFile({ fileType, allowedExtensions, maxFile
formData.append('upload_id', response.upload_id);
formData.append('chunk_number', chunkIndex.toString());
formData.append('chunk', chunk);
/* formData.append('findSimilar', '0'); */
formData.append('findSimilar', '1');
const chunkResponse = await chunkUpload(formData);
@@ -238,6 +218,11 @@ export default function SectionSearchFile({ fileType, allowedExtensions, maxFile
}
setUploadProgress(Math.floor((chunkIndex + 1) / totalChunks * 100));
if (isLastChunk) {
setFileId(chunkResponse.message_body.file_id);
document.cookie = `searchedFileId=${chunkResponse.message_body.file_id}`
}
}
const chunkStatus = await checkChunkStatus(response.upload_id);
@@ -257,7 +242,7 @@ export default function SectionSearchFile({ fileType, allowedExtensions, maxFile
} finally {
setUploadId(null);
}
}, [uploadId, fileType, t]);
}, [uploadId, fileType]);
useEffect(() => {
// Обработка закрытия вкладки
@@ -295,11 +280,42 @@ export default function SectionSearchFile({ fileType, allowedExtensions, maxFile
}
});
const handlerSearchUserFile = useCallback(async (fileId: string): Promise<void> => {
try {
let result = await searchUserFiles(fileId);
if (result.content.length) {
setSearchedUserFiles(result.content);
}
} catch (error) {
}
}, [fileId])
const handlerSearchGlobalFile = useCallback(async (fileId: string): Promise<void> => {
try {
let result = await searchGlobalFiles(fileId);
if (result.images.length) {
setSearchedGlobalFiles(result.images);
}
} catch (error) {
}
}, [fileId])
return (
<div className="upload-section">
<h3>
{t('upload-for-protection', { fileType: t(fileType.toLocaleLowerCase()) })}
</h3>
<div className="search-info">
<div className="search-info-title">Как работает поиск?</div>
<div className="search-info-text">
Наша система анализирует загруженный файл и сравнивает его с вашими защищенными файлами,
используя алгоритмы компьютерного зрения и цифровых отпечатков.
</div>
</div>
<div
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
@@ -307,25 +323,10 @@ export default function SectionSearchFile({ fileType, allowedExtensions, maxFile
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<IconShieldAdd />
<IconSearch />
<h4>
{t('drag-the-file-here')}
Выберите файл для поиска
</h4>
{/* <p className="mb-2.5 text-[#6b7280]">
{t('or')}
</p> */}
{fileType === "image" && (
<p className="description">
{t('image-resolution')}: 100x100 - 10000x10000 {t('pixels')}
</p>
)}
<p className="description">
{t('file-format')}: {acceptString}
</p>
<p className="description">
{t('file-size')}: {t('to')} {(maxFileSize / 1024 / 1024)} {t('mb')}
</p>
<input
ref={fileInputRef}
@@ -341,7 +342,7 @@ export default function SectionSearchFile({ fileType, allowedExtensions, maxFile
onClick={handleButtonClick}
type="button"
>
{t('select-files-to-protect')}
Выбрать файл
</button>
{error && (
@@ -352,9 +353,9 @@ export default function SectionSearchFile({ fileType, allowedExtensions, maxFile
{selectedFile && (
<div
className={`selected-file ${isFileUploaded ? 'done' : ''}`}
className={`selected-file`}
>
<div className="selected-file-file-info">
<div className="selected-file-file-info relative">
<div>
<p className="text-gray-700">
<span className="font-medium">
@@ -366,52 +367,10 @@ export default function SectionSearchFile({ fileType, allowedExtensions, maxFile
{t('size')}:
</span> {selectedFile.size}
</p>
<p className="text-gray-700">
<span className="font-medium">{t('cost-of-protection')}:</span> 0
</p>
<p className="text-gray-700">
<span className="font-medium">{t('current-balance')}:</span> 0
</p>
</div>
<div>
{isFileUploaded && (
<div className="font-medium text-green-600 text-2xl">
{t('file-successfully-uploaded')}
</div>
)}
</div>
</div>
{(fileType === 'image' && selectedFile.preview) && (
<img
src={selectedFile.preview}
alt="Предпросмотр загруженного изображения"
className="uploaded-image"
onError={() => setError('Не удалось загрузить превью изображения')}
/>
)}
<div className="mt-4 flex gap-3 justify-center relative">
<button
className="btn btn-cancel"
onClick={handleClearFile}
type="button"
>
{isFileUploaded ? t('close') : t('cancel')}
</button>
<button
className={`btn btn-confirm ${error || isFileUploaded || uploadId ? 'disabled' : ''}`}
onClick={() => {
handlerFileUpload(selectedFile);
}}
type="button"
disabled={error || isFileUploaded || uploadId ? true : false}
>
{t('upload-file')}
</button>
{uploadProgress !== 0 && (
<div
className={`absolute top-5 right-5 font-medium ${isFileUploaded ? "text-green-600" : ""}`}
className={`absolute top-0 right-0 font-medium ${isFileUploaded ? "text-green-600" : ""}`}
>
{uploadProgress}%
</div>
@@ -420,6 +379,120 @@ export default function SectionSearchFile({ fileType, allowedExtensions, maxFile
</div>
)}
</div>
{isFileUploaded && (
<>
<div
className="mb-4"
>
<button
className="btn btn-primary btn-search"
/* disabled={fileId ? false : true} */
onClick={() => {
if (fileId) {
handlerSearchUserFile(fileId)
}
}}
>
Начать поиск
</button>
</div>
{(searchedUserFiles.length !== 0) && (
<SearchedUserFilesList list={searchedUserFiles} />
)}
<div
className="global-search-section"
>
<div className="global-search-header">
<div className="global-search-title">
Глобальный поиск изображений
</div>
<div className="">
Найти где ещё используется ваше изображение в интернете
</div>
<div className="global-search-badge">
Интернет поиск
</div>
</div>
<div className="global-search-content">
<div className="global-search-info">
<div className="global-search-info-title">
Поиск по всему интернету
</div>
<div className="global-search-info-text">
Используем продвинутые технологии обратного поиска по изображению для поиска копий вашего контента
на сайтах, в социальных сетях и других источниках. Поможет обнаружить несанкционированное использование.
</div>
</div>
<div className="search-counter" id="search-counter">
<div className="counter-info">
<div className="counter-icon" id="counter-icon"></div>
<div className="counter-text">
<div className="counter-label">Глобальных поисков сегодня</div>
<div className="counter-value" id="counter-value">0 из 10</div>
</div>
</div>
<div className="progress-bar">
<div
className="progress-fill"
id="progress-fill"
style={{ width: '0%' }}
>
</div>
</div>
</div>
<button
type="button"
className="global-search-btn"
id="global-search-btn"
onClick={() => {
if (fileId) {
handlerSearchGlobalFile(fileId)
}
}}
>
Найти в интернете
</button>
<div className="global-loading" id="global-loading">
<div className="global-spinner"></div>
<div className="global-loading-text" id="global-loading-text">
Поиск изображений в интернете...
</div>
</div>
<div className={`global-result ${searchedGlobalFiles.length ? 'show' : ''}`} id="global-result">
<div className="global-result-card">
<div className="global-result-header">
<span>Найдено в интернете</span>
</div>
<div className="global-result-content">
<SearchedGlobalFilesList list={searchedGlobalFiles} />
</div>
</div>
</div>
<div className="global-no-result" id="global-no-result">
<div className="global-no-result-icon">&zwj;</div>
<h4>Похожие изображения в интернете не найдены</h4>
<p>Это может означать, что ваше изображение уникально и не используется на других сайтах.</p>
</div>
<div className="global-error" id="global-error">
<strong>Ошибка глобального поиска:</strong>
<span id="global-error-text"></span>
</div>
</div>
</div>
</>
)}
</div>
);
}