275 lines
7.1 KiB
TypeScript
275 lines
7.1 KiB
TypeScript
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
|
|
}
|
|
|
|
// 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
|