import { createSelector, type Accessor, type Setter, createSignal, type ResourceActions, createEffect } from "solid-js"; import "./Playlist.css"; import { getItemName, type PlayerState, type PlaylistItem, type AppOptions } from "../types"; import { formatTime, toast } from "../utils"; import { ClearSVG, DownloadSVG, DragSVG, LinkSVG, SaveSVG, SyncSVG } from "./SVG"; import { savePlaylist, isFileCached, download, downloadStatusMap } from "../offline"; import { DraggableList } from "./DraggableList"; import type { FileListing } from "music-server-shared/types"; interface PlaylistProps { currentDir: Accessor; playlist: Accessor; setPlaylist: Setter; playerState: Accessor; setPlayerState: Setter; playTrack: (item: PlaylistItem) => void; audioElement: HTMLAudioElement; options: Accessor; fetchFiles: ResourceActions; } export default function Playlist(props: PlaylistProps) { function removeFromPlaylist(id: string) { const currentTrack = props.playerState().currentTrack; const newPlaylist = props.playlist().filter((i) => i.id !== id); if (currentTrack && currentTrack.id === id) { const idx = props.playlist().findIndex((i) => i.id === id); if (newPlaylist.length > 0) { props.playTrack(idx < newPlaylist.length ? newPlaylist[idx] : newPlaylist[0]); } } props.setPlaylist(newPlaylist); } async function playlistDownload(download: boolean) { try { const playlist = props.playlist(); const filePaths = playlist.map((item) => item.path); const response = await fetch("/prepare-playlist", { body: JSON.stringify(filePaths), method: "POST" }); if (!response.ok) throw new Error(`Generating playlist id not successful: ${await response.text()}`); const id = await response.text(); if (download) { toast("Started playlist download", "info"); const a = document.createElement("a"); a.href = `/download-playlist/${id}`; document.body.appendChild(a); a.click(); document.body.removeChild(a); } else { toast("Copied playlist link to clipboard", "info"); const urlPath = location.href.split(/\?|#/)[0]; navigator.clipboard.writeText(`${urlPath}${urlPath.endsWith("/") ? "" : "/"}download-playlist/${id}`); } } catch (e) { toast("Playlist items download failed", "error", e); } } const [syncing, setSyncing] = createSignal(false); async function syncPlaylist() { if (syncing()) return; setSyncing(true); const progressToast = toast("Syncing files in playlist...", "progress"); try { const itemsToSync: PlaylistItem[] = []; for (const item of props.playlist()) { if (!(await isFileCached(item.path, downloadStatusMap[item.path]))) itemsToSync.push(item); } const syncPromises = itemsToSync.map((item) => { const basePath = item.path.split("/").slice(0, -1).join("/"); const fileName = item.path.split("/").toReversed()[0]; return download(fileName, item, basePath, props.currentDir, props.options(), props.fetchFiles); }); await Promise.allSettled(syncPromises); progressToast.hideToast(); toast("Playlist sync completed", "success"); } catch (e) { progressToast.hideToast(); toast("Playlist sync failed", "error", e); } finally { setSyncing(false); } } let dialogElement!: HTMLDialogElement; let playlistNameInput!: HTMLInputElement; let allowOverwriteCheckbox!: HTMLInputElement; async function save() { try { if (!playlistNameInput.reportValidity()) return; await savePlaylist({ name: playlistNameInput.value, items: props.playlist() }, allowOverwriteCheckbox.checked); closeDialog(); } catch (e) { if (e instanceof Error && e.name === "ConstraintError") { toast("Playlist with this name already exists", "error"); } else { toast("Saving playlist failed", "error", e); } } } function closeDialog() { dialogElement.close(); playlistNameInput.value = ""; allowOverwriteCheckbox.checked = false; } const [syncedCount, setSyncedCount] = createSignal(0); createEffect(async () => { const playlist = props.playlist(); let count = 0; const promises = []; for (const item of playlist) { promises.push( isFileCached(item.path, downloadStatusMap[item.path]).then((res) => { if (res) count++; }), ); } await Promise.all(promises); setSyncedCount(count); }); return (
Playlist ({props.playlist().length} tracks, {syncedCount()} synced)
items={props.playlist} setItems={props.setPlaylist} id="playlist"> {(item, _, { dragHandlers, isDragging, dropPosition }) => { const isActive = createSelector( () => props.playerState().currentTrack?.id, (itemId, currentId) => currentId === itemId, ); return (
props.playTrack(item)} >
{getItemName(item)}
{formatTime(item.metadata.duration)}
); }}
); }