import { createSelector, type Accessor, type Setter, createSignal, type ResourceActions, createEffect, onMount, Show, on, batch, } from "solid-js"; import "./Playlist.css"; import { getItemName, type PlayerState, type PlaylistItem, type AppOptions } from "../types"; import { formatTime, LocalStorageValues, toast } from "../utils"; import { ClearSVG, DownloadSVG, DownSVG, DragSVG, LinkSVG, SaveSVG, SyncSVG, UpSVG } from "./SVG"; import { savePlaylist, download, storedFilesLive } from "../offline"; import { DraggableList } from "./DraggableList"; import type { FlatFileListing } from "../App"; interface PlaylistProps { playlist: Accessor; setPlaylist: Setter; playerState: PlayerState; setPlayerState: Setter; playTrack: (item: PlaylistItem) => void; playNext: () => void; audioElement: HTMLAudioElement; options: AppOptions; fetchFiles: ResourceActions; } export default function Playlist(props: PlaylistProps) { const [showActionButtons, setShowActionButtons] = createSignal(true); function removeFromPlaylist(id: string) { props.playNext(); const newPlaylist = props.playlist().filter((i) => i.id !== id); 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[] = []; const playlist = props.playlist(); const unique = playlist.filter((item, pos) => playlist.indexOf(item) === pos); const storedFiles = storedFilesLive(); for (const item of unique) { if (!storedFiles.includes(item.path)) itemsToSync.push(item); } let downloadPromises!: Promise[]; batch(() => { downloadPromises = 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.options, props.fetchFiles); }); }); await Promise.allSettled(downloadPromises); toast("Playlist sync completed", "success"); } catch (e) { toast("Playlist sync failed", "error", e); } finally { progressToast.hideToast(); 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( on([props.playlist, storedFilesLive], async ([playlist, storedFiles]) => { setSyncedCount(playlist.filter((item) => storedFiles.includes(item.path)).length); }), ); onMount(() => { const savedPlaylistButtons = localStorage.getItem(LocalStorageValues.playlistButtons); if (savedPlaylistButtons !== null) setShowActionButtons(savedPlaylistButtons === "true"); createEffect(() => localStorage.setItem(LocalStorageValues.playlistButtons, showActionButtons().toString())); }); return (
e.target.nodeName === "DIALOG" && closeDialog()}>
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)}
); }}
); }