StickerAI-Front/src/screens/Gallery.tsx
2025-03-13 15:51:19 +03:00

260 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState, useRef } from 'react';
import styles from './Gallery.module.css';
import apiService from '../services/api';
import { GeneratedImage, PendingTask } from '../types/api';
import ImageViewer from '../components/shared/ImageViewer';
const GalleryScreen: React.FC = () => {
const [images, setImages] = useState<GeneratedImage[]>([]);
const [pendingTasks, setPendingTasks] = useState<PendingTask[]>([]);
const [loading, setLoading] = useState(true);
const [loadingPendingTasks, setLoadingPendingTasks] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [refreshing, setRefreshing] = useState(false);
const [pullDistance, setPullDistance] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const startY = useRef<number | null>(null);
const threshold = 80; // Порог для активации обновления
// Функция для расчета времени ожидания
const getEstimatedWaitTime = (queuePosition: number | null): string => {
if (queuePosition === null) return 'Генерация началась';
const seconds = apiService.calculateEstimatedWaitTime(queuePosition);
if (seconds < 60) return `${seconds} сек`;
return `${Math.floor(seconds / 60)} мин ${seconds % 60} сек`;
};
// Функция для обновления всех данных
const refreshAll = async () => {
setRefreshing(true);
try {
// Загружаем изображения
const fetchedImages = await apiService.getGeneratedImages();
setImages(fetchedImages);
// Загружаем задачи в очереди
const fetchedPendingTasks = await apiService.getUserPendingTasks();
setPendingTasks(fetchedPendingTasks);
setError(null);
} catch (err) {
setError('Не удалось обновить данные');
console.error(err);
} finally {
// Задержка для лучшего UX, чтобы пользователь видел, что происходит обновление
setTimeout(() => {
setRefreshing(false);
}, 500);
}
};
// Обработчики для pull-to-refresh
const handleTouchStart = (e: React.TouchEvent) => {
if (containerRef.current && containerRef.current.scrollTop === 0) {
startY.current = e.touches[0].clientY;
}
};
const handleTouchMove = (e: React.TouchEvent) => {
if (!startY.current) return;
// Только если контейнер в верхней позиции
if (containerRef.current && containerRef.current.scrollTop === 0) {
const currentY = e.touches[0].clientY;
const diff = currentY - startY.current;
// Применяем сопротивление - чем дальше тянем, тем сложнее
if (diff > 0) {
const resistance = 0.4;
const distance = Math.min(diff * resistance, threshold * 1.5);
setPullDistance(distance);
}
}
};
const handleTouchEnd = async () => {
// Если достигли порога, запускаем обновление
if (pullDistance >= threshold) {
setRefreshing(true);
// Сбрасываем расстояние вытягивания
setPullDistance(0);
// Обновляем данные
await refreshAll();
} else {
// Анимированно возвращаем к исходному состоянию
setPullDistance(0);
}
startY.current = null;
};
// Загрузка данных при монтировании компонента
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setLoadingPendingTasks(true);
// Загружаем изображения
const fetchedImages = await apiService.getGeneratedImages();
setImages(fetchedImages);
// Загружаем задачи в очереди
const fetchedPendingTasks = await apiService.getUserPendingTasks();
setPendingTasks(fetchedPendingTasks);
setError(null);
} catch (err) {
setError('Не удалось загрузить данные');
console.error(err);
} finally {
setLoading(false);
setLoadingPendingTasks(false);
}
};
fetchData();
}, []);
// Отслеживание задач в очереди
useEffect(() => {
// Если в очереди нет задач, не запускаем интервал
if (pendingTasks.length === 0) return;
console.log('Запускаем отслеживание задач в очереди:', pendingTasks);
// Запускаем интервал только если есть задачи в очереди
const intervalId = setInterval(async () => {
try {
// Сохраняем предыдущий список задач для сравнения
const previousTasks = [...pendingTasks];
// Получаем обновленный список задач
const fetchedPendingTasks = await apiService.getUserPendingTasks();
console.log('Обновленные задачи в очереди:', fetchedPendingTasks);
// Обновляем список задач
setPendingTasks(fetchedPendingTasks);
// Проверяем, исчезла ли задача из очереди
if (previousTasks.length > fetchedPendingTasks.length) {
console.log('Задача исчезла из очереди, обновляем галерею');
const fetchedImages = await apiService.getGeneratedImages();
setImages(fetchedImages);
}
// Если все задачи завершены, очищаем интервал
if (fetchedPendingTasks.length === 0) {
console.log('Все задачи завершены, останавливаем отслеживание');
clearInterval(intervalId);
}
} catch (err) {
console.error('Ошибка при обновлении статуса задач:', err);
}
}, 15000); // Обновляем каждые 15 секунд
// Очищаем интервал при размонтировании компонента
return () => clearInterval(intervalId);
}, [pendingTasks.length]); // Зависимость от количества задач
return (
<div className={styles.pullToRefreshContainer}>
<div
className={`${styles.refreshIndicator} ${refreshing ? styles.refreshing : ''}`}
style={{ '--pull-distance': `${pullDistance}px` } as React.CSSProperties}
>
<div className={styles.refreshSpinner}></div>
{refreshing && <span>Обновление...</span>}
</div>
<div
ref={containerRef}
className={styles.content}
style={{ '--pull-distance': `${pullDistance}px` } as React.CSSProperties}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
<div className={styles.header}>
<h1 className={styles.title}>
Галерея стикеров
</h1>
</div>
{loading && (
<div className={styles.placeholder}>
Загрузка изображений...
</div>
)}
{error && (
<div className={styles.error}>
{error}
</div>
)}
{!loading && !error && images.length === 0 && pendingTasks.length === 0 && (
<div className={styles.placeholder}>
У вас пока нет сгенерированных стикеров
</div>
)}
{!loading && !loadingPendingTasks && pendingTasks.length > 0 && (
<div className={styles.pendingTasksSection}>
<h2 className={styles.sectionTitle}>В процессе генерации</h2>
<div className={styles.pendingTasksGrid}>
{pendingTasks.map((task) => (
<div key={task.task_id} className={styles.pendingTaskItem}>
<div className={styles.pendingTaskPlaceholder}>
<div className={styles.spinner}></div>
</div>
<div className={styles.pendingTaskInfo}>
<p className={styles.pendingTaskStatus}>
{task.status === 'PENDING'
? `В очереди: ${task.queue_position}`
: 'Генерация...'}
</p>
<p className={styles.pendingTaskTime}>
Осталось: {getEstimatedWaitTime(task.queue_position)}
</p>
</div>
</div>
))}
</div>
</div>
)}
{!loading && !error && images.length > 0 && (
<div className={styles.imageGrid}>
{images.map((image, index) => (
<div
key={image.id}
className={styles.imageItem}
onClick={() => image.url && setSelectedImage(image.url)}
>
<img
src={image.url || ''}
alt={`Стикер ${index + 1}`}
className={styles.image}
/>
</div>
))}
</div>
)}
</div>
{/* Полноэкранный просмотр */}
{selectedImage && (
<ImageViewer
imageUrl={selectedImage}
onClose={() => setSelectedImage(null)}
/>
)}
</div>
);
};
export default GalleryScreen;