import { createSignal, createEffect, onMount, on } from "solid-js"; import type { Component } from "solid-js"; import type { PlaylistItem, PlayerState, AppOptions } from "./types"; import OptionsModal from "./components/OptionsModal"; import PlayerControls from "./components/PlayerControls"; import FileBrowser from "./components/FileBrowser"; import { ClosedFolderSVG, CogSVG, FullscreenSVG, OpenFolderSVG } from "./components/SVG"; import { LocalStorageValues } from "./utils"; import Playlist from "./components/Playlist"; import { getSrc } from "./offline"; //TODO: error message when playlist item doesn't play from server //TODO: clear server cache && cover cache //TODO: delete downloaded file //TODO: initial app loading screen //TODO: download single file, playlist //TODO: better touch controls? //signal changed file listing when file is downloaded //--- //TODO: setting export/import //TODO: playlist management //TODO: button to show metadata //TODO: show cover of playing item const App: Component = () => { const [showFileBrowser, setShowFileBrowser] = createSignal(false); const [currentDir, setCurrentDir] = createSignal(decodeURI(window.location.hash.slice(1))); const [playlist, setPlaylist] = createSignal([]); const [playerState, setPlayerState] = createSignal({ isPlaying: false, currentTrack: null, currentTime: 0, volume: 0.8, loaded: 0, }); const [showOptions, setShowOptions] = createSignal(false); const [options, setOptions] = createSignal({ bitrate: 128000, enableTranscoding: true, }); const [breadcrumbs, setBreadcrumbs] = createSignal([]); const [isShuffled, setShuffled] = createSignal(false); const [isRepeated, setRepeated] = createSignal(false); createEffect(() => { const parts = currentDir().split("/").filter(Boolean); setBreadcrumbs(["root", ...parts]); // Update URL fragment when directory changes window.location.hash = encodeURI(currentDir()); }); function stopPlaying() { setPlayerState({ ...playerState(), currentTrack: null, isPlaying: false, currentTime: 0, }); audioElement.pause(); audioElement.srcObject = null; } createEffect( on(playlist, () => { if (playlist().length === 0) { stopPlaying(); } }), ); let audioElement!: HTMLAudioElement; // Play a track from the playlist async function playTrack(track: PlaylistItem) { setPlayerState({ ...playerState(), currentTrack: track, isPlaying: true, currentTime: 0, }); if (audioElement.src.startsWith("blob:")) URL.revokeObjectURL(audioElement.src); //need both of these lines to work without logging an error //just removeAttribute doesn't work at all //just srcObject = null works, but logs a weird csp error audioElement.removeAttribute("src"); audioElement.srcObject = null; audioElement.src = await getSrc(track.path, options()); //TODO: toast audioElement.play().catch((error) => console.error("Error playing track:", error)); } // Play next track function playNext() { const currentTrack = playerState().currentTrack; if (!currentTrack) return; let nextTrack: PlaylistItem | null = null; const currentIndex = playlist().findIndex((item) => item.id === currentTrack.id); if (isShuffled()) { // Pick a different random track if (playlist().length > 1) { const otherTracks = playlist().filter((_, idx) => idx !== currentIndex); nextTrack = otherTracks[Math.floor(Math.random() * otherTracks.length)]; } else { nextTrack = currentTrack; } } else if (currentIndex >= 0 && currentIndex < playlist().length - 1) { nextTrack = playlist()[currentIndex + 1]; } else if (isRepeated()) { nextTrack = playlist()[0]; } if (nextTrack) { playTrack(nextTrack); } else { stopPlaying(); } } // Play previous track function playPrevious() { const currentTrack = playerState().currentTrack; if (!currentTrack) return; const currentIndex = playlist().findIndex((item) => item.id === currentTrack.id); if (currentIndex > 0) playTrack(playlist()[currentIndex - 1]); } // Toggle play/pause function togglePlay() { const newIsPlaying = !playerState().isPlaying; setPlayerState({ ...playerState(), isPlaying: newIsPlaying }); if (newIsPlaying) { //TODO: toast audioElement.play().catch((error) => console.error("Error playing track:", error)); } else { audioElement.pause(); } } // Update progress when the time updates function updateProgress() { if (audioElement) { setPlayerState({ ...playerState(), currentTime: audioElement.currentTime, loaded: audioElement.buffered.length > 0 ? audioElement.buffered.end(audioElement.buffered.length - 1) : 0, }); } } // Initialize app onMount(async () => { // Load from localStorage try { const savedOptions = localStorage.getItem(LocalStorageValues.options); if (savedOptions) { setOptions(JSON.parse(savedOptions)); } const savedVolume = localStorage.getItem(LocalStorageValues.volume); if (savedVolume && !Number.isNaN(Number.parseFloat(savedVolume))) { setPlayerState({ ...playerState(), volume: Number.parseFloat(savedVolume) }); } const savedPlaylist = localStorage.getItem(LocalStorageValues.playlist); if (savedPlaylist) { setPlaylist(JSON.parse(savedPlaylist)); } const savedShuffle = localStorage.getItem(LocalStorageValues.shuffle); if (savedShuffle !== null) { setShuffled(savedShuffle === "true"); } const savedRepeat = localStorage.getItem(LocalStorageValues.repeat); if (savedRepeat !== null) { setRepeated(savedRepeat === "true"); } const savedPath = localStorage.getItem(LocalStorageValues.path); if (savedPath !== null && !window.location.hash) { setCurrentDir(savedPath); } } catch (error) { //TODO: toast console.error("loading values from localStorage failed:", error); localStorage.clear(); } // Sync to localStorage createEffect(() => localStorage.setItem(LocalStorageValues.options, JSON.stringify(options()))); createEffect(() => localStorage.setItem(LocalStorageValues.volume, playerState().volume.toString())); createEffect(() => localStorage.setItem(LocalStorageValues.playlist, JSON.stringify(playlist()))); createEffect(() => localStorage.setItem(LocalStorageValues.shuffle, isShuffled().toString())); createEffect(() => localStorage.setItem(LocalStorageValues.repeat, isRepeated().toString())); createEffect(() => localStorage.setItem(LocalStorageValues.path, currentDir())); // Set initial audio element volume audioElement.volume = playerState().volume; // Set up audio element event handlers audioElement.addEventListener("ended", playNext); audioElement.addEventListener("timeupdate", updateProgress); audioElement.addEventListener("durationchange", updateProgress); window.addEventListener("hashchange", () => { setCurrentDir(decodeURI(window.location.hash.slice(1))); }); }); createEffect(() => { // Update audio element when volume changes audioElement.volume = playerState().volume; }); return (
Pico Pixel Player
); }; export default App;