diff --git a/src/app/[locale]/pages/search/page.tsx b/src/app/[locale]/pages/search/page.tsx index b21aa3b..0910d6e 100644 --- a/src/app/[locale]/pages/search/page.tsx +++ b/src/app/[locale]/pages/search/page.tsx @@ -11,9 +11,13 @@ export default function Page() {

- +
- test + side
diff --git a/src/app/actions/fileEntity.ts b/src/app/actions/fileEntity.ts index a42edc2..e9607f9 100644 --- a/src/app/actions/fileEntity.ts +++ b/src/app/actions/fileEntity.ts @@ -42,7 +42,7 @@ export async function getUserFilesData(page: number, pageSize: number) { } } -export async function removeUserFile(fileId: string) { +export async function removeUserFile(fileId: string, fullDelete: number) { const token = await getSessionData('token'); try { @@ -54,6 +54,7 @@ export async function removeUserFile(fileId: string) { message_body: { action: 'delete_file', file_id: fileId, + full_delete: fullDelete, token: token } }), @@ -75,6 +76,67 @@ export async function removeUserFile(fileId: string) { throw (`${response.status}`); } + } catch (error) { + return error + } +} + +export async function searchUserFiles(fileId: string) { + const token = await getSessionData('token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/v1/files/${fileId}/similar?auth_token=${token}`, { + method: 'GET' + }); + + if (response.ok) { + let parsed = await response.json(); + + if (parsed.message_code === 0) { + return parsed.message_body; + } else { + throw parsed; + } + } else { + throw (`${response.status}`); + } + + } catch (error) { + return error + } +} + +export async function searchGlobalFiles(fileId: string) { + const token = await getSessionData('token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 20007, + message_body: { + file_id: fileId, + } + }), + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }); + + if (response.ok) { + let parsed = await response.json(); + + if (parsed.message_code === 0) { + return parsed.message_body; + } else { + throw parsed; + } + } else { + throw (`${response.status}`); + } + } catch (error) { return error } diff --git a/src/app/components/ActivityTracker.tsx b/src/app/components/ActivityTracker.tsx index b5ebad5..039bcff 100644 --- a/src/app/components/ActivityTracker.tsx +++ b/src/app/components/ActivityTracker.tsx @@ -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(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) { diff --git a/src/app/components/section-search-file.tsx b/src/app/components/section-search-file.tsx index bff82db..c0c533b 100644 --- a/src/app/components/section-search-file.tsx +++ b/src/app/components/section-search-file.tsx @@ -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(null); const [isFileUploaded, setIsFileUploaded] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); + const [fileId, setFileId] = useState(null); + + const [searchedUserFiles, setSearchedUserFiles] = useState([]); + const [searchedGlobalFiles, setSearchedGlobalFiles] = useState([]); 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 => { 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): 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 => { + + try { + let result = await searchUserFiles(fileId); + + if (result.content.length) { + setSearchedUserFiles(result.content); + } + } catch (error) { + + } + + }, [fileId]) + + const handlerSearchGlobalFile = useCallback(async (fileId: string): Promise => { + + try { + let result = await searchGlobalFiles(fileId); + if (result.images.length) { + setSearchedGlobalFiles(result.images); + } + } catch (error) { + + } + + }, [fileId]) + return (
-

- {t('upload-for-protection', { fileType: t(fileType.toLocaleLowerCase()) })} -

+
+
Как работает поиск?
+
+ Наша система анализирует загруженный файл и сравнивает его с вашими защищенными файлами, + используя алгоритмы компьютерного зрения и цифровых отпечатков. +
+
- +

- {t('drag-the-file-here')} + Выберите файл для поиска

- {/*

- {t('or')} -

*/} - - {fileType === "image" && ( -

- {t('image-resolution')}: 100x100 - 10000x10000 {t('pixels')} -

- )} -

- {t('file-format')}: {acceptString} -

-

- {t('file-size')}: {t('to')} {(maxFileSize / 1024 / 1024)} {t('mb')} -

- {t('select-files-to-protect')} + Выбрать файл {error && ( @@ -352,9 +353,9 @@ export default function SectionSearchFile({ fileType, allowedExtensions, maxFile {selectedFile && (
-
+

@@ -366,52 +367,10 @@ export default function SectionSearchFile({ fileType, allowedExtensions, maxFile {t('size')}: {selectedFile.size}

-

- {t('cost-of-protection')}: 0 -

-

- {t('current-balance')}: 0 -

-
- {isFileUploaded && ( -
- {t('file-successfully-uploaded')} -
- )} -
-
- - {(fileType === 'image' && selectedFile.preview) && ( - Предпросмотр загруженного изображения setError('Не удалось загрузить превью изображения')} - /> - )} - -
- - {uploadProgress !== 0 && (
{uploadProgress}%
@@ -420,6 +379,120 @@ export default function SectionSearchFile({ fileType, allowedExtensions, maxFile
)}
+ + {isFileUploaded && ( + <> +
+ +
+ + {(searchedUserFiles.length !== 0) && ( + + + )} + +
+
+
+ Глобальный поиск изображений +
+
+ Найти где ещё используется ваше изображение в интернете +
+
+ Интернет поиск +
+
+ + +
+
+
+ Поиск по всему интернету +
+
+ Используем продвинутые технологии обратного поиска по изображению для поиска копий вашего контента + на сайтах, в социальных сетях и других источниках. Поможет обнаружить несанкционированное использование. +
+
+ +
+
+
+
+
Глобальных поисков сегодня
+
0 из 10
+
+
+
+
+
+
+
+ + + +
+
+
+ Поиск изображений в интернете... +
+
+ +
+
+
+ Найдено в интернете +
+
+ +
+
+
+ +
+
+

Похожие изображения в интернете не найдены

+

Это может означать, что ваше изображение уникально и не используется на других сайтах.

+
+ +
+ Ошибка глобального поиска: + +
+
+
+ + )}
); } \ No newline at end of file diff --git a/src/app/components/tanstakTable.tsx b/src/app/components/tanstakTable.tsx index cc0ac74..8045053 100644 --- a/src/app/components/tanstakTable.tsx +++ b/src/app/components/tanstakTable.tsx @@ -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 (
- {row.original.supportId ? row.original.supportId: '-'} + {row.original.supportId ? row.original.supportId : '-'}
) }, @@ -447,7 +447,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) { {t('download')} - + */}
@@ -515,7 +515,10 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
+ +
+ + ) + })} + + + + + + ) +} \ No newline at end of file diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index d5a6b12..9d9ddda 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -143,6 +143,7 @@ "pixels": "pixels", "error-processing-file": "Error processing file", "error-uploading-file": "Error uploading file", + "error-duplicate-file": "Such a file already exists", "unknown-validation-error": "Unknown validation error", "unsupported-file-format": "Unsupported file format.", "the-file-is-too-large": "The file is too large", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 5b93fbb..f97c9c2 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -143,6 +143,7 @@ "pixels": "пикселей", "error-processing-file": "Ошибка при обработке файла", "error-uploading-file": "Ошибка при загрузке файла", + "error-duplicate-file": "Такой файл уже есть", "unknown-validation-error": "Неизвестная ошибка валидации", "unsupported-file-format": "Неподдерживаемый формат файла.", "the-file-is-too-large": "Файл слишком большой",