Add files via upload
This commit is contained in:
parent
d612a65f81
commit
5748d2d13c
31 changed files with 3197 additions and 2492 deletions
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
116
hooks/useAuth.ts
116
hooks/useAuth.ts
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue