Add files via upload

This commit is contained in:
Savsis 2026-05-05 18:17:09 +05:00 • committed by GitHub
parent d612a65f81
commit 5748d2d13c
31 changed files with 3197 additions and 2492 deletions

View file

@ -1,55 +1,55 @@
import TrackPlayer, { Event } from 'react-native-track-player';
import { usePlayer } from '../store';
import { loadAndPlay } from './useAudio';
export async function PlaybackService() {
TrackPlayer.addEventListener(Event.RemotePlay, () => TrackPlayer.play());
TrackPlayer.addEventListener(Event.RemotePause, () => TrackPlayer.pause());
TrackPlayer.addEventListener(Event.RemoteNext, async () => {
const state = usePlayer.getState();
const ni = state.nextTrack();
state.setIndex(ni);
const track = state.queue[ni];
if (track) await loadAndPlay(track).catch(console.error);
});
TrackPlayer.addEventListener(Event.RemotePrevious, async () => {
const state = usePlayer.getState();
const progress = await TrackPlayer.getProgress();
if (progress.position > 3) {
await TrackPlayer.seekTo(0);
return;
}
const pi = state.prevTrack();
state.setIndex(pi);
const track = state.queue[pi];
if (track) await loadAndPlay(track).catch(console.error);
});
TrackPlayer.addEventListener(Event.RemoteSeek, ({ position }) =>
TrackPlayer.seekTo(position),
);
TrackPlayer.addEventListener(Event.PlaybackQueueEnded, async () => {
const state = usePlayer.getState();
const prevIdx = state.index;
if (state.repeat === 2) {
await TrackPlayer.seekTo(0);
await TrackPlayer.play();
return;
}
const ni = state.nextTrack();
state.setIndex(ni);
if (ni === prevIdx) {
state.setPlaying(false);
return;
}
const track = state.queue[ni];
if (track) await loadAndPlay(track).catch(console.error);
});
}
import TrackPlayer, { Event } from 'react-native-track-player';
import { usePlayer } from '../store';
import { loadAndPlay } from './useAudio';
export async function PlaybackService() {
TrackPlayer.addEventListener(Event.RemotePlay, () => TrackPlayer.play());
TrackPlayer.addEventListener(Event.RemotePause, () => TrackPlayer.pause());
TrackPlayer.addEventListener(Event.RemoteNext, async () => {
const state = usePlayer.getState();
const ni = state.nextTrack();
state.setIndex(ni);
const track = state.queue[ni];
if (track) await loadAndPlay(track).catch(console.error);
});
TrackPlayer.addEventListener(Event.RemotePrevious, async () => {
const state = usePlayer.getState();
const progress = await TrackPlayer.getProgress();
if (progress.position > 3) {
await TrackPlayer.seekTo(0);
return;
}
const pi = state.prevTrack();
state.setIndex(pi);
const track = state.queue[pi];
if (track) await loadAndPlay(track).catch(console.error);
});
TrackPlayer.addEventListener(Event.RemoteSeek, ({ position }) =>
TrackPlayer.seekTo(position),
);
TrackPlayer.addEventListener(Event.PlaybackQueueEnded, async () => {
const state = usePlayer.getState();
const prevIdx = state.index;
if (state.repeat === 2) {
await TrackPlayer.seekTo(0);
await TrackPlayer.play();
return;
}
const ni = state.nextTrack();
state.setIndex(ni);
if (ni === prevIdx) {
state.setPlaying(false);
return;
}
const track = state.queue[ni];
if (track) await loadAndPlay(track).catch(console.error);
});
}

View file

