Files
furumi-ng/furumi-node-player/client/src/store/slices/queueSlice.ts
T

275 lines
7.1 KiB
TypeScript
Raw Normal View History

2026-04-02 00:13:30 +03:00
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<number>) {
const i = action.payload
if (i < 0 || i >= state.items.length) return
state.currentIndex = i
state.scrollSignal += 1
},
removeFromQueueAt(state, action: PayloadAction<number>) {
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
}
2026-04-04 19:17:33 +03:00
// TODO: toggle shuffle should rebuild the shuffle order
2026-04-02 00:13:30 +03:00
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