Add files via upload

This commit is contained in:
Savsis 2026-05-08 03:53:51 +05:00 • committed by GitHub
parent 6050fed0d8
commit 8439481e34
9 changed files with 580 additions and 317 deletions

View file

@ -62,9 +62,13 @@ export default function Player() {
// Shake → shuffle
const onShake = useCallback(() => {
if (!shuffle) toggleShuffle();
const ni = usePlayer.getState().nextTrack();
usePlayer.getState().setIndex(ni);
try {
if (!shuffle) toggleShuffle();
const state = usePlayer.getState();
if (state.nextTrack && state.setIndex) {
state.setIndex(state.nextTrack());
}
} catch {}
}, [shuffle, toggleShuffle]);
useShakeToShuffle(onShake);
@ -184,10 +188,35 @@ export default function Player() {
pointerEvents: playerOpacity.value > 0.5 ? 'auto' : 'none',
}));
// onNext/onPrev MUST be defined BEFORE coverGesture (no hoisting for useCallback)
const onNext = useCallback(() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
try {
const ni = usePlayer.getState().nextTrack();
usePlayer.getState().setIndex(ni);
} catch {}
}, []);
const onPrev = useCallback(() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
try {
if (progress > 3) { seekTo(0); return; }
const pi = usePlayer.getState().prevTrack();
usePlayer.getState().setIndex(pi);
} catch {}
}, [progress]);
const onCopyTitle = useCallback(() => {
try {
Clipboard.setStringAsync(`${track?.artist} — ${track?.title}`);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
} catch {}
}, [track]);
// Horizontal swipe on cover → next/prev track
const coverGesture = useAnimatedGestureHandler<any, { startX: number }>({
onStart: (_, ctx) => { ctx.startX = coverTranslateX.value; },
onActive: (e, ctx) => {
onActive: (e) => {
if (Math.abs(e.translationY) > Math.abs(e.translationX) * 1.5) return;
coverTranslateX.value = e.translationX * 0.4;
},
@ -213,24 +242,6 @@ export default function Player() {
onCancel: () => { coverTranslateX.value = withSpring(0, { damping: 20 }); },
});
const onNext = useCallback(() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
const ni = usePlayer.getState().nextTrack();
usePlayer.getState().setIndex(ni);
}, []);
const onPrev = useCallback(() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
if (progress > 3) { seekTo(0); return; }
const pi = usePlayer.getState().prevTrack();
usePlayer.getState().setIndex(pi);
}, [progress]);
const onCopyTitle = useCallback(() => {
Clipboard.setStringAsync(`${track?.artist} — ${track?.title}`);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}, [track]);
if (!track) return null;
// cover is already declared above via dynColor
@ -243,12 +254,16 @@ export default function Player() {
<StatusBar barStyle="light-content" />
{/* Blurred background */}
<Image
source={cover ? { uri: cover } : require('../assets/placeholder.png')}
style={StyleSheet.absoluteFill}
contentFit="cover"
blurRadius={80}
/>
{cover ? (
<Image
source={{ uri: cover }}
style={StyleSheet.absoluteFill}
contentFit="cover"
blurRadius={80}
/>
) : (
<View style={[StyleSheet.absoluteFill, { backgroundColor: '#0a0a1a' }]} />
)}
{/* Dynamic color background overlay */}
<View style={[StyleSheet.absoluteFill, s.bgDim]} />
<View style={[StyleSheet.absoluteFill, { backgroundColor: dynColor, opacity: 0.35 }]} />

View file

@ -1,14 +1,10 @@
/**
* ShareTrackModal — share sheet + QR for co-listening (deep link, no backend)
* Co-listening: generates a deep link burmalda://listen?tid=123&ts=456
* Friend opens it → app seeks to same position and plays same track
* ShareTrackModal — share + QR co-listening (no backend)
*/
import { useState } from 'react';
import { View, Text, StyleSheet, Pressable, Modal, Alert } from 'react-native';
import * as Sharing from 'expo-sharing';
import { View, Text, StyleSheet, Pressable, Modal, Alert, Share, Platform } from 'react-native';
import * as Clipboard from 'expo-clipboard';
import * as Haptics from 'expo-haptics';
import QRCode from 'react-native-qrcode-svg';
import { BlurView } from 'expo-blur';
import { Image } from 'expo-image';
import Svg, { Path } from 'react-native-svg';
@ -33,23 +29,18 @@ export default function ShareTrackModal({ track, progress, visible, onClose }: P
const shareText = `${track.artist} — ${track.title}\n${deepLink}`;
const handleShare = async () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
try {
if (await Sharing.isAvailableAsync()) {
await Sharing.shareAsync(deepLink, {
dialogTitle: `${track.artist} — ${track.title}`,
});
} else {
await Clipboard.setStringAsync(shareText);
Alert.alert('Скопировано', 'Ссылка скопирована в буфер');
}
await Share.share({ message: shareText, title: `${track.artist} — ${track.title}` });
} catch {}
};
const handleCopy = async () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
await Clipboard.setStringAsync(deepLink);
Alert.alert('Скопировано', 'Ссылка скопирована');
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
try {
await Clipboard.setStringAsync(deepLink);
Alert.alert('Скопировано', 'Ссылка скопирована');
} catch {}
};
return (
@ -58,7 +49,6 @@ export default function ShareTrackModal({ track, progress, visible, onClose }: P
<BlurView intensity={40} tint="dark" style={s.sheet}>
<View style={s.handle} />
{/* Track info */}
<View style={s.trackRow}>
{cover
? <Image source={{ uri: cover }} style={s.cover} contentFit="cover" />
@ -70,41 +60,25 @@ export default function ShareTrackModal({ track, progress, visible, onClose }: P
</View>
</View>
{/* QR toggle */}
{showQR && (
<View style={s.qrWrap}>
<QRCode
value={deepLink}
size={180}
backgroundColor="transparent"
color={Colors.text}
/>
<Text style={s.qrHint}>Друг сканирует — сразу переходит к этому месту</Text>
</View>
)}
{/* Actions */}
<View style={s.actions}>
<ActionBtn
icon="share"
label="Поделиться"
onPress={handleShare}
/>
<ActionBtn
icon="copy"
label="Скопировать ссылку"
onPress={handleCopy}
/>
<ActionBtn
icon="qr"
label={showQR ? 'Скрыть QR' : 'QR-код'}
onPress={() => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); setShowQR(v => !v); }}
active={showQR}
/>
<Pressable style={s.actionBtn} onPress={handleShare}>
<Svg width={20} height={20} viewBox="0 0 24 24" fill="none">
<Path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8M16 6l-4-4-4 4M12 2v13"
stroke={Colors.text} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
</Svg>
<Text style={s.actionLabel}>Поделиться</Text>
</Pressable>
<Pressable style={s.actionBtn} onPress={handleCopy}>
<Svg width={20} height={20} viewBox="0 0 24 24" fill="none">
<Path d="M8 16H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2M16 8h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-8a2 2 0 0 1-2-2v-2"
stroke={Colors.text} strokeWidth={2} strokeLinecap="round" />
</Svg>
<Text style={s.actionLabel}>Копировать</Text>
</Pressable>
</View>
<Text style={s.hint}>
Совместное прослушивание: друг откроет ссылку и приложение перейдёт к этому треку и позиции
Ссылка: {deepLink}
</Text>
<Pressable style={s.closeBtn} onPress={onClose}>
@ -115,49 +89,18 @@ export default function ShareTrackModal({ track, progress, visible, onClose }: P
);
}
function ActionBtn({ icon, label, onPress, active = false }: {
icon: 'share' | 'copy' | 'qr'; label: string; onPress: () => void; active?: boolean;
}) {
return (
<Pressable onPress={onPress} style={[s.actionBtn, active && s.actionBtnActive]}>
<View style={s.actionIcon}>
{icon === 'share' && (
<Svg width={20} height={20} viewBox="0 0 24 24" fill="none">
<Path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8M16 6l-4-4-4 4M12 2v13" stroke={active ? Colors.accent : Colors.text} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
</Svg>
)}
{icon === 'copy' && (
<Svg width={20} height={20} viewBox="0 0 24 24" fill="none">
<Path d="M8 16H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2M16 8h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-8a2 2 0 0 1-2-2v-2" stroke={active ? Colors.accent : Colors.text} strokeWidth={2} strokeLinecap="round" />
</Svg>
)}
{icon === 'qr' && (
<Svg width={20} height={20} viewBox="0 0 24 24" fill="none">
<Path d="M3 3h6v6H3zM15 3h6v6h-6zM3 15h6v6H3zM15 15h2v2h-2zM19 15h2v2h-2zM17 17h2v2h-2zM15 19h2v2h-2zM19 19h2v2h-2z" stroke={active ? Colors.accent : Colors.text} strokeWidth={1.8} strokeLinecap="round" />
</Svg>
)}
</View>
<Text style={[s.actionLabel, active && { color: Colors.accent }]}>{label}</Text>
</Pressable>
);
}
const s = StyleSheet.create({
backdrop: { flex: 1 },
sheet: { borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: 24, paddingBottom: 40, overflow: 'hidden', borderTopWidth: 1, borderColor: 'rgba(255,255,255,0.08)' },
handle: { width: 36, height: 4, borderRadius: 2, backgroundColor: 'rgba(255,255,255,0.3)', alignSelf: 'center', marginBottom: 20 },
trackRow: { flexDirection: 'row', alignItems: 'center', gap: 14, marginBottom: 24 },
cover: { width: 52, height: 52, borderRadius: Radius.sm },
title: { color: Colors.text, fontSize: Font.md, fontWeight: '700' },
artist: { color: 'rgba(255,255,255,0.55)', fontSize: Font.sm, marginTop: 2 },
qrWrap: { alignItems: 'center', marginBottom: 24, padding: 20, backgroundColor: 'rgba(255,255,255,0.06)', borderRadius: 16 },
qrHint: { color: 'rgba(255,255,255,0.45)', fontSize: 12, marginTop: 12, textAlign: 'center' },
actions: { flexDirection: 'row', gap: 10, marginBottom: 16 },
actionBtn: { flex: 1, alignItems: 'center', gap: 8, backgroundColor: 'rgba(255,255,255,0.08)', borderRadius: 14, paddingVertical: 14, paddingHorizontal: 8 },
actionBtnActive: { backgroundColor: `${Colors.accent}20`, borderWidth: 1, borderColor: `${Colors.accent}50` },
actionIcon: { width: 36, height: 36, alignItems: 'center', justifyContent: 'center' },
actionLabel: { color: Colors.text, fontSize: 11, fontWeight: '600', textAlign: 'center' },
hint: { color: 'rgba(255,255,255,0.3)', fontSize: 11, textAlign: 'center', lineHeight: 16, marginBottom: 20 },
closeBtn: { paddingVertical: 14, alignItems: 'center', backgroundColor: 'rgba(255,255,255,0.08)', borderRadius: 14 },
closeTxt: { color: Colors.text, fontSize: Font.md, fontWeight: '600' },
backdrop: { flex: 1 },
sheet: { borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: 24, paddingBottom: 40, overflow: 'hidden', borderTopWidth: 1, borderColor: 'rgba(255,255,255,0.08)' },
handle: { width: 36, height: 4, borderRadius: 2, backgroundColor: 'rgba(255,255,255,0.3)', alignSelf: 'center', marginBottom: 20 },
trackRow: { flexDirection: 'row', alignItems: 'center', gap: 14, marginBottom: 24 },
cover: { width: 52, height: 52, borderRadius: Radius.sm },
title: { color: Colors.text, fontSize: Font.md, fontWeight: '700' },
artist: { color: 'rgba(255,255,255,0.55)', fontSize: Font.sm, marginTop: 2 },
actions: { flexDirection: 'row', gap: 10, marginBottom: 16 },
actionBtn: { flex: 1, alignItems: 'center', gap: 8, backgroundColor: 'rgba(255,255,255,0.08)', borderRadius: 14, paddingVertical: 16 },
actionLabel: { color: Colors.text, fontSize: 12, fontWeight: '600' },
hint: { color: 'rgba(255,255,255,0.3)', fontSize: 10, textAlign: 'center', marginBottom: 20 },
closeBtn: { paddingVertical: 14, alignItems: 'center', backgroundColor: 'rgba(255,255,255,0.08)', borderRadius: 14 },
closeTxt: { color: Colors.text, fontSize: Font.md, fontWeight: '600' },
});