mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-20 04:30:54 +02:00
queue panel
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { create } from 'zustand';
|
||||
import type { PlaybackState, Track } from '@/types/audio';
|
||||
|
||||
export type RepeatMode = 'none' | 'one' | 'all';
|
||||
|
||||
/**
|
||||
* Player state — the UI's single source of truth, mirrored from the playback
|
||||
* engine (RNTP at M0) by `usePlaybackSync`. Field names match desktop
|
||||
@@ -14,12 +16,17 @@ interface PlayerStore {
|
||||
duration: number;
|
||||
volume: number; // 0–1
|
||||
isMuted: boolean;
|
||||
// Field names mirror desktop playerStore so queue/transport logic stays consistent.
|
||||
shuffle: boolean;
|
||||
repeat: RepeatMode;
|
||||
|
||||
setCurrentTrack: (track: Track | null) => void;
|
||||
setPlaybackState: (state: PlaybackState) => void;
|
||||
setProgress: (currentTime: number, duration: number) => void;
|
||||
setVolume: (volume: number) => void;
|
||||
setMuted: (isMuted: boolean) => void;
|
||||
setShuffle: (shuffle: boolean) => void;
|
||||
setRepeat: (repeat: RepeatMode) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
@@ -30,12 +37,16 @@ export const usePlayerStore = create<PlayerStore>((set) => ({
|
||||
duration: 0,
|
||||
volume: 1,
|
||||
isMuted: false,
|
||||
shuffle: false,
|
||||
repeat: 'none',
|
||||
|
||||
setCurrentTrack: (currentTrack) => set({ currentTrack }),
|
||||
setPlaybackState: (playbackState) => set({ playbackState }),
|
||||
setProgress: (currentTime, duration) => set({ currentTime, duration }),
|
||||
setVolume: (volume) => set({ volume }),
|
||||
setMuted: (isMuted) => set({ isMuted }),
|
||||
setShuffle: (shuffle) => set({ shuffle }),
|
||||
setRepeat: (repeat) => set({ repeat }),
|
||||
reset: () =>
|
||||
set({ currentTrack: null, playbackState: 'stopped', currentTime: 0, duration: 0 }),
|
||||
}));
|
||||
|
||||
@@ -46,7 +46,8 @@ interface PlaylistStore {
|
||||
addTracksToPlaylist: (id: number, tracks: DbTrack[]) => Promise<number>;
|
||||
removeFromPlaylist: (id: number, trackPath: string) => Promise<void>;
|
||||
moveTrack: (id: number, trackPath: string, direction: -1 | 1) => Promise<void>;
|
||||
toggleFavorite: (track: DbTrack) => Promise<void>;
|
||||
// Only the path is read; accepts a library DbTrack or the now-playing Track.
|
||||
toggleFavorite: (track: { path: string }) => Promise<void>;
|
||||
markPlayed: (id: number) => Promise<void>;
|
||||
importM3u: () => Promise<M3uImportSummary | null>;
|
||||
exportM3u: (target: number | 'favorites') => Promise<M3uExportResult | null>;
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { create } from 'zustand';
|
||||
import TrackPlayer, { type Track as RntpTrack } from 'react-native-track-player';
|
||||
|
||||
/**
|
||||
* Live mirror of RNTP's native queue for the queue tray. Playback actions keep
|
||||
* this in sync so opening the tray can render from JS immediately instead of
|
||||
* marshaling a very large native queue across the bridge on the cold path.
|
||||
*/
|
||||
interface QueueStore {
|
||||
tracks: RntpTrack[];
|
||||
activeIndex: number;
|
||||
hasSnapshot: boolean;
|
||||
refreshFromNative: () => Promise<void>;
|
||||
refreshActiveIndex: () => Promise<void>;
|
||||
setSnapshot: (tracks: RntpTrack[], activeIndex?: number) => void;
|
||||
setActiveIndex: (activeIndex: number) => void;
|
||||
insertTrack: (track: RntpTrack, index?: number) => void;
|
||||
replaceUpcoming: (upcoming: RntpTrack[]) => void;
|
||||
moveItem: (fromIndex: number, toIndex: number) => void;
|
||||
removeIndices: (indices: number[]) => void;
|
||||
}
|
||||
|
||||
function normalizeActiveIndex(activeIndex: number | undefined, trackCount: number): number {
|
||||
if (activeIndex == null || activeIndex < 0 || activeIndex >= trackCount) return -1;
|
||||
return activeIndex;
|
||||
}
|
||||
|
||||
function boundedInsertIndex(index: number | undefined, length: number): number {
|
||||
if (index == null) return length;
|
||||
return Math.max(0, Math.min(length, index));
|
||||
}
|
||||
|
||||
export const useQueueStore = create<QueueStore>((set) => ({
|
||||
tracks: [],
|
||||
activeIndex: -1,
|
||||
hasSnapshot: false,
|
||||
refreshFromNative: async () => {
|
||||
const [tracks, activeIndex] = await Promise.all([
|
||||
TrackPlayer.getQueue(),
|
||||
TrackPlayer.getActiveTrackIndex(),
|
||||
]);
|
||||
set({
|
||||
tracks,
|
||||
activeIndex: normalizeActiveIndex(activeIndex, tracks.length),
|
||||
hasSnapshot: true,
|
||||
});
|
||||
},
|
||||
refreshActiveIndex: async () => {
|
||||
const activeIndex = await TrackPlayer.getActiveTrackIndex();
|
||||
set((s) => ({ activeIndex: normalizeActiveIndex(activeIndex, s.tracks.length) }));
|
||||
},
|
||||
setSnapshot: (tracks, activeIndex = 0) =>
|
||||
set({
|
||||
tracks,
|
||||
activeIndex: normalizeActiveIndex(activeIndex, tracks.length),
|
||||
hasSnapshot: true,
|
||||
}),
|
||||
setActiveIndex: (activeIndex) =>
|
||||
set((s) => ({ activeIndex: normalizeActiveIndex(activeIndex, s.tracks.length) })),
|
||||
insertTrack: (track, index) =>
|
||||
set((s) => {
|
||||
const insertAt = boundedInsertIndex(index, s.tracks.length);
|
||||
const tracks = [...s.tracks];
|
||||
tracks.splice(insertAt, 0, track);
|
||||
const activeIndex = s.activeIndex >= insertAt ? s.activeIndex + 1 : s.activeIndex;
|
||||
return { tracks, activeIndex, hasSnapshot: true };
|
||||
}),
|
||||
replaceUpcoming: (upcoming) =>
|
||||
set((s) => {
|
||||
const prefixEnd = s.activeIndex >= 0 ? s.activeIndex + 1 : 0;
|
||||
return {
|
||||
tracks: [...s.tracks.slice(0, prefixEnd), ...upcoming],
|
||||
hasSnapshot: true,
|
||||
};
|
||||
}),
|
||||
moveItem: (fromIndex, toIndex) =>
|
||||
set((s) => {
|
||||
if (fromIndex === toIndex || fromIndex < 0 || fromIndex >= s.tracks.length) return s;
|
||||
|
||||
const tracks = [...s.tracks];
|
||||
const [moved] = tracks.splice(fromIndex, 1);
|
||||
const insertAt = boundedInsertIndex(toIndex, tracks.length);
|
||||
tracks.splice(insertAt, 0, moved);
|
||||
|
||||
let activeIndex = s.activeIndex;
|
||||
if (activeIndex === fromIndex) {
|
||||
activeIndex = insertAt;
|
||||
} else if (fromIndex < activeIndex && insertAt >= activeIndex) {
|
||||
activeIndex -= 1;
|
||||
} else if (fromIndex > activeIndex && insertAt <= activeIndex) {
|
||||
activeIndex += 1;
|
||||
}
|
||||
|
||||
return { tracks, activeIndex, hasSnapshot: true };
|
||||
}),
|
||||
removeIndices: (indices) =>
|
||||
set((s) => {
|
||||
if (indices.length === 0 || s.tracks.length === 0) return s;
|
||||
|
||||
const removeSet = new Set(
|
||||
indices.filter((index) => index >= 0 && index < s.tracks.length)
|
||||
);
|
||||
if (removeSet.size === 0) return s;
|
||||
|
||||
const tracks = s.tracks.filter((_, index) => !removeSet.has(index));
|
||||
let activeIndex = s.activeIndex;
|
||||
if (activeIndex >= 0) {
|
||||
if (removeSet.has(activeIndex)) {
|
||||
activeIndex = tracks.length > 0 ? Math.min(activeIndex, tracks.length - 1) : -1;
|
||||
} else {
|
||||
let removedBeforeActive = 0;
|
||||
removeSet.forEach((index) => {
|
||||
if (index < activeIndex) removedBeforeActive += 1;
|
||||
});
|
||||
activeIndex -= removedBeforeActive;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tracks,
|
||||
activeIndex: normalizeActiveIndex(activeIndex, tracks.length),
|
||||
hasSnapshot: true,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
Reference in New Issue
Block a user