feat: refactoring
This commit is contained in:
@@ -4,6 +4,8 @@ import { createFurumiApiClient } from './furumiApi'
|
||||
import { SearchDropdown } from './components/SearchDropdown'
|
||||
import { Breadcrumbs } from './components/Breadcrumbs'
|
||||
import { LibraryList } from './components/LibraryList'
|
||||
import { QueueList, type QueueItem } from './components/QueueList'
|
||||
import { NowPlaying } from './components/NowPlaying'
|
||||
|
||||
type FurumiPlayerProps = {
|
||||
apiRoot: string
|
||||
@@ -35,18 +37,24 @@ export function FurumiPlayer({ apiRoot }: FurumiPlayerProps) {
|
||||
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 queueActionsRef = useRef<{
|
||||
playIndex: (i: number) => void
|
||||
removeFromQueue: (idx: number) => void
|
||||
moveQueueItem: (fromPos: number, toPos: number) => void
|
||||
} | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// --- Original player script adapted for React environment ---
|
||||
const audio = document.getElementById('audioEl') as HTMLAudioElement
|
||||
if (!audio) return
|
||||
|
||||
let queue: Array<{
|
||||
slug: string
|
||||
title: string
|
||||
artist: string
|
||||
album_slug: string | null
|
||||
duration: number | null
|
||||
}> = []
|
||||
let queue: QueueItem[] = []
|
||||
let queueIndex = -1
|
||||
let shuffle = false
|
||||
let repeatAll = true
|
||||
@@ -245,7 +253,7 @@ export function FurumiPlayer({ apiRoot }: FurumiPlayerProps) {
|
||||
return
|
||||
}
|
||||
queue.push(track)
|
||||
renderQueue()
|
||||
updateQueueModel()
|
||||
if (playNow || (queueIndex === -1 && queue.length === 1)) {
|
||||
playIndex(queue.length - 1)
|
||||
}
|
||||
@@ -266,7 +274,7 @@ export function FurumiPlayer({ apiRoot }: FurumiPlayerProps) {
|
||||
duration: t.duration_secs,
|
||||
})
|
||||
})
|
||||
renderQueue()
|
||||
updateQueueModel()
|
||||
if (playFirst || queueIndex === -1) playIndex(firstIdx)
|
||||
showToast(`Added ${list.length} tracks`)
|
||||
}
|
||||
@@ -285,7 +293,7 @@ export function FurumiPlayer({ apiRoot }: FurumiPlayerProps) {
|
||||
duration: t.duration_secs,
|
||||
})
|
||||
})
|
||||
renderQueue()
|
||||
updateQueueModel()
|
||||
playIndex(0)
|
||||
showToast(`Added ${list.length} tracks`)
|
||||
}
|
||||
@@ -297,8 +305,8 @@ export function FurumiPlayer({ apiRoot }: FurumiPlayerProps) {
|
||||
audio.src = `${API}/stream/${track.slug}`
|
||||
void audio.play().catch(() => {})
|
||||
updateNowPlaying(track)
|
||||
renderQueue()
|
||||
scrollQueueToActive()
|
||||
updateQueueModel()
|
||||
setQueueScrollSignal((s) => s + 1)
|
||||
if (window.history && window.history.replaceState) {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set('t', track.slug)
|
||||
@@ -306,24 +314,13 @@ export function FurumiPlayer({ apiRoot }: FurumiPlayerProps) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateNowPlaying(track: (typeof queue)[number] | null) {
|
||||
const npTitle = document.getElementById('npTitle')
|
||||
const npArtist = document.getElementById('npArtist')
|
||||
if (!track) {
|
||||
if (npTitle) npTitle.textContent = 'Nothing playing'
|
||||
if (npArtist) npArtist.textContent = '—'
|
||||
return
|
||||
}
|
||||
if (npTitle) npTitle.textContent = track.title
|
||||
if (npArtist) npArtist.textContent = track.artist || '—'
|
||||
function updateNowPlaying(track: QueueItem | null) {
|
||||
setNowPlayingTrack(track)
|
||||
if (!track) return
|
||||
|
||||
document.title = `${track.title} — Furumi`
|
||||
|
||||
const cover = document.getElementById('npCover')
|
||||
const coverUrl = `${API}/tracks/${track.slug}/cover`
|
||||
if (cover) {
|
||||
cover.innerHTML = `<img src="${coverUrl}" alt="" onerror="this.parentElement.innerHTML='🎵'">`
|
||||
}
|
||||
|
||||
if ('mediaSession' in navigator) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
navigator.mediaSession.metadata = new window.MediaMetadata({
|
||||
@@ -356,77 +353,11 @@ export function FurumiPlayer({ apiRoot }: FurumiPlayerProps) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderQueue() {
|
||||
const el = document.getElementById('queueList')
|
||||
if (!el) return
|
||||
if (!queue.length) {
|
||||
el.innerHTML =
|
||||
'<div class="queue-empty"><div class="empty-icon">🎵</div><div>Select an album to start</div></div>'
|
||||
return
|
||||
}
|
||||
function updateQueueModel() {
|
||||
const order = currentOrder()
|
||||
el.innerHTML = ''
|
||||
order.forEach((origIdx, pos) => {
|
||||
const t = queue[origIdx]
|
||||
const isPlaying = origIdx === queueIndex
|
||||
const div = document.createElement('div')
|
||||
div.className = 'queue-item' + (isPlaying ? ' playing' : '')
|
||||
|
||||
const coverSrc = t.album_slug ? `${API}/tracks/${t.slug}/cover` : ''
|
||||
const coverHtml = coverSrc
|
||||
? `<img src="${coverSrc}" alt="" onerror="this.parentElement.innerHTML='🎵'">`
|
||||
: '🎵'
|
||||
const dur = t.duration ? fmt(t.duration) : ''
|
||||
|
||||
div.innerHTML = `
|
||||
<span class="qi-index">${isPlaying ? '' : pos + 1}</span>
|
||||
<div class="qi-cover">${coverHtml}</div>
|
||||
<div class="qi-info"><div class="qi-title">${esc(
|
||||
t.title,
|
||||
)}</div><div class="qi-artist">${esc(t.artist || '')}</div></div>
|
||||
<span class="qi-dur">${dur}</span>
|
||||
<button class="qi-remove">✕</button>
|
||||
`
|
||||
div.addEventListener('click', () => playIndex(origIdx))
|
||||
|
||||
const removeBtn = div.querySelector('.qi-remove') as HTMLButtonElement | null
|
||||
if (removeBtn) {
|
||||
removeBtn.onclick = (ev) => {
|
||||
ev.stopPropagation()
|
||||
removeFromQueue(origIdx)
|
||||
}
|
||||
}
|
||||
|
||||
div.draggable = true
|
||||
div.addEventListener('dragstart', (e) => {
|
||||
e.dataTransfer?.setData('text/plain', String(pos))
|
||||
div.classList.add('dragging')
|
||||
})
|
||||
div.addEventListener('dragend', () => {
|
||||
div.classList.remove('dragging')
|
||||
el
|
||||
.querySelectorAll('.queue-item')
|
||||
.forEach((x) => x.classList.remove('drag-over'))
|
||||
})
|
||||
div.addEventListener('dragover', (e) => {
|
||||
e.preventDefault()
|
||||
})
|
||||
div.addEventListener('dragenter', () => div.classList.add('drag-over'))
|
||||
div.addEventListener('dragleave', () => div.classList.remove('drag-over'))
|
||||
div.addEventListener('drop', (e) => {
|
||||
e.preventDefault()
|
||||
div.classList.remove('drag-over')
|
||||
const from = parseInt(e.dataTransfer?.getData('text/plain') ?? '', 10)
|
||||
if (!Number.isNaN(from)) moveQueueItem(from, pos)
|
||||
})
|
||||
|
||||
el.appendChild(div)
|
||||
})
|
||||
}
|
||||
|
||||
function scrollQueueToActive() {
|
||||
const el = document.querySelector('.queue-item.playing') as HTMLElement | null
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
setQueueItemsView(queue.slice())
|
||||
setQueueOrderView(order.slice())
|
||||
setQueuePlayingOrigIdxView(queueIndex)
|
||||
}
|
||||
|
||||
function removeFromQueue(idx: number) {
|
||||
@@ -446,7 +377,7 @@ export function FurumiPlayer({ apiRoot }: FurumiPlayerProps) {
|
||||
if (shuffleOrder[i] > idx) shuffleOrder[i]--
|
||||
}
|
||||
}
|
||||
renderQueue()
|
||||
updateQueueModel()
|
||||
}
|
||||
|
||||
function moveQueueItem(from: number, to: number) {
|
||||
@@ -461,7 +392,13 @@ export function FurumiPlayer({ apiRoot }: FurumiPlayerProps) {
|
||||
else if (from < queueIndex && to >= queueIndex) queueIndex--
|
||||
else if (from > queueIndex && to <= queueIndex) queueIndex++
|
||||
}
|
||||
renderQueue()
|
||||
updateQueueModel()
|
||||
}
|
||||
|
||||
queueActionsRef.current = {
|
||||
playIndex,
|
||||
removeFromQueue,
|
||||
moveQueueItem,
|
||||
}
|
||||
|
||||
function clearQueue() {
|
||||
@@ -472,7 +409,7 @@ export function FurumiPlayer({ apiRoot }: FurumiPlayerProps) {
|
||||
audio.src = ''
|
||||
updateNowPlaying(null)
|
||||
document.title = 'Furumi Player'
|
||||
renderQueue()
|
||||
updateQueueModel()
|
||||
}
|
||||
|
||||
// --- Playback controls ---
|
||||
@@ -514,7 +451,7 @@ export function FurumiPlayer({ apiRoot }: FurumiPlayerProps) {
|
||||
const btn = document.getElementById('btnShuffle')
|
||||
btn?.classList.toggle('active', shuffle)
|
||||
window.localStorage.setItem('furumi_shuffle', shuffle ? '1' : '0')
|
||||
renderQueue()
|
||||
updateQueueModel()
|
||||
}
|
||||
|
||||
function toggleRepeat() {
|
||||
@@ -603,15 +540,6 @@ export function FurumiPlayer({ apiRoot }: FurumiPlayerProps) {
|
||||
return String(n).padStart(2, '0')
|
||||
}
|
||||
|
||||
function esc(s: string | null | undefined) {
|
||||
return String(s ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function showToast(msg: string) {
|
||||
const t = document.getElementById('toast')
|
||||
if (!t) return
|
||||
@@ -716,6 +644,7 @@ export function FurumiPlayer({ apiRoot }: FurumiPlayerProps) {
|
||||
|
||||
// Cleanup: best-effort remove listeners on unmount
|
||||
return () => {
|
||||
queueActionsRef.current = null
|
||||
audio.pause()
|
||||
}
|
||||
}, [apiRoot])
|
||||
@@ -771,28 +700,26 @@ export function FurumiPlayer({ apiRoot }: FurumiPlayerProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="queue-list" id="queueList">
|
||||
<div className="queue-empty">
|
||||
<div className="empty-icon">🎵</div>
|
||||
<div>Select an album to start</div>
|
||||
</div>
|
||||
<QueueList
|
||||
apiRoot={apiRoot}
|
||||
queue={queueItemsView}
|
||||
order={queueOrderView}
|
||||
playingOrigIdx={queuePlayingOrigIdxView}
|
||||
scrollSignal={queueScrollSignal}
|
||||
onPlay={(origIdx) => queueActionsRef.current?.playIndex(origIdx)}
|
||||
onRemove={(origIdx) =>
|
||||
queueActionsRef.current?.removeFromQueue(origIdx)
|
||||
}
|
||||
onMove={(fromPos, toPos) =>
|
||||
queueActionsRef.current?.moveQueueItem(fromPos, toPos)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="player-bar">
|
||||
<div className="np-info">
|
||||
<div className="np-cover" id="npCover">
|
||||
🎵
|
||||
</div>
|
||||
<div className="np-text">
|
||||
<div className="np-title" id="npTitle">
|
||||
Nothing playing
|
||||
</div>
|
||||
<div className="np-artist" id="npArtist">
|
||||
—
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NowPlaying apiRoot={apiRoot} track={nowPlayingTrack} />
|
||||
<div className="controls">
|
||||
<div className="ctrl-btns">
|
||||
<button className="ctrl-btn" id="btnPrev">
|
||||
|
||||
Reference in New Issue
Block a user