Something went wrong. Try again.
a simple but full-featured web player for OpenSubsonic music servers (Navidrome, Gonic, Ampache, etc) devins.page/tinysub
svelte typescript javascript subsonic gonic navidrome
Something went wrong. Try again.
Svelte
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251<script lang="ts">import { type Component, onMount } from "svelte";import { init, onGlobalKey } from "./lib/app.svelte.js";import { auth, reconnectSession, retrySession } from "./lib/auth.svelte.js";import Controls from "./lib/Controls.svelte";import CoverBackground from "./lib/CoverBackground.svelte";import { getSourceApi, songArtId } from "./lib/client.svelte.js";import UpdateModal from "./lib/dialogs/UpdateModal.svelte";import { rtlLangs } from "./lib/languages/index.js";import { lib, loadLib } from "./lib/library.svelte.js";import { motionEnabled } from "./lib/motion.js";import { offlineManager } from "./lib/offline/offlineManager.svelte.js";import { player } from "./lib/player.svelte.js";import Queue from "./lib/Queue.svelte";import Sidebar from "./lib/Sidebar.svelte";import { artworkSizesForIconSize, iconSettingsForFontSize, settings,} from "./lib/settings.svelte.js";import { transparencyEnabled } from "./lib/transparency.js";import { ui } from "./lib/ui.svelte.js";import VisualizerHost from "./lib/VisualizerHost.svelte";
type WindowControlsOverlayNavigator = Navigator & { windowControlsOverlay?: EventTarget & { visible: boolean };};
onMount(() => { const overlay = (navigator as WindowControlsOverlayNavigator) .windowControlsOverlay; const root = document.documentElement; const updateOverlayVisibility = () => root.classList.toggle("window-controls-overlay", !!overlay?.visible);
updateOverlayVisibility(); overlay?.addEventListener("geometrychange", updateOverlayVisibility); void init(); return () => overlay?.removeEventListener("geometrychange", updateOverlayVisibility);});
let SettingsModal = $state<Component | null>(null);let ShortcutsModal = $state<Component | null>(null);let PlaylistModals = $state<Component | null>(null);let RadioModals = $state<Component | null>(null);let ShareModal = $state<Component | null>(null);let InfoModal = $state<Component | null>(null);let UnsyncedChangesModal = $state<Component | null>(null);const playlistModalOpen = $derived( !!(ui.playlistForm || ui.confirmDeletePlaylist),);const radioModalOpen = $derived( !offlineManager.isOfflineMode && !!(ui.stationForm || ui.confirmDeleteStation),);const shareModalOpen = $derived( !offlineManager.isOfflineMode && !!ui.shareItemId,);const infoModalOpen = $derived(!offlineManager.isOfflineMode && !!ui.infoItem);
let preMiniplayerSize: { width: number; height: number } | null = null;let miniplayerSize: { width: number; height: number } | null = null;
$effect(() => { if (ui.miniplayer) { preMiniplayerSize = { width: window.outerWidth, height: window.outerHeight, }; const size = miniplayerSize ?? { width: 192, height: 320 }; window.resizeTo(size.width, size.height); } else if (preMiniplayerSize) { miniplayerSize = { width: window.outerWidth, height: window.outerHeight, }; window.resizeTo(preMiniplayerSize.width, preMiniplayerSize.height); preMiniplayerSize = null; }});
let lastMode = offlineManager.isOfflineMode;$effect(() => { const currentMode = offlineManager.isOfflineMode; if (lastMode === currentMode) return; if (!currentMode && auth.busy) { lastMode = currentMode; return; }
if (!currentMode) { if (auth.loggedIn) { void reconnectSession(auth.sourceId); } else if (auth.autoLogin) { if (auth.sourceId) { void retrySession(auth.sourceId).catch((err: unknown) => { console.error("failed to retry stored session:", err); }); } } } else { ui.shareItemId = null; ui.shareItemSourceId = null; ui.infoItem = null; ui.stationForm = null; ui.confirmDeleteStation = null; void loadLib(auth.sourceId).catch((err) => { console.error("failed to refresh after going offline:", err); }); }
lastMode = currentMode;});
$effect(() => { if (ui.showSettings && !SettingsModal) void import("./lib/dialogs/SettingsModal.svelte").then( (mod) => (SettingsModal = mod.default), ); if (ui.showKeys && !ShortcutsModal) void import("./lib/dialogs/ShortcutsModal.svelte").then( (mod) => (ShortcutsModal = mod.default), ); if (playlistModalOpen && !PlaylistModals) void import("./lib/dialogs/PlaylistModals.svelte").then( (mod) => (PlaylistModals = mod.default), ); if (radioModalOpen && !RadioModals) void import("./lib/dialogs/RadioModals.svelte").then( (mod) => (RadioModals = mod.default), ); if (shareModalOpen && !ShareModal) void import("./lib/dialogs/ShareModal.svelte").then( (mod) => (ShareModal = mod.default), ); if (infoModalOpen && !InfoModal) void import("./lib/dialogs/InfoModal.svelte").then( (mod) => (InfoModal = mod.default), ); if (ui.showUnsyncedChanges && !UnsyncedChangesModal) void import("./lib/dialogs/UnsyncedChangesModal.svelte").then( (mod) => (UnsyncedChangesModal = mod.default), );});
let lastFontSize: string | undefined;
$effect(() => { const root = document.documentElement; const style = root.style; const fontSize = settings.fontSize;
style.setProperty("--font-family", settings.fontFamily.trim()); style.setProperty( "--font-family-monospace", settings.fontFamilyMonospace.trim(), ); style.setProperty("--font-size", fontSize.trim());
if (!ui.busy) { if (lastFontSize !== undefined && fontSize !== lastFontSize) { const iconSettings = iconSettingsForFontSize( Number.parseFloat(getComputedStyle(root).fontSize), settings.iconPack, ); Object.assign( settings, iconSettings, artworkSizesForIconSize(iconSettings.iconSize, settings), ); } lastFontSize = fontSize; }
style.setProperty( "--icon-size", settings.iconSize === 16 ? "" : `${settings.iconSize}px`, );
const classList = root.classList; classList.toggle("motion", motionEnabled()); classList.toggle("transparency", transparencyEnabled()); classList.toggle("dynamic-colors", settings.dynamicColors); classList.toggle("rounded-corners", settings.roundedCorners); classList.toggle("transparent-borders", settings.transparentOutlines); classList.toggle("miniplayer", ui.miniplayer); classList.toggle("show-library-mobile", ui.showLibraryMobile);
root.lang = settings.lang; root.dir = rtlLangs.has(settings.lang) ? "rtl" : "ltr";});</script>
<svelte:window onkeydown={onGlobalKey} />
{#if player.track} {@const artist = settings.coverBackground === "artist" && player.track.artistId ? lib.artists.find( ({ id, sourceId }) => id === player.track?.artistId && sourceId === player.track?.sourceId, ) : undefined} {@const artId = artist?.coverArt || songArtId(player.track)} {#if artId && player.track.sourceId} {@const backgroundUrl = getSourceApi(player.track.sourceId).getCoverArt( artId, artist?.coverArt ? 128 : 512, )} <CoverBackground className="app-background" src={backgroundUrl} fadeIfComplete /> {/if}{/if}
{#if settings.visualizerLocation === "background"} <VisualizerHost />{/if}
<div class="titlebar-drag-region" aria-hidden="true"></div><Sidebar /><Queue /><Controls />
{#if ui.showSettings && SettingsModal} <SettingsModal />{/if}{#if ui.showKeys && ShortcutsModal} <ShortcutsModal />{/if}{#if playlistModalOpen && PlaylistModals} <PlaylistModals />{/if}{#if radioModalOpen && RadioModals} <RadioModals />{/if}{#if shareModalOpen && ShareModal} <ShareModal />{/if}{#if infoModalOpen && InfoModal} <InfoModal />{/if}{#if ui.showUnsyncedChanges && UnsyncedChangesModal} <UnsyncedChangesModal />{/if}<UpdateModal />