feat: обновлен механизм удаления стикеров в галерее
- обавлено удаление стикеров по file_id - зменен механизм выхода из режима удаления (теперь по касанию в любое место кроме крестика) - далена кнопка 'отово' и связанные стили - лучшен UX режима удаления
This commit is contained in:
parent
6426b05597
commit
b9c026a23c
108
src/components/shared/ImageWithFallback.module.css
Normal file
108
src/components/shared/ImageWithFallback.module.css
Normal file
@ -0,0 +1,108 @@
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-surface-variant, #f0f0f0);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.loadingContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--color-surface-variant, #f0f0f0);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 3px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 50%;
|
||||
border-top-color: var(--color-primary, #3498db);
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.errorContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--color-surface-variant, #f0f0f0);
|
||||
z-index: 2;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.errorIcon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-error, #d32f2f);
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
color: var(--color-text, #333);
|
||||
font-size: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.retryButton {
|
||||
background-color: var(--color-primary, #3498db);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--border-radius, 4px);
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.retryButton:hover {
|
||||
background-color: var(--color-primary-dark, #2980b9);
|
||||
}
|
||||
|
||||
/* Для мобильных устройств делаем кнопку больше для удобства нажатия */
|
||||
@media (max-width: 768px) {
|
||||
.retryButton {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
150
src/components/shared/ImageWithFallback.tsx
Normal file
150
src/components/shared/ImageWithFallback.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import styles from './ImageWithFallback.module.css';
|
||||
|
||||
interface ImageWithFallbackProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
maxRetries?: number;
|
||||
}
|
||||
|
||||
const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({
|
||||
src,
|
||||
alt,
|
||||
className = '',
|
||||
onClick,
|
||||
maxRetries = 2 // По умолчанию 2 попытки автоматической перезагрузки
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
const [imageSrc, setImageSrc] = useState(src);
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
// Сбрасываем состояние при изменении src
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
setRetryCount(0);
|
||||
setImageSrc(src);
|
||||
}, [src]);
|
||||
|
||||
// Проверяем, загружено ли изображение из кэша
|
||||
useEffect(() => {
|
||||
// Если изображение уже загружено (из кэша), сразу устанавливаем состояние
|
||||
if (imgRef.current && imgRef.current.complete && imgRef.current.naturalWidth > 0) {
|
||||
console.log('Изображение уже загружено (из кэша):', src);
|
||||
setLoading(false);
|
||||
setError(false);
|
||||
}
|
||||
|
||||
// Устанавливаем таймаут для предотвращения бесконечной загрузки
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (loading) {
|
||||
console.log('Таймаут загрузки изображения:', src);
|
||||
// Проверяем, загружено ли изображение фактически
|
||||
if (imgRef.current && imgRef.current.complete && imgRef.current.naturalWidth > 0) {
|
||||
// Изображение загружено, но событие onLoad не сработало
|
||||
console.log('Изображение фактически загружено, но событие onLoad не сработало:', src);
|
||||
setLoading(false);
|
||||
setError(false);
|
||||
} else {
|
||||
// Изображение действительно не загрузилось
|
||||
console.error('Изображение не загрузилось после таймаута:', src);
|
||||
setLoading(false);
|
||||
setError(true);
|
||||
}
|
||||
}
|
||||
}, 5000); // 5 секунд
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [loading, src]);
|
||||
|
||||
// Функция для перезагрузки изображения
|
||||
const handleRetry = () => {
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
// Добавляем случайный параметр к URL для предотвращения кэширования
|
||||
setImageSrc(`${src}${src.includes('?') ? '&' : '?'}retry=${Date.now()}`);
|
||||
setRetryCount(prevCount => prevCount + 1);
|
||||
};
|
||||
|
||||
// Обработчик успешной загрузки
|
||||
const handleLoad = () => {
|
||||
console.log('Изображение успешно загружено:', src);
|
||||
setLoading(false);
|
||||
setError(false);
|
||||
};
|
||||
|
||||
// Обработчик ошибки загрузки
|
||||
const handleError = () => {
|
||||
console.error('Ошибка загрузки изображения:', src);
|
||||
setLoading(false);
|
||||
setError(true);
|
||||
|
||||
// Автоматически пытаемся перезагрузить изображение, если не превышено максимальное количество попыток
|
||||
if (retryCount < maxRetries) {
|
||||
console.log(`Автоматическая попытка перезагрузки изображения (${retryCount + 1}/${maxRetries}):`, src);
|
||||
// Увеличиваем задержку с каждой попыткой (экспоненциальный backoff)
|
||||
const delay = Math.pow(2, retryCount) * 1000;
|
||||
setTimeout(handleRetry, delay); // Пауза перед повторной попыткой
|
||||
} else {
|
||||
console.error('Не удалось загрузить изображение после нескольких попыток:', src);
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик клика по изображению
|
||||
const handleClick = () => {
|
||||
// Если есть ошибка и пользователь кликает на изображение с ошибкой, пытаемся перезагрузить
|
||||
if (error) {
|
||||
handleRetry();
|
||||
} else if (onClick) {
|
||||
// Иначе вызываем переданный обработчик клика
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.container} ${className}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{/* Показываем индикатор загрузки, если изображение загружается */}
|
||||
{loading && (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner}></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Показываем изображение */}
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={imageSrc}
|
||||
alt={alt}
|
||||
className={`${styles.image} ${error ? styles.hidden : ''}`}
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
/>
|
||||
|
||||
{/* Показываем сообщение об ошибке и кнопку перезагрузки, если произошла ошибка */}
|
||||
{error && (
|
||||
<div className={styles.errorContainer}>
|
||||
<div className={styles.errorIcon}>!</div>
|
||||
<div className={styles.errorMessage}>Ошибка загрузки</div>
|
||||
<button
|
||||
className={styles.retryButton}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Предотвращаем всплытие события
|
||||
handleRetry();
|
||||
}}
|
||||
>
|
||||
Перезагрузить
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageWithFallback;
|
||||
@ -44,6 +44,7 @@
|
||||
font-size: 14px;
|
||||
margin-bottom: var(--spacing-small);
|
||||
line-height: 1.4;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.promptContainer {
|
||||
|
||||
@ -9,7 +9,9 @@ interface NotificationModalProps {
|
||||
promptText?: string;
|
||||
onGalleryClick: () => void;
|
||||
onContinueClick: () => void;
|
||||
showGalleryButton?: boolean; // Новый параметр для управления видимостью кнопки "В галерею"
|
||||
showGalleryButton?: boolean; // Параметр для управления видимостью кнопки "В галерею"
|
||||
showButtons?: boolean; // Новый параметр для управления видимостью всех кнопок
|
||||
continueButtonText?: string; // Новый параметр для изменения текста кнопки "Продолжить"
|
||||
}
|
||||
|
||||
const NotificationModal: React.FC<NotificationModalProps> = ({
|
||||
@ -20,7 +22,9 @@ const NotificationModal: React.FC<NotificationModalProps> = ({
|
||||
promptText,
|
||||
onGalleryClick,
|
||||
onContinueClick,
|
||||
showGalleryButton = true // По умолчанию кнопка видима
|
||||
showGalleryButton = true, // По умолчанию кнопка "В галерею" видима
|
||||
showButtons = true, // По умолчанию все кнопки видимы
|
||||
continueButtonText = 'Продолжить' // По умолчанию текст кнопки "Продолжить"
|
||||
}) => {
|
||||
if (!isVisible) return null;
|
||||
|
||||
@ -42,6 +46,7 @@ const NotificationModal: React.FC<NotificationModalProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showButtons && (
|
||||
<div className={styles.buttons}>
|
||||
{showGalleryButton && (
|
||||
<button
|
||||
@ -55,9 +60,10 @@ const NotificationModal: React.FC<NotificationModalProps> = ({
|
||||
className={`${styles.button} ${!showGalleryButton ? styles.primaryButton : styles.secondaryButton}`}
|
||||
onClick={onContinueClick}
|
||||
>
|
||||
Продолжить
|
||||
{continueButtonText}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -114,6 +114,52 @@
|
||||
background-color: var(--color-surface);
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent; /* Убираем подсветку при тапе на мобильных */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.deleteMode .imageItem {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.deleteControls {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px;
|
||||
background-color: var(--color-surface);
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.exitDeleteModeButton {
|
||||
padding: 10px 24px;
|
||||
background-color: var(--color-primary, #3498db);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image {
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import React, { useEffect, useState, useRef, useCallback } 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';
|
||||
import ImageWithFallback from '../components/shared/ImageWithFallback';
|
||||
import NotificationModal from '../components/shared/NotificationModal';
|
||||
|
||||
const GalleryScreen: React.FC = () => {
|
||||
const [images, setImages] = useState<GeneratedImage[]>([]);
|
||||
@ -17,6 +19,12 @@ const GalleryScreen: React.FC = () => {
|
||||
const startY = useRef<number | null>(null);
|
||||
const threshold = 80; // Порог для активации обновления
|
||||
|
||||
// Состояния для режима удаления
|
||||
const [isDeleteMode, setIsDeleteMode] = useState(false);
|
||||
const [selectedForDelete, setSelectedForDelete] = useState<GeneratedImage | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const longPressTimer = useRef<number | null>(null);
|
||||
|
||||
// Функция для расчета времени ожидания
|
||||
const getEstimatedWaitTime = (queuePosition: number | null): string => {
|
||||
if (queuePosition === null) return 'Генерация началась';
|
||||
@ -26,8 +34,66 @@ const GalleryScreen: React.FC = () => {
|
||||
return `${Math.floor(seconds / 60)} мин ${seconds % 60} сек`;
|
||||
};
|
||||
|
||||
// Обработчики для режима удаления
|
||||
const handleLongPress = useCallback((image: GeneratedImage) => {
|
||||
setIsDeleteMode(true);
|
||||
}, []);
|
||||
|
||||
const startLongPressTimer = useCallback((image: GeneratedImage) => {
|
||||
longPressTimer.current = setTimeout(() => {
|
||||
handleLongPress(image);
|
||||
}, 800); // 800ms для долгого нажатия
|
||||
}, [handleLongPress]);
|
||||
|
||||
const cancelLongPressTimer = useCallback(() => {
|
||||
if (longPressTimer.current) {
|
||||
clearTimeout(longPressTimer.current);
|
||||
longPressTimer.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDeleteClick = useCallback((image: GeneratedImage) => {
|
||||
setSelectedForDelete(image);
|
||||
}, []);
|
||||
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
if (selectedForDelete) {
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await apiService.deleteImage(selectedForDelete.link);
|
||||
|
||||
// Обновляем список изображений
|
||||
setImages(prevImages =>
|
||||
prevImages.filter(img => img.id !== selectedForDelete.id)
|
||||
);
|
||||
|
||||
setIsDeleting(false);
|
||||
setSelectedForDelete(null);
|
||||
} catch (error) {
|
||||
console.error('Error deleting image:', error);
|
||||
setIsDeleting(false);
|
||||
}
|
||||
}
|
||||
}, [selectedForDelete]);
|
||||
|
||||
const exitDeleteMode = useCallback(() => {
|
||||
setIsDeleteMode(false);
|
||||
setSelectedForDelete(null);
|
||||
}, []);
|
||||
|
||||
// Обработчик клика по контейнеру для выхода из режима удаления
|
||||
const handleGridClick = useCallback((e: React.MouseEvent) => {
|
||||
// Проверяем, что клик был не по крестику удаления
|
||||
if (isDeleteMode && !(e.target as HTMLElement).closest(`.${styles.deleteButton}`)) {
|
||||
setIsDeleteMode(false);
|
||||
setSelectedForDelete(null);
|
||||
}
|
||||
}, [isDeleteMode]);
|
||||
|
||||
// Функция для обновления всех данных
|
||||
const refreshAll = async () => {
|
||||
// Выходим из режима удаления при обновлении
|
||||
setIsDeleteMode(false);
|
||||
setRefreshing(true);
|
||||
try {
|
||||
// Загружаем изображения
|
||||
@ -227,22 +293,38 @@ const GalleryScreen: React.FC = () => {
|
||||
)}
|
||||
|
||||
{!loading && !error && images.length > 0 && (
|
||||
<div className={styles.imageGrid}>
|
||||
<div
|
||||
className={`${styles.imageGrid} ${isDeleteMode ? styles.deleteMode : ''}`}
|
||||
onClick={handleGridClick}
|
||||
>
|
||||
{images.map((image, index) => (
|
||||
<div
|
||||
key={image.id}
|
||||
className={styles.imageItem}
|
||||
onClick={() => image.url && setSelectedImage(image.url)}
|
||||
onTouchStart={() => !isDeleteMode && startLongPressTimer(image)}
|
||||
onTouchEnd={() => !isDeleteMode && cancelLongPressTimer()}
|
||||
onTouchMove={() => !isDeleteMode && cancelLongPressTimer()}
|
||||
>
|
||||
<img
|
||||
<ImageWithFallback
|
||||
src={image.url || ''}
|
||||
alt={`Стикер ${index + 1}`}
|
||||
className={styles.image}
|
||||
onClick={() => !isDeleteMode && image.url && setSelectedImage(image.url)}
|
||||
maxRetries={3}
|
||||
/>
|
||||
{isDeleteMode && (
|
||||
<div
|
||||
className={styles.deleteButton}
|
||||
onClick={() => handleDeleteClick(image)}
|
||||
>
|
||||
✕
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Полноэкранный просмотр */}
|
||||
@ -252,6 +334,18 @@ const GalleryScreen: React.FC = () => {
|
||||
onClose={() => setSelectedImage(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Модальное окно подтверждения удаления */}
|
||||
<NotificationModal
|
||||
isVisible={!!selectedForDelete}
|
||||
title="Удаление стикера"
|
||||
message="Вы уверены, что хотите удалить этот стикер?"
|
||||
isLoading={isDeleting}
|
||||
showGalleryButton={false}
|
||||
continueButtonText="Удалить"
|
||||
onContinueClick={handleConfirmDelete}
|
||||
onGalleryClick={() => setSelectedForDelete(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -45,6 +45,8 @@ const Home: React.FC = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [promptText, setPromptText] = useState('');
|
||||
const [showGalleryButton, setShowGalleryButton] = useState(true);
|
||||
const [showButtons, setShowButtons] = useState(true); // Новое состояние для управления видимостью всех кнопок
|
||||
const [continueButtonText, setContinueButtonText] = useState('Продолжить'); // Новое состояние для текста кнопки "Продолжить"
|
||||
|
||||
// Состояние для хранения данных о последней успешной генерации
|
||||
const [lastGenerationData, setLastGenerationData] = useState<LastGenerationData>({});
|
||||
@ -82,7 +84,9 @@ const Home: React.FC = () => {
|
||||
setNotificationTitle('Внимание');
|
||||
setNotificationMessage('Сначала загрузите изображение');
|
||||
setIsLoading(false);
|
||||
setShowGalleryButton(true); // Показываем кнопку "В галерею" для уведомлений
|
||||
setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была запущена
|
||||
setShowButtons(true); // Показываем кнопки
|
||||
setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть"
|
||||
setIsNotificationVisible(true);
|
||||
return;
|
||||
}
|
||||
@ -92,7 +96,9 @@ const Home: React.FC = () => {
|
||||
setNotificationTitle('Внимание');
|
||||
setNotificationMessage('Выберите образ для генерации');
|
||||
setIsLoading(false);
|
||||
setShowGalleryButton(true); // Показываем кнопку "В галерею" для уведомлений
|
||||
setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была запущена
|
||||
setShowButtons(true); // Показываем кнопки
|
||||
setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть"
|
||||
setIsNotificationVisible(true);
|
||||
return;
|
||||
}
|
||||
@ -102,7 +108,9 @@ const Home: React.FC = () => {
|
||||
setNotificationTitle('Внимание');
|
||||
setNotificationMessage('Введите текст промпта');
|
||||
setIsLoading(false);
|
||||
setShowGalleryButton(true); // Показываем кнопку "В галерею" для уведомлений
|
||||
setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была запущена
|
||||
setShowButtons(true); // Показываем кнопки
|
||||
setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть"
|
||||
setIsNotificationVisible(true);
|
||||
return;
|
||||
}
|
||||
@ -136,7 +144,9 @@ const Home: React.FC = () => {
|
||||
setNotificationTitle('Внимание');
|
||||
setNotificationMessage('Нельзя отправить одну и ту же комбинацию изображения и образа подряд. Пожалуйста, измените изображение или выберите другой образ.');
|
||||
setIsLoading(false);
|
||||
setShowGalleryButton(true); // Показываем кнопку "В галерею" для уведомлений
|
||||
setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была запущена
|
||||
setShowButtons(true); // Показываем кнопки
|
||||
setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть"
|
||||
setIsNotificationVisible(true);
|
||||
return;
|
||||
}
|
||||
@ -148,6 +158,8 @@ const Home: React.FC = () => {
|
||||
setIsLoading(true);
|
||||
setPromptText('');
|
||||
setShowGalleryButton(true); // Показываем кнопку "В галерею" для уведомлений о генерации
|
||||
setShowButtons(false); // Скрываем все кнопки во время отправки запроса
|
||||
setContinueButtonText('Продолжить'); // Сбрасываем текст кнопки на значение по умолчанию
|
||||
setIsNotificationVisible(true);
|
||||
|
||||
// Если выбран "Свой промпт" и введен текст, используем его
|
||||
@ -176,6 +188,9 @@ const Home: React.FC = () => {
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была успешно запущена
|
||||
setShowButtons(true); // Показываем кнопки в случае ошибки перевода
|
||||
setContinueButtonText('Закрыть'); // Меняем текст кнопки на "Закрыть"
|
||||
return;
|
||||
}
|
||||
|
||||
@ -193,13 +208,24 @@ const Home: React.FC = () => {
|
||||
? `${minutes} мин ${seconds} сек`
|
||||
: `${seconds} сек`;
|
||||
|
||||
setNotificationMessage(`Ваша задача отправлена на генерацию!\nПозиция в очереди: ${result.queue_position}\nПримерное время ожидания: ${timeString}`);
|
||||
// Форматируем сообщение в выбранном формате (Вариант 2)
|
||||
setNotificationMessage(
|
||||
`Создание стикеров началось!\n` +
|
||||
`Позиция в очереди: ${result.queue_position}\n` +
|
||||
`Время ожидания: ${timeString}\n\n` +
|
||||
`Результат будет доступен в галерее после завершения генерации.`
|
||||
);
|
||||
} else {
|
||||
setNotificationMessage('Ваша задача отправлена на генерацию!');
|
||||
setNotificationMessage(
|
||||
`Создание стикеров началось!\n\n` +
|
||||
`Результат будет доступен в галерее после завершения генерации.`
|
||||
);
|
||||
}
|
||||
|
||||
// Устанавливаем использованный промпт и убираем индикатор загрузки
|
||||
// Устанавливаем использованный промпт, показываем кнопки и меняем текст кнопки "Продолжить" на "Закрыть"
|
||||
setPromptText(usedPrompt);
|
||||
setShowButtons(true);
|
||||
setContinueButtonText('Закрыть');
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
@ -208,7 +234,9 @@ const Home: React.FC = () => {
|
||||
setNotificationTitle('Ошибка');
|
||||
setNotificationMessage('Не удалось начать генерацию');
|
||||
setIsLoading(false);
|
||||
setShowGalleryButton(true); // Показываем кнопку "В галерею" для уведомлений
|
||||
setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была успешно запущена
|
||||
setShowButtons(true); // Показываем кнопки в случае ошибки
|
||||
setContinueButtonText('Закрыть'); // Меняем текст кнопки на "Закрыть"
|
||||
setIsNotificationVisible(true);
|
||||
}
|
||||
return;
|
||||
@ -307,6 +335,8 @@ const Home: React.FC = () => {
|
||||
onGalleryClick={handleGalleryClick}
|
||||
onContinueClick={handleContinueClick}
|
||||
showGalleryButton={showGalleryButton}
|
||||
showButtons={showButtons}
|
||||
continueButtonText={continueButtonText}
|
||||
/>
|
||||
|
||||
{/* Компонент обработки обратной связи */}
|
||||
@ -318,6 +348,8 @@ const Home: React.FC = () => {
|
||||
setNotificationMessage('Ваше сообщение успешно отправлено');
|
||||
setIsLoading(false);
|
||||
setShowGalleryButton(false); // Скрываем кнопку "В галерею" для уведомления об обратной связи
|
||||
setShowButtons(true); // Показываем кнопки
|
||||
setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть"
|
||||
setIsNotificationVisible(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -111,28 +111,54 @@ const apiService = {
|
||||
throw new Error('Failed to fetch images');
|
||||
}
|
||||
|
||||
// Получаем массив строк с file_id
|
||||
const fileIds = await response.json() as string[];
|
||||
// Получаем массив массивов [file_id, created_at]
|
||||
const rawData = await response.json() as [string, string][];
|
||||
|
||||
// Преобразуем массив строк в массив объектов GeneratedImage
|
||||
const images = fileIds.map((fileId, index) => ({
|
||||
// Преобразуем в массив объектов GeneratedImage
|
||||
const images = rawData.map((item, index) => {
|
||||
const [fileId, createdAt] = item;
|
||||
return {
|
||||
id: index + 1,
|
||||
link: fileId,
|
||||
prompt_id: '',
|
||||
status: 'COMPLETED',
|
||||
created_at: new Date().toISOString(),
|
||||
created_at: createdAt,
|
||||
sticker_set_id: null,
|
||||
url: `${API_BASE_URL}/stickers/proxy/sticker/${encodeURIComponent(fileId)}`
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
||||
// Сортируем изображения от новых к старым (по id в обратном порядке)
|
||||
return images.sort((a, b) => b.id - a.id);
|
||||
// Сортируем изображения от новых к старым по дате создания
|
||||
return images.sort((a, b) =>
|
||||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error fetching images:', error);
|
||||
throw new Error('Failed to fetch images');
|
||||
}
|
||||
},
|
||||
|
||||
// Удаление изображения по file_id
|
||||
async deleteImage(fileId: string): Promise<string> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/delete-by-link?link=${encodeURIComponent(fileId)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete image');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error deleting image:', error);
|
||||
throw new Error('Failed to delete image');
|
||||
}
|
||||
},
|
||||
|
||||
async generateImage(imageData: string, style?: string, promptId?: string, userPrompt?: string): Promise<GenerationResult> {
|
||||
try {
|
||||
// Создаем копию базового воркфлоу
|
||||
|
||||
Loading…
Reference in New Issue
Block a user