import "./FileBrowser.css"; import { type Accessor, createEffect, createResource, createSignal, For, onMount, type Setter, Show, Suspense, } from "solid-js"; import type { AudioFile, FileListing } from "music-server-shared/types"; import type { AppOptions, PlaylistItem } from "../types"; import { generateUUID, joinPath, LocalStorageValues, toast } from "../utils"; import { ClosedFolderSVG, MusicSVG, RefreshSVG } from "./SVG"; import { clearCoverCache, clearDirectoryCache, getCover, listFiles } from "../offline"; import { DownloadButton } from "./DownloadButton"; import { LoadingScreen } from "./LoadingScreen"; interface FileBrowserProps { breadcrumbs: Accessor; currentDir: Accessor; setCurrentDir: Setter; playTrack: (track: PlaylistItem) => void; playlist: Accessor; setPlaylist: Setter; showFileBrowser: Accessor; setShowFileBrowser: Setter; options: Accessor; isOffline: Accessor; setOffline: Setter; } export default function FileBrowser(props: FileBrowserProps) { const [loadingActions, setLoadingActions] = createSignal(0); const [files, fetchFiles] = createResource( () => ({ dir: props.currentDir(), offline: props.isOffline() }), async (args) => await listFiles(args.dir, args.offline, false), { initialValue: [] }, ); const [cover, fetchCover] = createResource( () => ({ dir: props.currentDir(), offline: props.isOffline() }), async (args, info) => { if (info.value) URL.revokeObjectURL(info.value as string); return await getCover(args.dir, args.offline); }, { initialValue: undefined }, ); function handleBreadcrumbClick(index: number) { if (index === 0) { props.setCurrentDir(""); } else { props.setCurrentDir(joinPath(...props.breadcrumbs().slice(1, index + 1))); } } function fileToPlaylistItem(basePath: string, item: AudioFile) { const normpath = basePath === "" ? "" : `${basePath}/`; return { id: generateUUID(), name: item.name.split("/").pop() || item.name, duration: item.duration, path: `${normpath}${item.name.split("/").pop()}`, }; } // Recursively collect audio files function collectAudioFiles(files: FileListing, basePath: string): PlaylistItem[] { let result: PlaylistItem[] = []; for (const file of files) { if ("duration" in file) { // It's an audio file result.push(fileToPlaylistItem(basePath, file)); } else if ("files" in file && file.files.length > 0) { // It's a directory with files, recursively add its contents const normpath = basePath === "" ? "" : `${basePath}/`; const subPath = `${normpath}${file.name}`; result = [...result, ...collectAudioFiles(file.files, subPath)]; } } return result; } function playFile(item: AudioFile) { props.playTrack(fileToPlaylistItem(props.currentDir(), item)); } function addToPlaylist(file: AudioFile, path: string) { const newItem: PlaylistItem = { id: generateUUID(), name: file.name, duration: file.duration, path: joinPath(path, file.name), }; props.setPlaylist([...props.playlist(), newItem]); if (props.playlist().length === 1) { props.playTrack(newItem); } } async function playFiles(recursive: boolean, queue: boolean, dir?: string) { if (recursive) { setLoadingActions(loadingActions() + 1); } try { const selectedDir = dir || props.currentDir(); const files = await listFiles(selectedDir, props.isOffline(), recursive); const newPlaylist = collectAudioFiles(files, selectedDir); const oldPlaylist = props.playlist(); if (queue) { props.setPlaylist([...oldPlaylist, ...newPlaylist]); if (oldPlaylist.length === 0 && newPlaylist.length !== 0) props.playTrack(newPlaylist[0]); } else { props.setPlaylist(newPlaylist); if (newPlaylist.length !== 0) props.playTrack(newPlaylist[0]); } } finally { if (recursive) { setLoadingActions(loadingActions() - 1); } } } function reloadDirectory() { clearDirectoryCache(props.currentDir()); clearCoverCache(props.currentDir()); fetchFiles.refetch(); fetchCover.refetch(); } onMount(() => { try { const savedOffline = localStorage.getItem(LocalStorageValues.offline); props.setOffline(savedOffline === "true"); } catch (e) { toast("loading values from localStorage failed", "error", e); localStorage.clear(); } createEffect(() => { createEffect(() => localStorage.setItem(LocalStorageValues.offline, props.isOffline().toString())); }); }); return (
{(item) => { const isDirectory = "files" in item; const fullPath = joinPath(props.currentDir(), item.name); return ( <>
(isDirectory && props.setCurrentDir(fullPath)) || playFile(item as AudioFile)} > {(isDirectory && ) || } {item.name}
); }}
}>
0}>
); }