From d6ae0cf92c75f18d1ac4dd9216b971af981c35ed Mon Sep 17 00:00:00 2001 From: Vlad0sEnIgma345 Date: Thu, 27 Nov 2025 08:41:23 +0300 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=D0=B0=20=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/app/(tabs)/two.tsx | 85 ++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 apps/frontend/app/(tabs)/two.tsx diff --git a/apps/frontend/app/(tabs)/two.tsx b/apps/frontend/app/(tabs)/two.tsx new file mode 100644 index 0000000..3752f9e --- /dev/null +++ b/apps/frontend/app/(tabs)/two.tsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react'; +import { + TextInput, + View as RNView, + TouchableOpacity, + Alert, + ActivityIndicator, +} from 'react-native'; +import { useRouter } from 'expo-router'; +import { Text, View } from '@/components/Themed'; +import { useColorScheme } from '@/components/useColorScheme'; +import Colors from '@/constants/Colors'; +import { usersApi } from '@/lib/api'; +import { Search, User } from 'lucide-react-native'; + +export default function SearchScreen() { + const [searchQuery, setSearchQuery] = useState(''); + const [loading, setLoading] = useState(false); + const colorScheme = useColorScheme(); + const colors = Colors[colorScheme ?? 'light']; + const router = useRouter(); + + const handleSearch = async () => { + const query = searchQuery.trim(); + + if (!query) { + Alert.alert('Ошибка', 'Введите имя пользователя'); + return; + } + + setLoading(true); + try { + const user = await usersApi.getUserByName(query); + // Переходим к профилю пользователя по его ID + router.push(`/user/${user.id}` as any); + setSearchQuery(''); + } catch (error: any) { + Alert.alert('Пользователь не найден', error.message || 'Пользователь с таким именем не существует'); + } finally { + setLoading(false); + } + }; + + return ( + + + + + + {loading ? ( + + ) : ( + + )} + + + + + + Поиск пользователей + + Введите имя пользователя в поле выше и нажмите поиск, чтобы просмотреть его профиль + + + + ); +} -- 2.49.0 From 7fd19f9a0921db0776c345cea87432714e4f6b05 Mon Sep 17 00:00:00 2001 From: Vlad0sEnIgma345 Date: Thu, 27 Nov 2025 09:00:18 +0300 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=20=D0=B4?= =?UTF-8?q?=D0=B5=D1=82=D0=B0=D0=BB=D0=B5=D0=B9=20=D1=81=D0=B5=D0=B9=D0=B2?= =?UTF-8?q?=D0=B0=20=D1=81=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=D1=8E=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/app/save/[id].tsx | 432 ++++++++++++++++++++++++++++++++ 1 file changed, 432 insertions(+) create mode 100644 apps/frontend/app/save/[id].tsx 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}`} + + + + )} + + + )} + + + ); +} -- 2.49.0 From bc972febc5529f7c56429d1c74e5d22c8ff5f7f5 Mon Sep 17 00:00:00 2001 From: Vlad0sEnIgma345 Date: Thu, 27 Nov 2025 09:11:24 +0300 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8?= =?UTF-8?q?=D0=BB=D1=8C=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8F=20=D1=81=20=D0=BF=D1=83=D0=B1=D0=BB?= =?UTF-8?q?=D0=B8=D1=87=D0=BD=D1=8B=D0=BC=D0=B8=20=D1=81=D0=B5=D0=B9=D0=B2?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/app/user/[slug].tsx | 184 ++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 apps/frontend/app/user/[slug].tsx diff --git a/apps/frontend/app/user/[slug].tsx b/apps/frontend/app/user/[slug].tsx new file mode 100644 index 0000000..4c4f9d9 --- /dev/null +++ b/apps/frontend/app/user/[slug].tsx @@ -0,0 +1,184 @@ +import React, { useEffect, useState } from 'react'; +import { + FlatList, + TouchableOpacity, + Image, + RefreshControl, + Alert, + ActivityIndicator, +} from 'react-native'; +import { useRouter, useLocalSearchParams } from 'expo-router'; +import { useVideoPlayer, VideoView } from 'expo-video'; +import { Text, View } from '@/components/Themed'; +import { savesApi } from '@/lib/api'; +import type { SaveListItem } from '@shared-types'; +import { useColorScheme } from '@/components/useColorScheme'; +import Colors from '@/constants/Colors'; +import { File, ArrowLeft, Image as ImageIcon } from 'lucide-react-native'; + +export default function UserProfileScreen() { + const { slug } = useLocalSearchParams<{ slug: string }>(); + const [saves, setSaves] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const router = useRouter(); + const colorScheme = useColorScheme(); + const colors = Colors[colorScheme ?? 'light']; + + const loadSaves = async () => { + if (!slug) return; + + try { + const data = await savesApi.getPublicSavesByUser(slug); + setSaves(data); + } catch (error: any) { + Alert.alert('Ошибка', error.message || 'Не удалось загрузить сейвы'); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + useEffect(() => { + if (slug) { + loadSaves(); + } + }, [slug]); + + const handleRefresh = () => { + setRefreshing(true); + loadSaves(); + }; + + const handleSavePress = (save: SaveListItem) => { + router.push(`/save/${save.id}`); + }; + + // Компонент для элемента списка + const SaveItem = React.memo(({ item }: { item: SaveListItem }) => { + if (!item) return null; + + const itemColorScheme = useColorScheme(); + const itemColors = Colors[itemColorScheme ?? 'light']; + + const videoPlayer = useVideoPlayer( + item.type === 'video' ? item.url : null, + (player) => { + player.muted = true; // Без звука в списке + player.loop = true; // Зацикливание для превью + } + ); + + return ( + handleSavePress(item)} + > + + + {item.type === 'image' || item.type === 'gif' ? ( + + ) : item.type === 'video' ? ( + + ) : ( + + + + )} + + + + {item.name || 'Без названия'} + + {item.description && ( + + {item.description} + + )} + {item.tags && item.tags.length > 0 && ( + + {item.tags.slice(0, 3).map((tag, idx) => ( + + + {tag} + + + ))} + {item.tags.length > 3 && ( + + +{item.tags.length - 3} + + )} + + )} + + {new Date(item.createdAt).toLocaleDateString('ru-RU')} + + + + + ); + }); + + if (loading) { + return ( + + + + ); + } + + return ( + + + router.back()} + className="p-2" + > + + + Публичные сейвы + + + + {saves.length === 0 ? ( + + + Нет публичных сейвов + + У этого пользователя пока нет публичных сейвов + + + ) : ( + item.id.toString()} + renderItem={({ item }) => } + refreshControl={ + + } + contentContainerStyle={{ padding: 16 }} + /> + )} + + ); +} -- 2.49.0 From 362172e832b147cbc8c71ad732823ce9a6bbe8ac Mon Sep 17 00:00:00 2001 From: Vlad0sEnIgma345 Date: Thu, 27 Nov 2025 09:47:08 +0300 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D0=B5=D0=B9?= =?UTF-8?q?=D0=B2=D0=B0=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=B5=20=D1=81=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=BE=D0=B9=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/app/add.tsx | 212 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 apps/frontend/app/add.tsx diff --git a/apps/frontend/app/add.tsx b/apps/frontend/app/add.tsx new file mode 100644 index 0000000..2cd8009 --- /dev/null +++ b/apps/frontend/app/add.tsx @@ -0,0 +1,212 @@ +import React, { useState } from 'react'; +import { + TextInput, + TouchableOpacity, + ScrollView, + Alert, + ActivityIndicator, + Platform, +} from 'react-native'; +import { useRouter } from 'expo-router'; +import * as ImagePicker from 'expo-image-picker'; +import { Text, View } from '@/components/Themed'; +import { savesApi } from '@/lib/api'; +import { useColorScheme } from '@/components/useColorScheme'; +import Colors from '@/constants/Colors'; + +export default function AddSaveScreen() { + const [mode, setMode] = useState<'upload' | 'url'>('upload'); + const [url, setUrl] = useState(''); + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [tags, setTags] = useState(''); + const [visibility, setVisibility] = useState<'public' | 'link'>('link'); + const [loading, setLoading] = useState(false); + const router = useRouter(); + const colorScheme = useColorScheme(); + const colors = Colors[colorScheme ?? 'light']; + + const handleUpload = async () => { + if (mode === 'url') { + if (!url) { + Alert.alert('Ошибка', 'Введите URL'); + return; + } + } + + setLoading(true); + try { + if (mode === 'url') { + const tagsArray = tags + ? tags.split(',').map((t) => t.trim()).filter(Boolean) + : undefined; + + await savesApi.createFromUrl({ + url, + name: name || undefined, + description: description || undefined, + tags: tagsArray, + visibility, + }); + } else { + // Загрузка файла + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.All, + allowsEditing: false, + quality: 1, + }); + + if (result.canceled) { + setLoading(false); + return; + } + + const asset = result.assets[0]; + const file = { + uri: asset.uri, + type: asset.mimeType || 'image/jpeg', + name: asset.fileName || `image.${asset.uri.split('.').pop()}`, + }; + + const tagsArray = tags + ? tags.split(',').map((t) => t.trim()).filter(Boolean) + : undefined; + + await savesApi.uploadFile(file, { + name: name || undefined, + description: description || undefined, + tags: tagsArray, + visibility, + }); + } + + Alert.alert('Успех', 'Сейв успешно создан', [ + { text: 'OK', onPress: () => router.back() }, + ]); + } catch (error: any) { + Alert.alert('Ошибка', error.message || 'Не удалось создать сейв'); + } finally { + setLoading(false); + } + }; + + return ( + + + Добавить сейв + + + setMode('upload')} + > + + Загрузить файл + + + setMode('url')} + > + + По URL + + + + + {mode === 'url' && ( + + )} + + + + + + + + + Видимость: + setVisibility('public')} + > + + Публичный + + + setVisibility('link')} + > + + По ссылке + + + + + + {loading ? ( + + ) : ( + + {mode === 'url' ? 'Создать из URL' : 'Выбрать файл'} + + )} + + + + ); +} -- 2.49.0 From c1769be91fca53d323a1a353e88060e279b45e98 Mon Sep 17 00:00:00 2001 From: Vlad0sEnIgma345 Date: Thu, 27 Nov 2025 10:13:52 +0300 Subject: [PATCH 5/5] =?UTF-8?q?chore:=20=D0=BD=D0=B5=D0=B7=D0=BD=D0=B0?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D1=81=D1=82=D0=B8=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/app/(tabs)/two.tsx | 7 +++--- apps/frontend/app/save/[id].tsx | 36 +++++++++++++++---------------- apps/frontend/app/user/[slug].tsx | 12 +++++------ 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/apps/frontend/app/(tabs)/two.tsx b/apps/frontend/app/(tabs)/two.tsx index 3752f9e..b6d80a3 100644 --- a/apps/frontend/app/(tabs)/two.tsx +++ b/apps/frontend/app/(tabs)/two.tsx @@ -31,12 +31,13 @@ export default function SearchScreen() { setLoading(true); try { const user = await usersApi.getUserByName(query); - // Переходим к профилю пользователя по его ID router.push(`/user/${user.id}` as any); setSearchQuery(''); - } catch (error: any) { + } + catch (error: any) { Alert.alert('Пользователь не найден', error.message || 'Пользователь с таким именем не существует'); - } finally { + } + finally { setLoading(false); } }; diff --git a/apps/frontend/app/save/[id].tsx b/apps/frontend/app/save/[id].tsx index 9feb462..0a71ecd 100644 --- a/apps/frontend/app/save/[id].tsx +++ b/apps/frontend/app/save/[id].tsx @@ -46,10 +46,12 @@ export default function SaveDetailScreen() { setDescription(data.description || ''); setTags(data.tags.join(', ') || ''); setVisibility(data.visibility); - } catch (error: any) { + } + catch (error: any) { Alert.alert('Ошибка', error.message || 'Не удалось загрузить сейв'); router.back(); - } finally { + } + finally { setLoading(false); } }; @@ -71,9 +73,11 @@ export default function SaveDetailScreen() { setSave(updated as SaveDetailResponse); setEditing(false); Alert.alert('Успех', 'Сейв обновлен'); - } catch (error: any) { + } + catch (error: any) { Alert.alert('Ошибка', error.message || 'Не удалось обновить сейв'); - } finally { + } + finally { setSaving(false); } }; @@ -105,7 +109,6 @@ export default function SaveDetailScreen() { const handleShare = async () => { if (!save) return; - // Используем прямой URL из S3 const shareUrl = save.url; if (shareUrl) { @@ -114,7 +117,8 @@ export default function SaveDetailScreen() { message: shareUrl, url: shareUrl, }); - } catch (error) { + } + catch (error) { console.error('Ошибка при попытке поделиться:', error); } } @@ -123,17 +127,15 @@ export default function SaveDetailScreen() { 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 для ручного копирования + } + catch (error) { Alert.alert( 'Ссылка для доступа', `Скопируйте эту ссылку:\n\n${shareUrl}`, @@ -149,21 +151,19 @@ export default function SaveDetailScreen() { 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') { + } + else if (save && save.type !== 'video') { await videoPlayer.replaceAsync(null); } }; @@ -183,10 +183,8 @@ export default function SaveDetailScreen() { } const isOwner = user?.id === save.userId; - // Используем прямой URL из S3 вместо /download эндпоинта const mediaUrl = save.url; - // Компонент для отображения медиа const renderMedia = () => { if (save.type === 'video') { return ( @@ -198,7 +196,8 @@ export default function SaveDetailScreen() { fullscreenOptions={{ enable: true }} /> ); - } else if (save.type === 'image' || save.type === 'gif') { + } + else if (save.type === 'image' || save.type === 'gif') { return ( ); - } else { + } + else { return ( diff --git a/apps/frontend/app/user/[slug].tsx b/apps/frontend/app/user/[slug].tsx index 4c4f9d9..e2ced3c 100644 --- a/apps/frontend/app/user/[slug].tsx +++ b/apps/frontend/app/user/[slug].tsx @@ -31,9 +31,11 @@ export default function UserProfileScreen() { try { const data = await savesApi.getPublicSavesByUser(slug); setSaves(data); - } catch (error: any) { + } + catch (error: any) { Alert.alert('Ошибка', error.message || 'Не удалось загрузить сейвы'); - } finally { + } + finally { setLoading(false); setRefreshing(false); } @@ -54,18 +56,16 @@ export default function UserProfileScreen() { router.push(`/save/${save.id}`); }; - // Компонент для элемента списка const SaveItem = React.memo(({ item }: { item: SaveListItem }) => { if (!item) return null; const itemColorScheme = useColorScheme(); const itemColors = Colors[itemColorScheme ?? 'light']; - const videoPlayer = useVideoPlayer( item.type === 'video' ? item.url : null, (player) => { - player.muted = true; // Без звука в списке - player.loop = true; // Зацикливание для превью + player.muted = true; + player.loop = true; } ); -- 2.49.0