import "./FileBrowser.css"; import { type Accessor, createEffect, createResource, createSignal, For, onMount, type Setter, Show, Suspense, } from "solid-js"; import { type MediaFile, type FileListing, fileSort } 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 as deleteCover, 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( //TODO: try using store () => ({ dir: props.currentDir(), offline: props.isOffline() }), async (args) => { //new map to force rerender return new Map(await listFiles(args.dir, args.offline, false)); }, { initialValue: new Map() }, ); 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(path: string, item: MediaFile): PlaylistItem { return { id: generateUUID(), metadata: item.metadata, path: path, }; } // Recursively convert file listing to PlaylistItem array function listingToPlaylistItems(basePath: string, files: FileListing): PlaylistItem[] { let result: PlaylistItem[] = []; for (const [fileName, file] of files) { const fullPath = joinPath(basePath, fileName); if ("metadata" in file) { // It's an audio file result.push(fileToPlaylistItem(fullPath, file)); } else if (file.files && file.files.size > 0) { // It's a directory with files, recursively add its contents result = [...result, ...listingToPlaylistItems(fullPath, file.files)]; } } return result.sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true })); } function playFile(path: string, item: MediaFile) { props.playTrack(fileToPlaylistItem(path, item)); } function addToPlaylist(path: string, file: MediaFile) { const newItem: PlaylistItem = { id: generateUUID(), metadata: file.metadata, path: path, }; 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 = listingToPlaylistItems(selectedDir, files); 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()); //TODO: only clears the current dir, not recursively like clearDirectoryCache deleteCover(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 ( <>
{([fileName, item]) => { const isDirectory = !("metadata" in item); const fullPath = joinPath(props.currentDir(), fileName); return ( <>
(isDirectory && props.setCurrentDir(fullPath)) || playFile(fullPath, item as MediaFile) } > {(isDirectory && ) || } {fileName}
); }}
}>
0}>
props.setShowFileBrowser(false)} /> ); }