feat: work with order in state

This commit is contained in:
Boris Cherepanov
2026-04-02 00:13:30 +03:00
parent 8ceee6028a
commit 83a145d0a8
4 changed files with 397 additions and 183 deletions
+121 -182
View File
@@ -5,17 +5,33 @@ import {
searchTracks,
preloadStream,
} from './furumiApi'
import { useAppDispatch, useAppSelector } from './store'
import { store, useAppDispatch, useAppSelector } from './store'
import { fetchArtists } from './store/slices/artistsSlice'
import { fetchArtistAlbums } from './store/slices/albumsSlice'
import { fetchArtistTracks } from './store/slices/artistTracksSlice'
import { fetchAlbumTracks } from './store/slices/albumTracksSlice'
import { fetchTrackDetail } from './store/slices/trackDetailSlice'
import {
addTrack,
addTracksBatch,
replaceQueue,
clearQueue,
playAtIndex,
removeFromQueueAt,
moveQueueItemInOrder,
toggleShuffle,
toggleRepeat,
rebuildShuffleOrder,
selectQueueOrder,
selectPlayingOrigIdx,
selectQueueScrollSignal,
selectNowPlayingTrack,
selectQueueItems,
} from './store/slices/queueSlice'
import { fmt } from './utils'
import { Header } from './components/Header'
import { MainPanel, type Crumb } from './components/MainPanel'
import { PlayerBar } from './components/PlayerBar'
import type { QueueItem } from './components/QueueList'
import type { Track } from './types'
export function FurumiPlayer() {
@@ -26,6 +42,13 @@ export function FurumiPlayer() {
const albumsError = useAppSelector((s) => s.albums.error)
const albumTracksLoading = useAppSelector((s) => s.albumTracks.loading)
const albumTracksError = useAppSelector((s) => s.albumTracks.error)
const queueItemsView = useAppSelector(selectQueueItems)
const queueOrderView = useAppSelector(selectQueueOrder)
const queuePlayingOrigIdxView = useAppSelector(selectPlayingOrigIdx)
const queueScrollSignal = useAppSelector(selectQueueScrollSignal)
const nowPlayingTrack = useAppSelector(selectNowPlayingTrack)
const [breadcrumbs, setBreadcrumbs] = useState<Crumb[]>(
[],
)
@@ -47,13 +70,6 @@ export function FurumiPlayer() {
const [searchOpen, setSearchOpen] = useState(false)
const searchSelectRef = useRef<(type: string, slug: string) => void>(() => {})
const [nowPlayingTrack, setNowPlayingTrack] = useState<QueueItem | null>(null)
const [queueItemsView, setQueueItemsView] = useState<QueueItem[]>([])
const [queueOrderView, setQueueOrderView] = useState<number[]>([])
const [queuePlayingOrigIdxView, setQueuePlayingOrigIdxView] = useState<number>(-1)
const [queueScrollSignal, setQueueScrollSignal] = useState(0)
const [queue, setQueue] = useState<QueueItem[]>([])
const queueActionsRef = useRef<{
playIndex: (i: number) => void
removeFromQueue: (idx: number) => void
@@ -63,20 +79,48 @@ export function FurumiPlayer() {
const audioRef = useRef<HTMLAudioElement>(null)
useEffect(() => {
// --- Original player script adapted for React environment ---
if (!nowPlayingTrack) {
document.title = 'Furumi Player'
return
}
document.title = `${nowPlayingTrack.title} — Furumi`
const coverUrl = `${API_ROOT}/tracks/${nowPlayingTrack.slug}/cover`
if ('mediaSession' in navigator) {
try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
navigator.mediaSession.metadata = new window.MediaMetadata({
title: nowPlayingTrack.title,
artist: nowPlayingTrack.artist || '',
album: '',
artwork: [{ src: coverUrl, sizes: '512x512' }],
})
} catch {
// ignore
}
}
}, [nowPlayingTrack])
const shuffle = useAppSelector((s) => s.queue.shuffle)
const repeatAll = useAppSelector((s) => s.queue.repeatAll)
useEffect(() => {
const btnShuffle = document.getElementById('btnShuffle')
const btnRepeat = document.getElementById('btnRepeat')
btnShuffle?.classList.toggle('active', shuffle)
btnRepeat?.classList.toggle('active', repeatAll)
}, [shuffle, repeatAll])
useEffect(() => {
const audioEl = audioRef.current
if (!audioEl) return
const audio = audioEl
let queueIndex = -1
let shuffle = false
let repeatAll = true
let shuffleOrder: number[] = []
let searchTimer: number | null = null
let toastTimer: number | null = null
let muted = false
let playbackErrorSkips = 0
const MAX_PLAYBACK_ERROR_SKIPS = 5
// Restore prefs
try {
const v = window.localStorage.getItem('furumi_vol')
const volSlider = document.getElementById('volSlider') as HTMLInputElement | null
@@ -84,17 +128,10 @@ export function FurumiPlayer() {
audio.volume = Number(v) / 100
volSlider.value = v
}
const btnShuffle = document.getElementById('btnShuffle')
const btnRepeat = document.getElementById('btnRepeat')
shuffle = window.localStorage.getItem('furumi_shuffle') === '1'
repeatAll = window.localStorage.getItem('furumi_repeat') !== '0'
btnShuffle?.classList.toggle('active', shuffle)
btnRepeat?.classList.toggle('active', repeatAll)
} catch {
// ignore
}
// --- Audio events ---
audio.addEventListener('timeupdate', () => {
if (audio.duration) {
const fill = document.getElementById('progressFill')
@@ -106,6 +143,9 @@ export function FurumiPlayer() {
}
})
audio.addEventListener('ended', () => nextTrack())
audio.addEventListener('playing', () => {
playbackErrorSkips = 0
})
audio.addEventListener('play', () => {
const btn = document.getElementById('btnPlayPause')
if (btn) btn.innerHTML = '&#9208;'
@@ -116,10 +156,11 @@ export function FurumiPlayer() {
})
audio.addEventListener('error', () => {
showToast('Playback error')
if (playbackErrorSkips >= MAX_PLAYBACK_ERROR_SKIPS) return
playbackErrorSkips += 1
nextTrack()
})
// --- Library navigation ---
async function showArtists() {
setBreadcrumb([{ label: 'Artists', action: showArtists }])
try {
@@ -231,7 +272,6 @@ export function FurumiPlayer() {
setBreadcrumbs(parts)
}
// --- Queue management ---
function addTrackToQueue(
track: {
slug: string
@@ -242,15 +282,11 @@ export function FurumiPlayer() {
},
playNow?: boolean,
) {
const existing = queue.findIndex((t) => t.slug === track.slug)
if (existing !== -1) {
if (playNow) playIndex(existing)
return
}
setQueue((q) => [...q, track]);
updateQueueModel()
if (playNow || (queueIndex === -1 && queue.length === 1)) {
playIndex(queue.length - 1)
const prevIdx = store.getState().queue.currentIndex
dispatch(addTrack({ track, playNow }))
const q = store.getState().queue
if (q.currentIndex !== -1 && (playNow || q.currentIndex !== prevIdx)) {
playIndex(q.currentIndex)
}
}
@@ -259,23 +295,19 @@ export function FurumiPlayer() {
if (result.meta.requestStatus === 'rejected') return
const { tracks } = result.payload as { albumSlug: string; tracks: Track[] }
if (!tracks || !tracks.length) return
const list = tracks
let firstIdx = queue.length
list.forEach((t) => {
if (queue.find((q) => q.slug === t.slug)) return
setQueue((q) => [
...q,
{
slug: t.slug,
title: t.title,
artist: t.artist_name,
album_slug: t.album_slug,
duration: t.duration_secs,
},
])
})
updateQueueModel()
if (playFirst || queueIndex === -1) playIndex(firstIdx)
const list = tracks.map((t) => ({
slug: t.slug,
title: t.title,
artist: t.artist_name,
album_slug: t.album_slug,
duration: t.duration_secs,
}))
const prevIdx = store.getState().queue.currentIndex
dispatch(addTracksBatch({ tracks: list, playFirst }))
const q = store.getState().queue
if (q.currentIndex !== -1 && q.currentIndex !== prevIdx) {
playIndex(q.currentIndex)
}
showToast(`Added ${list.length} tracks`)
}
@@ -284,29 +316,25 @@ export function FurumiPlayer() {
if (result.meta.requestStatus === 'rejected') return
const { tracks } = result.payload as { artistSlug: string; tracks: Track[] }
if (!tracks || !tracks.length) return
const list = tracks
clearQueue()
setQueue(list.map((t) => ({
const list = tracks.map((t) => ({
slug: t.slug,
title: t.title,
artist: t.artist_name,
album_slug: t.album_slug,
duration: t.duration_secs,
})))
updateQueueModel()
}))
dispatch(replaceQueue({ items: list, playFromIndex: 0 }))
playIndex(0)
showToast(`Added ${list.length} tracks`)
}
function playIndex(i: number) {
if (i < 0 || i >= queue.length) return
queueIndex = i
const track = queue[i]
const q = store.getState().queue
if (i < 0 || i >= q.items.length) return
dispatch(playAtIndex(i))
const track = store.getState().queue.items[i]
audio.src = `${API_ROOT}/stream/${track.slug}`
void audio.play().catch(() => {})
updateNowPlaying(track)
updateQueueModel()
setQueueScrollSignal((s) => s + 1)
if (window.history && window.history.replaceState) {
const url = new URL(window.location.href)
url.searchParams.set('t', track.slug)
@@ -314,88 +342,17 @@ export function FurumiPlayer() {
}
}
function updateNowPlaying(track: QueueItem | null) {
setNowPlayingTrack(track)
if (!track) return
document.title = `${track.title} — Furumi`
const coverUrl = `${API_ROOT}/tracks/${track.slug}/cover`
if ('mediaSession' in navigator) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
navigator.mediaSession.metadata = new window.MediaMetadata({
title: track.title,
artist: track.artist || '',
album: '',
artwork: [{ src: coverUrl, sizes: '512x512' }],
})
}
}
function currentOrder() {
if (!shuffle) return [...Array(queue.length).keys()]
if (shuffleOrder.length !== queue.length) buildShuffleOrder()
return shuffleOrder
}
function buildShuffleOrder() {
shuffleOrder = [...Array(queue.length).keys()]
for (let i = shuffleOrder.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[shuffleOrder[i], shuffleOrder[j]] = [shuffleOrder[j], shuffleOrder[i]]
}
if (queueIndex !== -1) {
const ci = shuffleOrder.indexOf(queueIndex)
if (ci > 0) {
shuffleOrder.splice(ci, 1)
shuffleOrder.unshift(queueIndex)
}
}
}
function updateQueueModel() {
const order = currentOrder()
setQueueItemsView(queue)
setQueueOrderView(order.slice())
setQueuePlayingOrigIdxView(queueIndex)
}
function removeFromQueue(idx: number) {
if (idx === queueIndex) {
queueIndex = -1
const wasPlaying = store.getState().queue.currentIndex === idx
dispatch(removeFromQueueAt(idx))
if (wasPlaying) {
audio.pause()
audio.src = ''
updateNowPlaying(null)
} else if (queueIndex > idx) {
queueIndex--
}
// queue.splice(idx, 1)
setQueue((q) => q.filter((_, i) => i !== idx));
if (shuffle) {
const si = shuffleOrder.indexOf(idx)
if (si !== -1) shuffleOrder.splice(si, 1)
for (let i = 0; i < shuffleOrder.length; i++) {
if (shuffleOrder[i] > idx) shuffleOrder[i]--
}
}
updateQueueModel()
}
function moveQueueItem(from: number, to: number) {
if (from === to) return
if (shuffle) {
const item = shuffleOrder.splice(from, 1)[0]
shuffleOrder.splice(to, 0, item)
} else {
const item = queue.splice(from, 1)[0]
queue.splice(to, 0, item)
if (queueIndex === from) queueIndex = to
else if (from < queueIndex && to >= queueIndex) queueIndex--
else if (from > queueIndex && to <= queueIndex) queueIndex++
}
updateQueueModel()
function moveQueueItem(fromPos: number, toPos: number) {
dispatch(moveQueueItemInOrder({ fromPos, toPos }))
}
queueActionsRef.current = {
@@ -404,21 +361,16 @@ export function FurumiPlayer() {
moveQueueItem,
}
function clearQueue() {
setQueue([]);
queueIndex = -1
shuffleOrder = []
function clearQueuePlayback() {
dispatch(clearQueue())
audio.pause()
audio.src = ''
updateNowPlaying(null)
document.title = 'Furumi Player'
updateQueueModel()
}
// --- Playback controls ---
function togglePlay() {
if (!audio.src && queue.length) {
playIndex(queueIndex === -1 ? 0 : queueIndex)
const q = store.getState().queue
if (!audio.src && q.items.length) {
playIndex(q.currentIndex === -1 ? 0 : q.currentIndex)
return
}
if (audio.paused) void audio.play()
@@ -426,45 +378,39 @@ export function FurumiPlayer() {
}
function nextTrack() {
if (!queue.length) return
const order = currentOrder()
const pos = order.indexOf(queueIndex)
const q = store.getState().queue
if (!q.items.length) return
const order = selectQueueOrder(store.getState())
const pos = order.indexOf(q.currentIndex)
if (pos < order.length - 1) playIndex(order[pos + 1])
else if (repeatAll) {
if (shuffle) buildShuffleOrder()
playIndex(currentOrder()[0])
else if (q.repeatAll) {
if (q.shuffle) dispatch(rebuildShuffleOrder())
const first = selectQueueOrder(store.getState())[0]
if (first !== undefined) playIndex(first)
}
}
function prevTrack() {
if (!queue.length) return
const q = store.getState().queue
if (!q.items.length) return
if (audio.currentTime > 3) {
audio.currentTime = 0
return
}
const order = currentOrder()
const pos = order.indexOf(queueIndex)
const order = selectQueueOrder(store.getState())
const pos = order.indexOf(q.currentIndex)
if (pos > 0) playIndex(order[pos - 1])
else if (repeatAll) playIndex(order[order.length - 1])
else if (q.repeatAll) playIndex(order[order.length - 1])
}
function toggleShuffle() {
shuffle = !shuffle
if (shuffle) buildShuffleOrder()
const btn = document.getElementById('btnShuffle')
btn?.classList.toggle('active', shuffle)
window.localStorage.setItem('furumi_shuffle', shuffle ? '1' : '0')
updateQueueModel()
function onToggleShuffle() {
dispatch(toggleShuffle())
}
function toggleRepeat() {
repeatAll = !repeatAll
const btn = document.getElementById('btnRepeat')
btn?.classList.toggle('active', repeatAll)
window.localStorage.setItem('furumi_repeat', repeatAll ? '1' : '0')
function onToggleRepeat() {
dispatch(toggleRepeat())
}
// --- Seek & Volume ---
function seekTo(e: MouseEvent) {
if (!audio.duration) return
const bar = document.getElementById('progressBar') as HTMLDivElement | null
@@ -488,7 +434,6 @@ export function FurumiPlayer() {
window.localStorage.setItem('furumi_vol', String(v))
}
// --- Search ---
function onSearch(q: string) {
if (searchTimer) {
window.clearTimeout(searchTimer)
@@ -527,7 +472,6 @@ export function FurumiPlayer() {
}
searchSelectRef.current = onSearchSelect
// --- Helpers ---
function showToast(msg: string) {
const t = document.getElementById('toast')
if (!t) return
@@ -544,7 +488,6 @@ export function FurumiPlayer() {
overlay?.classList.toggle('show')
}
// --- MediaSession ---
if ('mediaSession' in navigator) {
try {
navigator.mediaSession.setActionHandler('play', togglePlay)
@@ -561,7 +504,6 @@ export function FurumiPlayer() {
}
}
// --- Wire DOM events that were inline in HTML ---
const btnMenu = document.querySelector('.btn-menu')
btnMenu?.addEventListener('click', () => toggleSidebar())
@@ -579,11 +521,11 @@ export function FurumiPlayer() {
}
const btnShuffle = document.getElementById('btnShuffle')
btnShuffle?.addEventListener('click', () => toggleShuffle())
btnShuffle?.addEventListener('click', () => onToggleShuffle())
const btnRepeat = document.getElementById('btnRepeat')
btnRepeat?.addEventListener('click', () => toggleRepeat())
btnRepeat?.addEventListener('click', () => onToggleRepeat())
const btnClear = document.getElementById('btnClearQueue')
btnClear?.addEventListener('click', () => clearQueue())
btnClear?.addEventListener('click', () => clearQueuePlayback())
const btnPrev = document.getElementById('btnPrev')
btnPrev?.addEventListener('click', () => prevTrack())
@@ -606,9 +548,8 @@ export function FurumiPlayer() {
}
const clearQueueBtn = document.getElementById('btnClearQueue')
clearQueueBtn?.addEventListener('click', () => clearQueue())
clearQueueBtn?.addEventListener('click', () => clearQueuePlayback())
// --- Init ---
;(async () => {
const url = new URL(window.location.href)
const urlSlug = url.searchParams.get('t')
@@ -632,12 +573,11 @@ export function FurumiPlayer() {
void showArtists()
})()
// Cleanup: best-effort remove listeners on unmount
return () => {
queueActionsRef.current = null
audio.pause()
}
}, [])
}, [dispatch])
const libraryLoading =
breadcrumbs.length === 1
@@ -686,4 +626,3 @@ export function FurumiPlayer() {
</div>
)
}
+1 -1
View File
@@ -44,6 +44,6 @@ export async function getTrackInfo(trackSlug: string): Promise<TrackDetail | nul
}
export async function preloadStream(trackSlug: string) {
await furumiApi.get(`/stream/${trackSlug}`).catch(() => null)
await furumiApi.get(`/stream/${trackSlug}`, { responseType: 'arraybuffer' }).catch(() => null)
}
@@ -5,6 +5,7 @@ import albumsReducer from './slices/albumsSlice'
import albumTracksReducer from './slices/albumTracksSlice'
import artistTracksReducer from './slices/artistTracksSlice'
import trackDetailReducer from './slices/trackDetailSlice'
import queueReducer from './slices/queueSlice'
export const store = configureStore({
reducer: {
@@ -13,6 +14,7 @@ export const store = configureStore({
albumTracks: albumTracksReducer,
artistTracks: artistTracksReducer,
trackDetail: trackDetailReducer,
queue: queueReducer,
},
})
@@ -0,0 +1,273 @@
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
}
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