diff --git a/apps/frontend/app/save/[id].tsx b/apps/frontend/app/save/[id].tsx new file mode 100644 index 0000000..9feb462 --- /dev/null +++ b/apps/frontend/app/save/[id].tsx @@ -0,0 +1,432 @@ +import React, { useEffect, useState } from 'react'; +import { + ScrollView, + Image, + TouchableOpacity, + TextInput, + Alert, + ActivityIndicator, + Share, +} from 'react-native'; +import { useRouter, useLocalSearchParams } from 'expo-router'; +import { useVideoPlayer, VideoView } from 'expo-video'; +import { Text, View } from '@/components/Themed'; +import { useAuth } from '@/lib/auth'; +import { savesApi } from '@/lib/api'; +import { API_BASE_URL } from '@/config/api'; +import type { SaveDetailResponse } from '@shared-types'; +import { useColorScheme } from '@/components/useColorScheme'; +import Colors from '@/constants/Colors'; +import { File, X, Check, Copy, Share as ShareIcon, Edit, Trash, User } from 'lucide-react-native'; + +export default function SaveDetailScreen() { + const { id } = useLocalSearchParams<{ id: string }>(); + const [save, setSave] = useState(null); + const [loading, setLoading] = useState(true); + const [editing, setEditing] = useState(false); + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [tags, setTags] = useState(''); + const [visibility, setVisibility] = useState<'public' | 'link'>('link'); + const [saving, setSaving] = useState(false); + const { user } = useAuth(); + const router = useRouter(); + const colorScheme = useColorScheme(); + const colors = Colors[colorScheme ?? 'light']; + + useEffect(() => { + loadSave(); + }, [id]); + + const loadSave = async () => { + try { + const data = await savesApi.getSaveById(Number(id)); + setSave(data); + setName(data.name || ''); + setDescription(data.description || ''); + setTags(data.tags.join(', ') || ''); + setVisibility(data.visibility); + } catch (error: any) { + Alert.alert('Ошибка', error.message || 'Не удалось загрузить сейв'); + router.back(); + } finally { + setLoading(false); + } + }; + + const handleSave = async () => { + setSaving(true); + try { + const tagsArray = tags + ? tags.split(',').map((t) => t.trim()).filter(Boolean) + : []; + + const updated = await savesApi.updateSave(Number(id), { + name: name || undefined, + description: description || undefined, + tags: tagsArray, + visibility, + }); + + setSave(updated as SaveDetailResponse); + setEditing(false); + Alert.alert('Успех', 'Сейв обновлен'); + } catch (error: any) { + Alert.alert('Ошибка', error.message || 'Не удалось обновить сейв'); + } finally { + setSaving(false); + } + }; + + const handleDelete = () => { + Alert.alert( + 'Удаление', + 'Вы уверены, что хотите удалить этот сейв?', + [ + { text: 'Отмена', style: 'cancel' }, + { + text: 'Удалить', + style: 'destructive', + onPress: async () => { + try { + await savesApi.deleteSave(Number(id)); + Alert.alert('Успех', 'Сейв удален', [ + { text: 'OK', onPress: () => router.back() }, + ]); + } catch (error: any) { + Alert.alert('Ошибка', error.message || 'Не удалось удалить сейв'); + } + }, + }, + ] + ); + }; + + const handleShare = async () => { + if (!save) return; + + // Используем прямой URL из S3 + const shareUrl = save.url; + + if (shareUrl) { + try { + await Share.share({ + message: shareUrl, + url: shareUrl, + }); + } catch (error) { + console.error('Ошибка при попытке поделиться:', error); + } + } + }; + + const handleCopyShareUrl = async () => { + if (!save || !save.shareUrl) return; + + // Формируем полный URL для доступа к сейву по share token + const shareUrl = `${API_BASE_URL}/saves/${save.id}?share=${save.shareUrl}`; + + try { + // Используем Share API - на некоторых платформах это позволяет скопировать + await Share.share({ + message: `Ссылка для доступа к сейву: ${shareUrl}`, + url: shareUrl, + }); + } catch (error) { + // Если Share не работает, показываем Alert с URL для ручного копирования + Alert.alert( + 'Ссылка для доступа', + `Скопируйте эту ссылку:\n\n${shareUrl}`, + [ + { text: 'OK' }, + ] + ); + } + }; + + const handleViewUserProfile = () => { + if (!save || !save.userId) return; + router.push(`/user/${save.userId}` as any); + }; + + // Создаем видеоплеер (хуки должны вызываться до условных возвратов) + const videoPlayer = useVideoPlayer( + save?.type === 'video' ? save?.url ?? null : null, + (player) => { + // Автоматически не запускаем воспроизведение + player.muted = false; + } + ); + + // Обновляем источник видео при изменении save + useEffect(() => { + const updateVideoSource = async () => { + if (save?.type === 'video' && save?.url) { + await videoPlayer.replaceAsync(save.url); + } else if (save && save.type !== 'video') { + await videoPlayer.replaceAsync(null); + } + }; + updateVideoSource(); + }, [save?.type, save?.url, videoPlayer]); + + if (loading) { + return ( + + + + ); + } + + if (!save) { + return null; + } + + const isOwner = user?.id === save.userId; + // Используем прямой URL из S3 вместо /download эндпоинта + const mediaUrl = save.url; + + // Компонент для отображения медиа + const renderMedia = () => { + if (save.type === 'video') { + return ( + + ); + } else if (save.type === 'image' || save.type === 'gif') { + return ( + + ); + } else { + return ( + + + {save.type} + + ); + } + }; + + return ( + + + {renderMedia()} + + + + {editing ? 'Редактирование' : save.name || 'Без названия'} + + + {isOwner ? ( + editing ? ( + <> + { + setEditing(false); + setName(save.name || ''); + setDescription(save.description || ''); + setTags(save.tags.join(', ') || ''); + setVisibility(save.visibility); + }} + className="p-2" + > + + + + {saving ? ( + + ) : ( + + )} + + + ) : ( + <> + {save.shareUrl && ( + + + + )} + + + + setEditing(true)} + className="p-2" + > + + + + + + + ) + ) : ( + <> + {save.shareUrl && ( + + + + )} + + + + + + + + )} + + + + {editing ? ( + <> + + + + + Видимость: + setVisibility('public')} + > + + Публичный + + + setVisibility('link')} + > + + По ссылке + + + + + ) : ( + <> + {save.description && ( + {save.description} + )} + {save.tags.length > 0 && ( + + {save.tags.map((tag, idx) => ( + + + {tag} + + + ))} + + )} + + {!isOwner && save.userId && ( + + + + Просмотреть профиль автора + + + )} + + Тип: {save.type} + + + Видимость: {save.visibility === 'public' ? 'Публичный' : 'По ссылке'} + + + Создан: {new Date(save.createdAt).toLocaleString('ru-RU')} + + {save.shareUrl && ( + + + Ссылка для доступа: + + + + + {`${API_BASE_URL}/saves/${save.id}?share=${save.shareUrl}`} + + + + )} + + + )} + + + ); +}