import { createSlice, type PayloadAction } from '@reduxjs/toolkit' import type { QueueItem } from '../../components/QueueList' export interface QueueState { items: QueueItem[] currentIndex: number shuffle: boolean repeatAll: boolean shuffleOrder: number[] scrollSignal: number } function readShufflePref(): boolean { try { return window.localStorage.getItem('furumi_shuffle') === '1' } catch { return false } } function readRepeatPref(): boolean { try { return window.localStorage.getItem('furumi_repeat') !== '0' } catch { return true } } function buildShuffleOrder(state: QueueState) { const n = state.items.length if (n === 0) { state.shuffleOrder = [] return } const order = [...Array(n).keys()] for (let i = order.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)) ;[order[i], order[j]] = [order[j], order[i]] } if (state.currentIndex !== -1) { const ci = order.indexOf(state.currentIndex) if (ci > 0) { order.splice(ci, 1) order.unshift(state.currentIndex) } } state.shuffleOrder = order } function ensureShuffleOrder(state: QueueState) { if (!state.shuffle) return if (state.shuffleOrder.length !== state.items.length) { buildShuffleOrder(state) } } const initialState: QueueState = { items: [], currentIndex: -1, shuffle: typeof window !== 'undefined' ? readShufflePref() : false, repeatAll: typeof window !== 'undefined' ? readRepeatPref() : true, shuffleOrder: [], scrollSignal: 0, } const queueSlice = createSlice({ name: 'queue', initialState, reducers: { addTrack( state, action: PayloadAction<{ track: QueueItem playNow?: boolean }>, ) { const { track, playNow } = action.payload const existing = state.items.findIndex((t) => t.slug === track.slug) if (existing !== -1) { if (playNow) { state.currentIndex = existing state.scrollSignal += 1 } return } const oldLen = state.items.length const idle = state.currentIndex === -1 state.items.push(track) ensureShuffleOrder(state) if (playNow || (oldLen === 0 && idle)) { state.currentIndex = state.items.length - 1 state.scrollSignal += 1 } }, addTracksBatch( state, action: PayloadAction<{ tracks: QueueItem[] playFirst?: boolean }>, ) { const { tracks, playFirst } = action.payload let firstNewIdx: number | null = null for (const t of tracks) { if (state.items.some((q) => q.slug === t.slug)) continue if (firstNewIdx === null) firstNewIdx = state.items.length state.items.push(t) } ensureShuffleOrder(state) if (firstNewIdx === null) return if (playFirst || state.currentIndex === -1) { state.currentIndex = firstNewIdx state.scrollSignal += 1 } }, replaceQueue( state, action: PayloadAction<{ items: QueueItem[] playFromIndex?: number }>, ) { const { items, playFromIndex = 0 } = action.payload state.items = items state.currentIndex = items.length > 0 ? Math.min(playFromIndex, items.length - 1) : -1 state.shuffleOrder = [] ensureShuffleOrder(state) }, clearQueue(state) { state.items = [] state.currentIndex = -1 state.shuffleOrder = [] state.scrollSignal += 1 }, playAtIndex(state, action: PayloadAction) { const i = action.payload if (i < 0 || i >= state.items.length) return state.currentIndex = i state.scrollSignal += 1 }, removeFromQueueAt(state, action: PayloadAction) { const idx = action.payload if (idx < 0 || idx >= state.items.length) return if (idx === state.currentIndex) { state.currentIndex = -1 } else if (state.currentIndex > idx) { state.currentIndex -= 1 } state.items.splice(idx, 1) if (state.shuffle) { const si = state.shuffleOrder.indexOf(idx) if (si !== -1) state.shuffleOrder.splice(si, 1) for (let i = 0; i < state.shuffleOrder.length; i++) { if (state.shuffleOrder[i] > idx) state.shuffleOrder[i] -= 1 } } ensureShuffleOrder(state) }, moveQueueItemInOrder( state, action: PayloadAction<{ fromPos: number; toPos: number }>, ) { const { fromPos, toPos } = action.payload if (fromPos === toPos) return if (state.shuffle) { const order = state.shuffleOrder if (fromPos < 0 || fromPos >= order.length) return if (toPos < 0 || toPos >= order.length) return const item = order.splice(fromPos, 1)[0] order.splice(toPos, 0, item) return } const items = state.items if (fromPos < 0 || fromPos >= items.length) return if (toPos < 0 || toPos >= items.length) return const qIdx = state.currentIndex const item = items.splice(fromPos, 1)[0] items.splice(toPos, 0, item) if (qIdx === fromPos) state.currentIndex = toPos else if (fromPos < qIdx && toPos >= qIdx) state.currentIndex -= 1 else if (fromPos > qIdx && toPos <= qIdx) state.currentIndex += 1 }, toggleShuffle(state) { state.shuffle = !state.shuffle try { window.localStorage.setItem('furumi_shuffle', state.shuffle ? '1' : '0') } catch { // ignore } if (state.shuffle) buildShuffleOrder(state) else state.shuffleOrder = [] }, toggleRepeat(state) { state.repeatAll = !state.repeatAll try { window.localStorage.setItem('furumi_repeat', state.repeatAll ? '1' : '0') } catch { // ignore } }, rebuildShuffleOrder(state) { if (state.shuffle) buildShuffleOrder(state) }, }, }) export const { addTrack, addTracksBatch, replaceQueue, clearQueue, playAtIndex, removeFromQueueAt, moveQueueItemInOrder, toggleShuffle, toggleRepeat, rebuildShuffleOrder, } = queueSlice.actions type QueueSliceRoot = { queue: QueueState } export function selectQueueItems(state: QueueSliceRoot) { return state.queue.items } // TODO: toggle shuffle should rebuild the shuffle order export function selectQueueOrder(state: QueueSliceRoot): number[] { const q = state.queue if (!q.shuffle) return q.items.map((_, i) => i) if (q.shuffleOrder.length !== q.items.length) { return q.items.map((_, i) => i) } return q.shuffleOrder } export function selectPlayingOrigIdx(state: QueueSliceRoot) { return state.queue.currentIndex } export function selectQueueScrollSignal(state: QueueSliceRoot) { return state.queue.scrollSignal } export function selectNowPlayingTrack(state: QueueSliceRoot): QueueItem | null { const q = state.queue if (q.currentIndex < 0 || q.currentIndex >= q.items.length) return null return q.items[q.currentIndex] } export function selectShuffle(state: QueueSliceRoot) { return state.queue.shuffle } export function selectRepeatAll(state: QueueSliceRoot) { return state.queue.repeatAll } export default queueSlice.reducer