add null result for search files
This commit is contained in:
@@ -5,12 +5,16 @@ import { searchUserFiles, searchGlobalFiles } from '@/app/actions/fileEntity';
|
||||
|
||||
export function FileSearchPanel({ fileId, ref }: { fileId: string | null, ref: any }) {
|
||||
const [searchedUserFiles, setSearchedUserFiles] = useState<string[]>([]);
|
||||
const [searchedUserFilesShowNull, setSearchedUserFilesShowNull] = useState<boolean>(false);
|
||||
const [searchedGlobalFiles, setSearchedGlobalFiles] = useState<string[]>([]);
|
||||
const [searchedGlobalFilesShowNull, setSearchedGlobalFilesShowNull] = useState<boolean>(false);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
doSomething: () => {
|
||||
clearList: () => {
|
||||
setSearchedUserFiles([]);
|
||||
setSearchedGlobalFiles([])
|
||||
setSearchedGlobalFiles([]);
|
||||
setSearchedUserFilesShowNull(false);
|
||||
setSearchedGlobalFilesShowNull(false);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -22,6 +26,8 @@ export function FileSearchPanel({ fileId, ref }: { fileId: string | null, ref: a
|
||||
|
||||
if (result.content.length) {
|
||||
setSearchedUserFiles(result.content);
|
||||
} else {
|
||||
setSearchedUserFilesShowNull(true);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -35,6 +41,8 @@ export function FileSearchPanel({ fileId, ref }: { fileId: string | null, ref: a
|
||||
let result = await searchGlobalFiles(fileId);
|
||||
if (result.images.length) {
|
||||
setSearchedGlobalFiles(result.images);
|
||||
} else {
|
||||
setSearchedGlobalFilesShowNull(true);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -62,7 +70,29 @@ export function FileSearchPanel({ fileId, ref }: { fileId: string | null, ref: a
|
||||
|
||||
{(searchedUserFiles.length !== 0) && (
|
||||
<SearchedUserFilesList list={searchedUserFiles} />
|
||||
)}
|
||||
|
||||
{searchedUserFilesShowNull && (
|
||||
<div
|
||||
className="user-file-search-results"
|
||||
>
|
||||
<div className="results-header">
|
||||
<div className="results-title">Результаты поиска</div>
|
||||
<div className="results-count">
|
||||
Найдено файлов: {searchedUserFiles.length}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="empty-results"
|
||||
>
|
||||
<h3>Похожих файлов не найдено</h3>
|
||||
<p>В вашей библиотеке нет файлов, похожих на загруженный</p>
|
||||
<div className="empty-results-hint">
|
||||
<strong>Совет:</strong> Убедитесь, что вы загружали этот файл ранее через страницу "Маркировка"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
@@ -141,11 +171,13 @@ export function FileSearchPanel({ fileId, ref }: { fileId: string | null, ref: a
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="global-no-result" id="global-no-result">
|
||||
<div className="global-no-result-icon">‍</div>
|
||||
<h4>Похожие изображения в интернете не найдены</h4>
|
||||
<p>Это может означать, что ваше изображение уникально и не используется на других сайтах.</p>
|
||||
</div>
|
||||
{searchedGlobalFilesShowNull && (
|
||||
<div className="global-no-result" id="global-no-result">
|
||||
<div className="global-no-result-icon">‍</div>
|
||||
<h4>Похожие изображения в интернете не найдены</h4>
|
||||
<p>Это может означать, что ваше изображение уникально и не используется на других сайтах.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="global-error" id="global-error">
|
||||
<strong>Ошибка глобального поиска:</strong>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { toast } from 'sonner';
|
||||
import { useState, ReactNode } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import ModalWindow from '@/app/components/modalWindow';
|
||||
import { IconEye, IconDownload } from '@/app/ui/icons/icons';
|
||||
|
||||
export function SearchedUserFilesList({ list }: { list: any }) {
|
||||
const [isFileLoading, setIsFileLoading] = useState(false);
|
||||
@@ -53,7 +54,7 @@ export function SearchedUserFilesList({ list }: { list: any }) {
|
||||
<div
|
||||
className="modal-window-view-file-content"
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<div className="modal-window-view-file-content-wrapper">
|
||||
<div className="image-section">
|
||||
<div className="image-container">
|
||||
<img
|
||||
@@ -116,6 +117,7 @@ export function SearchedUserFilesList({ list }: { list: any }) {
|
||||
className="results-list"
|
||||
>
|
||||
{list.map((e: any, index: number) => {
|
||||
console.log(e);
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
@@ -124,18 +126,20 @@ export function SearchedUserFilesList({ list }: { list: any }) {
|
||||
<div className="result-item">
|
||||
<div className="result-header">
|
||||
<div className="result-preview">
|
||||
<img src="view-image.php?id=431" alt="Preview" />
|
||||
<div className="preview-fallback">
|
||||
<span>📄</span>
|
||||
</div>
|
||||
{e.url ? (
|
||||
<img src={e.url} alt="Preview" />
|
||||
) : (
|
||||
<div className="preview-fallback">
|
||||
<span>📄</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="result-info">
|
||||
<div className="result-filename" title={e.originalFileName}>
|
||||
{e.originalFileName}
|
||||
</div>
|
||||
<div className="result-details">
|
||||
Загружен: - •
|
||||
Загружен: {e.uploadDate ? e.uploadDate : '#'} •
|
||||
Размер: {convertBytes(e.fileSize)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,31 +151,29 @@ export function SearchedUserFilesList({ list }: { list: any }) {
|
||||
</div>
|
||||
|
||||
<div className="result-badges">
|
||||
<span className="badge badge-protected">
|
||||
🛡️ Защищено
|
||||
<span className={`badge ${e.status === 'PROTECTED' ? 'badge-protected' : 'badge-not-protected'}`}>
|
||||
{e.status}
|
||||
</span>
|
||||
<span className="badge badge-high-protection">
|
||||
🔒 Высокий уровень защиты
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div className="result-actions">
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
className="btn btn-secondary gap-1"
|
||||
onClick={() => {
|
||||
handlerViewfile(e);
|
||||
}}
|
||||
>
|
||||
👁️ Просмотр
|
||||
<IconEye />
|
||||
Просмотр
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
className="btn btn-success gap-1"
|
||||
disabled={isFileLoading}
|
||||
onClick={() => handlerDownload(e.fileId, e.originalFileName)}
|
||||
>
|
||||
💾 Скачать
|
||||
<IconDownload />
|
||||
Скачать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useState, useCallback, ChangeEvent, DragEvent, useEffect, useMemo } from 'react';
|
||||
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 { removeUserFile } from '@/app/actions/fileEntity';
|
||||
import { FileSearchPanel } from '@/app/ui/search/file-search-panel';
|
||||
interface SelectedFile {
|
||||
file: File;
|
||||
preview: string | undefined;
|
||||
name: string;
|
||||
size: string;
|
||||
}
|
||||
|
||||
interface FileUploadInitResponse {
|
||||
upload_id: string,
|
||||
file_name: string,
|
||||
total_chunks: number,
|
||||
chunk_size: number,
|
||||
status: string
|
||||
}
|
||||
|
||||
interface SectionSearchFile {
|
||||
fileType: string
|
||||
allowedExtensions: string[]
|
||||
maxFileSize: number
|
||||
}
|
||||
|
||||
export default function SectionSearchFile({ fileType, allowedExtensions, maxFileSize }: SectionSearchFile) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const [selectedFile, setSelectedFile] = useState<SelectedFile | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
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 queryClient = useQueryClient();
|
||||
const isCancelledRef = useRef(false);
|
||||
|
||||
const childRef = useRef(null);
|
||||
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const acceptString = useMemo(() => {
|
||||
if (!allowedExtensions || !Array.isArray(allowedExtensions)) {
|
||||
return '';
|
||||
}
|
||||
return allowedExtensions.map(e => `.${e}`).join(', ');
|
||||
}, [allowedExtensions]);
|
||||
|
||||
const validateFile = (file: File): { isValid: boolean; errorMessage?: string } => {
|
||||
if (!allowedExtensions.includes(file.type as string)) {
|
||||
const extension = file.name.split('.').pop()?.toLowerCase();
|
||||
if (!extension || !allowedExtensions.includes(extension as string)) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('unsupported-file-format')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_SIZE = maxFileSize;
|
||||
|
||||
if (file.size > MAX_SIZE) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('the-file-is-too-large')
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
if (childRef.current) {
|
||||
//@ts-ignore
|
||||
childRef.current.clearList();
|
||||
}
|
||||
|
||||
setUploadProgress(0);
|
||||
|
||||
const validation = validateFile(file);
|
||||
|
||||
if (!validation.isValid) {
|
||||
setError(validation.errorMessage || t('unknown-validation-error'));
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedFile({
|
||||
file,
|
||||
name: file.name,
|
||||
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
||||
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined
|
||||
});
|
||||
|
||||
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 => {
|
||||
const file = event.target.files?.[0] || null;
|
||||
handleFileSelect(file);
|
||||
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleButtonClick = (): void => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (event: DragEvent<HTMLDivElement>): void => {
|
||||
event.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (): void => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (event: DragEvent<HTMLDivElement>): void => {
|
||||
event.preventDefault();
|
||||
setIsDragging(false);
|
||||
|
||||
const file = event.dataTransfer.files[0];
|
||||
handleFileSelect(file);
|
||||
};
|
||||
|
||||
const handlerFileUpload = useCallback(async (fileInfo: SelectedFile): Promise<void> => {
|
||||
if (uploadId) return;
|
||||
|
||||
setError(null);
|
||||
isCancelledRef.current = false;
|
||||
const file = fileInfo.file;
|
||||
|
||||
try {
|
||||
const extension = file.name.split('.').pop() || '';
|
||||
const initMessageBody = {
|
||||
file_name: file.name,
|
||||
file_type: fileType,
|
||||
extension: extension,
|
||||
file_size: file.size
|
||||
};
|
||||
|
||||
const response = await fileUpload(initMessageBody) as FileUploadInitResponse;
|
||||
|
||||
if (!response?.upload_id) {
|
||||
throw new Error('Failed to get upload_id');
|
||||
}
|
||||
|
||||
const CHUNK_SIZE = response.chunk_size;
|
||||
const totalChunks = response.total_chunks;
|
||||
|
||||
setUploadId(response.upload_id);
|
||||
|
||||
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
|
||||
if (isCancelledRef.current) {
|
||||
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);
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('upload_id', response.upload_id);
|
||||
formData.append('chunk_number', chunkIndex.toString());
|
||||
formData.append('chunk', chunk);
|
||||
formData.append('findSimilar', '1');
|
||||
|
||||
const chunkResponse = await chunkUpload(formData);
|
||||
|
||||
if (chunkResponse.message_desc !== 'Chunk uploaded successfully') {
|
||||
throw new Error(`Chunk ${chunkIndex} upload failed`);
|
||||
}
|
||||
|
||||
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);
|
||||
if (chunkStatus.message_body.missing_chunks === 0) {
|
||||
setIsFileUploaded(true);
|
||||
await queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['userFilesInfo'] });
|
||||
} else {
|
||||
throw new Error('Not all chunks were uploaded');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (!isCancelledRef.current) {
|
||||
setError(t('error-uploading-file'));
|
||||
console.error('Upload error:', error);
|
||||
}
|
||||
} finally {
|
||||
setUploadId(null);
|
||||
}
|
||||
}, [uploadId, fileType]);
|
||||
|
||||
useEffect(() => {
|
||||
// Обработка закрытия вкладки
|
||||
// Тут можно попробовать изучить вопрос с navigator.sendBeacon('', blob)
|
||||
// он нужен для того что бы послать запрос на бек при закрытии вкладки,
|
||||
// но у него есть свои особенности и стандартные запросы не работают.
|
||||
const handleUnload = () => {
|
||||
if (uploadId) {
|
||||
//const data = JSON.stringify({ uploadId, reason: 'tab_closed' });
|
||||
//const blob = new Blob([data], { type: 'application/json' });
|
||||
//navigator.sendBeacon('/api/v1/data/cancel', blob);
|
||||
console.log('sendBeacon');
|
||||
}
|
||||
};
|
||||
if (uploadId) {
|
||||
window.addEventListener('unload', handleUnload);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('unload', handleUnload);
|
||||
};
|
||||
}, [uploadId]);
|
||||
|
||||
useNavigationBlocker({
|
||||
shouldBlock: !!uploadId,
|
||||
message: t('have-unsaved-changes'),
|
||||
onConfirm: async () => {
|
||||
console.log('User confirmed navigation');
|
||||
if (uploadId) {
|
||||
await cancelUpload(uploadId);
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
console.log('User cancelled navigation');
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="upload-section">
|
||||
<div className="search-info">
|
||||
<div className="search-info-title">Как работает поиск?</div>
|
||||
<div className="search-info-text">
|
||||
Наша система анализирует загруженный файл и сравнивает его с вашими защищенными файлами,
|
||||
используя алгоритмы компьютерного зрения и цифровых отпечатков.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<IconSearch />
|
||||
<h4>
|
||||
Выберите файл для поиска
|
||||
</h4>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept={acceptString}
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileInputChange}
|
||||
aria-label="Выбор файла для защиты"
|
||||
/>
|
||||
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleButtonClick}
|
||||
type="button"
|
||||
>
|
||||
Выбрать файл
|
||||
</button>
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-red-600 font-medium">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedFile && (
|
||||
<div
|
||||
className={`selected-file`}
|
||||
>
|
||||
<div className="selected-file-file-info relative">
|
||||
<div>
|
||||
<p className="text-gray-700">
|
||||
<span className="font-medium">
|
||||
{t('file')}:
|
||||
</span> {selectedFile.name}
|
||||
</p>
|
||||
<p className="text-gray-700">
|
||||
<span className="font-medium">
|
||||
{t('size')}:
|
||||
</span> {selectedFile.size}
|
||||
</p>
|
||||
</div>
|
||||
{uploadProgress !== 0 && (
|
||||
<div
|
||||
className={`absolute top-0 right-0 font-medium ${isFileUploaded ? "text-green-600" : ""}`}
|
||||
>
|
||||
{uploadProgress}%
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isFileUploaded && (
|
||||
<FileSearchPanel fileId={fileId} ref={childRef} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user