From ebf0256c748a9d5bed29cbeb26a6ba89b0af5632 Mon Sep 17 00:00:00 2001 From: Boris Cherepanov Date: Mon, 23 Mar 2026 14:22:44 +0300 Subject: [PATCH] feat: refactoring --- .../client/src/FurumiPlayer.tsx | 186 +++++------------- .../client/src/components/Header.tsx | 45 +++++ .../client/src/components/MainPanel.tsx | 86 ++++++++ .../client/src/components/PlayerBar.tsx | 47 +++++ furumi-node-player/client/src/utils.ts | 14 ++ 5 files changed, 236 insertions(+), 142 deletions(-) create mode 100644 furumi-node-player/client/src/components/Header.tsx create mode 100644 furumi-node-player/client/src/components/MainPanel.tsx create mode 100644 furumi-node-player/client/src/components/PlayerBar.tsx create mode 100644 furumi-node-player/client/src/utils.ts diff --git a/furumi-node-player/client/src/FurumiPlayer.tsx b/furumi-node-player/client/src/FurumiPlayer.tsx index 297eab9..3d614df 100644 --- a/furumi-node-player/client/src/FurumiPlayer.tsx +++ b/furumi-node-player/client/src/FurumiPlayer.tsx @@ -10,16 +10,14 @@ import { getTrackInfo, preloadStream, } 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 Crumb = { label: string; action?: () => void } +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' export function FurumiPlayer() { - const [breadcrumbs, setBreadcrumbs] = useState void }>>( + const [breadcrumbs, setBreadcrumbs] = useState( [], ) const [libraryLoading, setLibraryLoading] = useState(false) @@ -47,6 +45,7 @@ export function FurumiPlayer() { const [queueOrderView, setQueueOrderView] = useState([]) const [queuePlayingOrigIdxView, setQueuePlayingOrigIdxView] = useState(-1) const [queueScrollSignal, setQueueScrollSignal] = useState(0) + const [queue, setQueue] = useState([]) const queueActionsRef = useRef<{ playIndex: (i: number) => void @@ -54,12 +53,14 @@ export function FurumiPlayer() { moveQueueItem: (fromPos: number, toPos: number) => void } | null>(null) + const audioRef = useRef(null) + useEffect(() => { // --- Original player script adapted for React environment --- - const audio = document.getElementById('audioEl') as HTMLAudioElement - if (!audio) return + const audioEl = audioRef.current + if (!audioEl) return + const audio = audioEl - let queue: QueueItem[] = [] let queueIndex = -1 let shuffle = false let repeatAll = true @@ -253,7 +254,7 @@ export function FurumiPlayer() { if (playNow) playIndex(existing) return } - queue.push(track) + setQueue((q) => [...q, track]); updateQueueModel() if (playNow || (queueIndex === -1 && queue.length === 1)) { playIndex(queue.length - 1) @@ -267,13 +268,7 @@ export function FurumiPlayer() { let firstIdx = queue.length list.forEach((t) => { if (queue.find((q) => q.slug === t.slug)) return - queue.push({ - slug: t.slug, - title: t.title, - artist: t.artist_name, - album_slug: albumSlug, - duration: t.duration_secs, - }) + setQueue((q) => [...q, t]) }) updateQueueModel() if (playFirst || queueIndex === -1) playIndex(firstIdx) @@ -356,7 +351,7 @@ export function FurumiPlayer() { function updateQueueModel() { const order = currentOrder() - setQueueItemsView(queue.slice()) + setQueueItemsView(queue) setQueueOrderView(order.slice()) setQueuePlayingOrigIdxView(queueIndex) } @@ -370,7 +365,10 @@ export function FurumiPlayer() { } else if (queueIndex > idx) { queueIndex-- } - queue.splice(idx, 1) + + // 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) @@ -403,7 +401,7 @@ export function FurumiPlayer() { } function clearQueue() { - queue = [] + setQueue([]); queueIndex = -1 shuffleOrder = [] audio.pause() @@ -526,21 +524,6 @@ export function FurumiPlayer() { searchSelectRef.current = onSearchSelect // --- Helpers --- - function fmt(secs: number) { - if (!secs || Number.isNaN(secs)) return '0:00' - const s = Math.floor(secs) - const m = Math.floor(s / 60) - const h = Math.floor(m / 60) - if (h > 0) { - return `${h}:${pad(m % 60)}:${pad(s % 60)}` - } - return `${m}:${pad(s % 60)}` - } - - function pad(n: number) { - return String(n).padStart(2, '0') - } - function showToast(msg: string) { const t = document.getElementById('toast') if (!t) return @@ -652,115 +635,34 @@ export function FurumiPlayer() { return (
-
-
- - - - - - - Furumi - v -
-
-
- - searchSelectRef.current(type, slug)} - /> -
-
-
+
searchSelectRef.current(type, slug)} + /> -
-
- + queueActionsRef.current?.playIndex(origIdx)} + onQueueRemove={(origIdx) => + queueActionsRef.current?.removeFromQueue(origIdx) + } + onQueueMove={(fromPos, toPos) => + queueActionsRef.current?.moveQueueItem(fromPos, toPos) + } + /> -
-
- Queue -
- - - -
-
-
- queueActionsRef.current?.playIndex(origIdx)} - onRemove={(origIdx) => - queueActionsRef.current?.removeFromQueue(origIdx) - } - onMove={(fromPos, toPos) => - queueActionsRef.current?.moveQueueItem(fromPos, toPos) - } - /> -
-
-
- -
- -
-
- - - -
-
- - 0:00 - -
-
-
- - 0:00 - -
-
-
- - 🔊 - - -
-
+
-
) } diff --git a/furumi-node-player/client/src/components/Header.tsx b/furumi-node-player/client/src/components/Header.tsx new file mode 100644 index 0000000..1cd5708 --- /dev/null +++ b/furumi-node-player/client/src/components/Header.tsx @@ -0,0 +1,45 @@ +import { SearchDropdown } from './SearchDropdown' + +type SearchResultItem = { + result_type: string + slug: string + name: string + detail?: string +} + +type HeaderProps = { + searchOpen: boolean + searchResults: SearchResultItem[] + onSearchSelect: (type: string, slug: string) => void +} + +export function Header({ + searchOpen, + searchResults, + onSearchSelect, +}: HeaderProps) { + return ( +
+
+ + + + + + + Furumi + v +
+
+
+ + +
+
+
+ ) +} diff --git a/furumi-node-player/client/src/components/MainPanel.tsx b/furumi-node-player/client/src/components/MainPanel.tsx new file mode 100644 index 0000000..7601279 --- /dev/null +++ b/furumi-node-player/client/src/components/MainPanel.tsx @@ -0,0 +1,86 @@ +import type { MouseEvent as ReactMouseEvent } from 'react' +import { Breadcrumbs } from './Breadcrumbs' +import { LibraryList } from './LibraryList' +import { QueueList, type QueueItem } from './QueueList' + +export type Crumb = { label: string; action?: () => void } + +export type LibraryListItem = { + key: string + className: string + icon: string + name: string + detail?: string + nameClassName?: string + onClick: () => void + button?: { title: string; onClick: (ev: ReactMouseEvent) => void } +} + +type MainPanelProps = { + breadcrumbs: Crumb[] + libraryLoading: boolean + libraryError: string | null + libraryItems: LibraryListItem[] + queueItemsView: QueueItem[] + queueOrderView: number[] + queuePlayingOrigIdxView: number + queueScrollSignal: number + onQueuePlay: (origIdx: number) => void + onQueueRemove: (origIdx: number) => void + onQueueMove: (fromPos: number, toPos: number) => void +} + +export function MainPanel({ + breadcrumbs, + libraryLoading, + libraryError, + libraryItems, + queueItemsView, + queueOrderView, + queuePlayingOrigIdxView, + queueScrollSignal, + onQueuePlay, + onQueueRemove, + onQueueMove, +}: MainPanelProps) { + return ( +
+
+ + +
+
+ Queue +
+ + + +
+
+
+ +
+
+
+ ) +} diff --git a/furumi-node-player/client/src/components/PlayerBar.tsx b/furumi-node-player/client/src/components/PlayerBar.tsx new file mode 100644 index 0000000..48fcdff --- /dev/null +++ b/furumi-node-player/client/src/components/PlayerBar.tsx @@ -0,0 +1,47 @@ +import { NowPlaying } from './NowPlaying' +import type { QueueItem } from './QueueList' + +export function PlayerBar({ track }: { track: QueueItem | null }) { + return ( +
+ +
+
+ + + +
+
+ + 0:00 + +
+
+
+ + 0:00 + +
+
+
+ + 🔊 + + +
+
+ ) +} diff --git a/furumi-node-player/client/src/utils.ts b/furumi-node-player/client/src/utils.ts new file mode 100644 index 0000000..6e9ce67 --- /dev/null +++ b/furumi-node-player/client/src/utils.ts @@ -0,0 +1,14 @@ +function pad(n: number) { + return String(n).padStart(2, '0') +} + +export function fmt(secs: number) { + if (!secs || Number.isNaN(secs)) return '0:00' + const s = Math.floor(secs) + const m = Math.floor(s / 60) + const h = Math.floor(m / 60) + if (h > 0) { + return `${h}:${pad(m % 60)}:${pad(s % 60)}` + } + return `${m}:${pad(s % 60)}` +}