@ -1,106 +1,108 @@
import { useEffect } from 'react';
import TrackPlayer, {
State,
Capability,
AppKilledPlaybackBehavior,
usePlaybackState,
useProgress,
} from 'react-native-track-player';
import { usePlayer } from '../store';
import { coverUrl, Track } from '../services/api';
let _setupDone = false;
let _lastLoadedId: number | null = null;
async function ensureSetup() {
if (_setupDone) return;
_setupDone = true;
try {
await TrackPlayer.setupPlayer({ minBuffer: 15, maxBuffer: 50, playBuffer: 2.5 });
await TrackPlayer.updateOptions({
android: {
appKilledPlaybackBehavior:
AppKilledPlaybackBehavior.StopPlaybackAndRemoveNotification,
},
capabilities: [
Capability.Play,
Capability.Pause,
Capability.SkipToNext,
Capability.SkipToPrevious,
Capability.SeekTo,
],
compactCapabilities: [Capability.Play, Capability.Pause, Capability.SkipToNext],
});
} catch (e: any) {
// Player already set up on hot reload — update options only
if (e?.message?.toLowerCase().includes('already')) {
_setupDone = true;
return;
}
_setupDone = false;
throw e;
}
}
// Load a track and start playing. Mutex prevents double-load on the same ID.
export async function loadAndPlay(track: Track) {
if (_lastLoadedId === track.id) return;
_lastLoadedId = track.id;
try {
await TrackPlayer.reset();
await TrackPlayer.add({
id: String(track.id),
url: track.cdn_url2 || track.cdn_url,
title: track.title,
artist: track.artist,
artwork: coverUrl(track.cover_url) ?? undefined,
duration: track.duration,
});
await TrackPlayer.play();
} catch (e) {
_lastLoadedId = null;
throw e;
}
}
export async function togglePlay() {
const { state } = await TrackPlayer.getPlaybackState();
if (state === State.Playing) await TrackPlayer.pause();
else await TrackPlayer.play();
}
export async function seekTo(seconds: number) {
await TrackPlayer.seekTo(seconds);
}
// Call ONCE from the root layout only
export function useAudio() {
const { queue, index, setPlaying, setProgress, setDuration } = usePlayer();
const currentTrack = queue[index] ?? null;
const playbackState = usePlaybackState();
const progress = useProgress(500);
// One-time player setup
useEffect(() => {
ensureSetup().catch(console.error);
}, []);
// Sync TrackPlayer progress → store
useEffect(() => {
setProgress(progress.position);
setDuration(progress.duration);
}, [progress.position, progress.duration]);
// Sync TrackPlayer playing state → store
useEffect(() => {
setPlaying(playbackState.state === State.Playing);
}, [playbackState.state]);
// Load new track when queue index changes
useEffect(() => {
if (!currentTrack) return;
loadAndPlay(currentTrack).catch(console.error);
}, [currentTrack?.id]);
return { togglePlay, seek: seekTo };
}
import { useEffect } from 'react';
import TrackPlayer, {
State,
Capability,
AppKilledPlaybackBehavior,
usePlaybackState,
useProgress,
} from 'react-native-track-player';
import { usePlayer } from '../store';
import { coverUrl, Track, getTrackUrl } from '../services/api';
let _setupDone = false;
let _lastLoadedId: number | null = null;
async function ensureSetup() {
if (_setupDone) return;
_setupDone = true;
try {
await TrackPlayer.setupPlayer({ minBuffer: 15, maxBuffer: 50, playBuffer: 2.5 });
await TrackPlayer.updateOptions({
android: {
appKilledPlaybackBehavior:
AppKilledPlaybackBehavior.StopPlaybackAndRemoveNotification,
},
capabilities: [
Capability.Play,
Capability.Pause,
Capability.SkipToNext,
Capability.SkipToPrevious,
Capability.SeekTo,
],
compactCapabilities: [Capability.Play, Capability.Pause, Capability.SkipToNext],
});
} catch (e: any) {
// Player already set up on hot reload — update options only
if (e?.message?.toLowerCase().includes('already')) {
_setupDone = true;
return;
}
_setupDone = false;
throw e;
}
}
// Load a track and start playing. Mutex prevents double-load on the same ID.
// Uses local file if downloaded, otherwise streams from CDN
export async function loadAndPlay(track: Track) {
if (_lastLoadedId === track.id) return;
_lastLoadedId = track.id;
try {
const url = await getTrackUrl(track);
await TrackPlayer.reset();
await TrackPlayer.add({
id: String(track.id),
url: url || (track.cdn_url2 || track.cdn_url),
title: track.title,
artist: track.artist,
artwork: coverUrl(track.cover_url) ?? undefined,
duration: track.duration,
});
await TrackPlayer.play();
} catch (e) {
_lastLoadedId = null;
throw e;
}
}
export async function togglePlay() {
const { state } = await TrackPlayer.getPlaybackState();
if (state === State.Playing) await TrackPlayer.pause();
else await TrackPlayer.play();
}
export async function seekTo(seconds: number) {
await TrackPlayer.seekTo(seconds);
}
// Call ONCE from the root layout only
export function useAudio() {
const { queue, index, setPlaying, setProgress, setDuration } = usePlayer();
const currentTrack = queue[index] ?? null;
const playbackState = usePlaybackState();
const progress = useProgress(500);
// One-time player setup
useEffect(() => {
ensureSetup().catch(console.error);
}, []);
// Sync TrackPlayer progress → store
useEffect(() => {
setProgress(progress.position);
setDuration(progress.duration);
}, [progress.position, progress.duration]);
// Sync TrackPlayer playing state → store
useEffect(() => {
setPlaying(playbackState.state === State.Playing);
}, [playbackState.state]);
// Load new track when queue index changes
useEffect(() => {
if (!currentTrack) return;
loadAndPlay(currentTrack).catch(console.error);
}, [currentTrack?.id]);
return { togglePlay, seek: seekTo };
}

