import { h } from 'preact'; import { useState, useEffect } from 'preact/hooks'; import htm from 'htm'; import { api } from '../api.js'; const html = htm.bind(h); export function Library({ devices }) { const deviceEntries = Object.entries(devices); const firstDeviceId = deviceEntries.length > 0 ? deviceEntries[0][0] : null; const [deviceId, setDeviceId] = useState(firstDeviceId); const [servers, setServers] = useState([]); const [discovered, setDiscovered] = useState([]); const [server, setServer] = useState(null); // { udn, name, account } const [navStack, setNavStack] = useState([]); // [{ label, location, type }] const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(false); const [finding, setFinding] = useState(false); const [playingName, setPlayingName] = useState(null); // Sync deviceId when devices prop first arrives or changes enough to // invalidate the current selection. useEffect(() => { const entries = Object.entries(devices); if (!deviceId && entries.length > 0) { setDeviceId(entries[0][0]); } }, [devices]); // Reload registered servers whenever deviceId changes. useEffect(() => { if (!deviceId) return; setServer(null); setNavStack([]); setEntries([]); loadServers(deviceId); }, [deviceId]); async function loadServers(id) { setLoading(true); const resp = await api.libraryServers(id); setLoading(false); if (resp.success) setServers(resp.data || []); } async function discover() { setLoading(true); const resp = await api.libraryDiscover(6); setLoading(false); if (resp.success) setDiscovered(resp.data || []); } async function addServer(srv) { await api.libraryAddServer(deviceId, { udn: srv.udn, name: srv.name }); await loadServers(deviceId); setFinding(false); } async function removeServer(srv) { await api.libraryRemoveServer(deviceId, `${srv.udn}/0`); if (server && server.udn === srv.udn) { setServer(null); setNavStack([]); setEntries([]); } await loadServers(deviceId); } async function openServer(srv) { const account = `${srv.udn}/0`; setServer({ udn: srv.udn, name: srv.name, account }); const root = { label: srv.name, location: '', type: '' }; setNavStack([root]); await browseLevel(account, '', ''); } async function browseLevel(account, location, type) { setLoading(true); const resp = await api.libraryBrowse(deviceId, { account, location, type }); setLoading(false); if (resp.success) setEntries(resp.data?.entries || []); } async function browseEntry(entry) { const newFrame = { label: entry.name, location: entry.location, type: entry.type }; setNavStack(s => [...s, newFrame]); await browseLevel(server.account, entry.location, entry.type); } async function navTo(index) { const stack = navStack.slice(0, index + 1); setNavStack(stack); const frame = stack[stack.length - 1]; await browseLevel(server.account, frame.location, frame.type); } async function playEntry(entry) { // Pass the entry's own type so a folder selects as a container ("dir") // rather than a single track — that lets the speaker queue the folder so // next/previous and auto-advance work, instead of stopping after one item. await api.libraryPlay(deviceId, { account: server.account, location: entry.location, type: entry.type || 'track', name: entry.name, }); setPlayingName(entry.name); setTimeout(() => setPlayingName(null), 3000); } function toggleFinding() { const next = !finding; setFinding(next); if (next && discovered.length === 0) discover(); } const registeredUdns = new Set(servers.map(s => s.udn)); return html`
No devices found. Discover devices first.
` : html` `} ${loading ? html`` : null} ${finding ? html`No servers found yet. Click Rescan to search.
` : html`No media servers registered on this device. Use "Find servers" to add one.
` : null} ${servers.length > 0 && !server ? html`Media servers
No items found.
` : null}