add file search and some fixes and bugs
This commit is contained in:
@@ -3,12 +3,36 @@
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { tokenLifeExtension } from '@/app/actions/auth';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { removeUserFile } from '@/app/actions/fileEntity';
|
||||
|
||||
export default function ActivityTracker() {
|
||||
const lastRefreshTime = useRef(Date.now());
|
||||
const activityTimeoutRef = useRef<NodeJS.Timeout>(null);
|
||||
const isRefreshing = useRef(false);
|
||||
|
||||
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
const selectedId = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('searchedFileId='))
|
||||
?.split('=')[1];
|
||||
|
||||
const handleCookie = async () => {
|
||||
if (selectedId) {
|
||||
await removeUserFile(selectedId, 1);
|
||||
document.cookie = 'searchedFileId=; path=/; max-age=0';
|
||||
}
|
||||
};
|
||||
|
||||
if (selectedId) {
|
||||
handleCookie();
|
||||
}
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
// Debounce функция
|
||||
const debounce = useCallback((func: () => void, delay: number) => {
|
||||
if (activityTimeoutRef.current) {
|
||||
|
||||
@@ -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">‍</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>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
SortingState,
|
||||
ColumnFiltersState,
|
||||
} from '@tanstack/react-table';
|
||||
import { IconImageFile, IconVideoFile, IconAudioFile, IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconShieldExclamation, IconDelete } from '@/app/ui/icons/icons';
|
||||
import { IconImageFile, IconVideoFile, IconAudioFile, IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconShieldExclamation } from '@/app/ui/icons/icons';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import DropDownList from '@/app/components/dropDownList';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
@@ -207,7 +207,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="text-center table-item table-item-id">
|
||||
{row.original.supportId ? row.original.supportId: '-'}
|
||||
{row.original.supportId ? row.original.supportId : '-'}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
@@ -447,7 +447,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
{t('download')}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
{/* <button
|
||||
onClick={() => handleOpenWindowForRemove(row.original)}
|
||||
className="table-action-delete"
|
||||
>
|
||||
@@ -455,7 +455,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
<span>
|
||||
{t('delete')}
|
||||
</span>
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -515,7 +515,10 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
<div className="flex justify-center gap-4">
|
||||
<button className="btn-primary btn-modal"
|
||||
onClick={() => {
|
||||
deleteMutation.mutate(file.id)
|
||||
deleteMutation.mutate({
|
||||
fileId: file.id,
|
||||
removeParam: 1,
|
||||
})
|
||||
}}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
@@ -537,9 +540,10 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
};
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: removeUserFile,
|
||||
mutationFn: ({ fileId, removeParam }: { fileId: string; removeParam: number }) =>
|
||||
removeUserFile(fileId, removeParam),
|
||||
|
||||
onMutate: async (fileId: string) => {
|
||||
onMutate: async ({ fileId, removeParam }: { fileId: string; removeParam: number }) => {
|
||||
await queryClient.cancelQueries({ queryKey: ['userFilesData'] });
|
||||
|
||||
queryClient.setQueryData<ApiResponse>(['userFilesData'], (old) => {
|
||||
@@ -556,7 +560,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
toast.error(t('error'));
|
||||
},
|
||||
|
||||
onSuccess: (response, fileId) => {
|
||||
onSuccess: (response, { fileId }) => {
|
||||
if (response === fileId) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['userFilesData'],
|
||||
|
||||
@@ -234,6 +234,9 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
const chunkResponse = await chunkUpload(formData);
|
||||
|
||||
if (chunkResponse.message_desc !== 'Chunk uploaded successfully') {
|
||||
if (chunkResponse.message_desc === 'Duplicate file upload') {
|
||||
throw (`duplicate-file`);
|
||||
}
|
||||
throw new Error(`Chunk ${chunkIndex} upload failed`);
|
||||
}
|
||||
|
||||
@@ -251,8 +254,12 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
|
||||
} catch (error) {
|
||||
if (!isCancelledRef.current) {
|
||||
setError(t('error-uploading-file'));
|
||||
console.error('Upload error:', error);
|
||||
if (error === 'duplicate-file') {
|
||||
setError(t('error-duplicate-file'));
|
||||
} else {
|
||||
setError(t('error-uploading-file'));
|
||||
console.error('Upload error:', error);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setUploadId(null);
|
||||
|
||||
Reference in New Issue
Block a user