260 lines
9.8 KiB
TypeScript
260 lines
9.8 KiB
TypeScript
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;
|