View file

@ -1,58 +1,58 @@
import { useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { getAccessToken, getMe, clearTokens, User } from '../services/api';
import { useAuth } from '../store';
const USER_KEY = 'ns-music-user';
export async function saveUser(user: User): Promise<void> {
await AsyncStorage.setItem(USER_KEY, JSON.stringify(user));
}
export async function loadCachedUser(): Promise<User | null> {
try {
const raw = await AsyncStorage.getItem(USER_KEY);
return raw ? JSON.parse(raw) : null;
} catch { return null; }
}
export function useBootAuth() {
const { setUser, setReady } = useAuth();
useEffect(() => {
(async () => {
try {
const token = await getAccessToken();
if (!token) { setReady(true); return; }
// Use cached user for immediate display while validating
const cached = await loadCachedUser();
if (cached) setUser(cached);
// Validate token and get fresh data
const me = await getMe();
if (me) {
// Preserve plan from initial login since no endpoint exposes it
const merged: User = { ...me, plan: cached?.plan || me.plan };
setUser(merged);
saveUser(merged).catch(() => {});
} else {
// Token invalid — clear everything
await clearTokens();
await AsyncStorage.removeItem(USER_KEY);
setUser(null);
}
} catch {
// Network error — cached user already set above, just mark ready
} finally {
setReady(true);
}
})();
}, []);
}
export async function logout() {
await clearTokens();
await AsyncStorage.removeItem(USER_KEY).catch(() => {});
useAuth.getState().setUser(null);
}
import { useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { getAccessToken, getMe, clearTokens, User } from '../services/api';
import { useAuth } from '../store';
const USER_KEY = 'ns-music-user';
export async function saveUser(user: User): Promise<void> {
await AsyncStorage.setItem(USER_KEY, JSON.stringify(user));
}
export async function loadCachedUser(): Promise<User | null> {
try {
const raw = await AsyncStorage.getItem(USER_KEY);
return raw ? JSON.parse(raw) : null;
} catch { return null; }
}
export function useBootAuth() {
const { setUser, setReady } = useAuth();
useEffect(() => {
(async () => {
try {
const token = await getAccessToken();
if (!token) { setReady(true); return; }
// Use cached user for immediate display while validating
const cached = await loadCachedUser();
if (cached) setUser(cached);
// Validate token and get fresh data
const me = await getMe();
if (me) {
// Preserve plan from initial login since no endpoint exposes it
const merged: User = { ...me, plan: cached?.plan || me.plan };
setUser(merged);
saveUser(merged).catch(() => {});
} else {
// Token invalid — clear everything
await clearTokens();
await AsyncStorage.removeItem(USER_KEY);
setUser(null);
}
} catch {
// Network error — cached user already set above, just mark ready
} finally {
setReady(true);
}
})();
}, []);
}
export async function logout() {
await clearTokens();
await AsyncStorage.removeItem(USER_KEY).catch(() => {});
useAuth.getState().setUser(null);
}