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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257<script lang="ts">import { slide } from "svelte/transition";import { api } from "../api/client.svelte.js";import type { ScanStatus } from "../api/types.js";import { settings } from "../app/settings.svelte.js";import { t } from "../languages/translate.svelte.js";import { offlineManager } from "../offline/offlineManager.svelte.js";import { load, player } from "../playback/player.svelte.js";import { slideMotion } from "../ui/motion.svelte.js";import { ui } from "../ui/state.svelte.js";
let storageUsage = $state<number | null>(null);let scanStatus = $state<ScanStatus | null>(null);
async function refreshStorageUsage() { if (!navigator.storage?.estimate) { storageUsage = null; return; } const estimate = await navigator.storage.estimate(); storageUsage = estimate.usage ?? null;}
$effect(() => { if (!ui.showSettings) return; scanStatus = null; const refreshDataTab = () => { refreshStorageUsage().catch((err) => { console.error("failed to estimate storage:", err); }); api .getScanStatus() .then((status) => (scanStatus = status)) .catch((err) => { console.error("failed to get scan status:", err); }); }; refreshDataTab(); const timer = setInterval(refreshDataTab, 10000); return () => clearInterval(timer);});
function formatBytes(bytes: number | null): string { if (bytes === null) return "Unknown"; if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;}
async function handleDownloadsToggle(event: Event) { const input = event.currentTarget as HTMLInputElement; if (input.checked) { settings.downloads = true; await offlineManager.refreshDownloadsList(); return; } const confirmed = confirm(t("offline.clear_downloads_confirm")); if (!confirmed) { input.checked = true; return; } ui.busy = true; try { settings.downloads = false; settings.downloadsAuto = false; settings.downloadsAutoSyncMode = "none";
if (player.track && offlineManager.downloadedSongIds.has(player.track.id)) { await load(player.track, player.time, !player.paused); } await offlineManager.wipeOfflineData(); await refreshStorageUsage(); } catch (err) { settings.downloads = true; console.error("failed to disable downloads:", err); } finally { ui.busy = false; }}</script>
{#if !offlineManager.isOfflineMode} <div class="row"> <span>{t("settings.scan_status")}</span> <span class="num"> {#if scanStatus} {scanStatus.scanning ? `${t("settings.scanning")} ${scanStatus.count ?? ""}`.trim() : (scanStatus.count ?? "")} {/if} </span> </div> <br>{/if}{#if storageUsage !== null} <div class="row"> <span>{t("offline.storage_usage")}</span> <span class="num">{formatBytes(storageUsage)}</span> </div> <small class="row"> ℹ️ {t("offline.storage_usage_desc")} </small> <br>{/if}
<div class="row"> <span>{t("settings.transcoding")}</span> <input type="checkbox" bind:checked={settings.transcoding}></div>{#if settings.transcoding} <div class="sub-group" transition:slide={slideMotion()}> {#if settings.downloads} <small class="row"> ⚠️ {t("settings.downloaded_songs_not_transcoded")} </small> {/if} <div class="row"> <span>{t("settings.transcode_format")}</span> <input type="text" bind:value={settings.transcodingFormat} placeholder="opus" > </div> <div class="row"> <span>{t("settings.transcode_bitrate")}</span> <input type="number" value={settings.transcodingBitrate ?? ""} oninput={(e) => { const value = (e.currentTarget as HTMLInputElement).value; settings.transcodingBitrate = value === "" ? null : Number(value); }} placeholder="128" > </div> </div>{/if}
<br>
<div class="row"> <span>{t("settings.downloads")}</span> <input type="checkbox" checked={settings.downloads} onchange={handleDownloadsToggle} ></div>
{#if settings.downloads} <div class="sub-group" transition:slide={slideMotion()}> <div class="row"> <span>{t("common.songs")}</span> <span class="num">{offlineManager.downloadedSongIds.size}</span> </div> <div class="row"> <span>{t("common.albums")}</span> <span class="num">{offlineManager.downloadedAlbumIds.size} </span> </div> <div class="row"> <span>{t("common.artists")}</span> <span class="num">{offlineManager.downloadedArtistIds.size} </span> </div> <div class="row"> <span>{t("common.playlists")}</span> <span class="num">{offlineManager.downloadedPlaylistIds.size} </span> </div> <div class="row"> <span>{t("settings.auto_sync")}</span> <select bind:value={settings.downloadsAutoSyncMode}> <option value="none">{t("common.none")}</option> <option value="scrobbles"> {t("settings.auto_sync_scrobbles")} </option> <option value="all">{t("settings.auto_sync_all")}</option> </select> </div> <div class="row"> <span>{t("settings.auto_download")}</span> <input type="checkbox" bind:checked={settings.downloadsAuto}> </div> {#if settings.downloadsAuto} <div class="sub-group" transition:slide={slideMotion()}> {#if settings.downloadsAutoSongs > 6} <small class="row"> ⚠️ {t("settings.auto_download_high_warning")} </small> {/if} <div class="row"> <span >{t("settings.auto_download_songs")} (<span class="num">{settings.downloadsAutoSongs}</span>)</span > <div class="control"> <input type="range" min="1" max="16" step="1" bind:value={settings.downloadsAutoSongs} > </div> </div> <div class="row"> <span >{t("settings.auto_download_trigger")} (<span class="num" >{settings.downloadsAutoTrigger === 0 ? t("settings.auto_download_start") : `${settings.downloadsAutoTrigger}%`}</span >)</span > <div class="control"> <input type="range" min="0" max="80" step="5" bind:value={settings.downloadsAutoTrigger} > </div> </div> </div> {/if} <div class="row"> <span>{t("offline.auto_delete")}</span> <input type="checkbox" bind:checked={settings.downloadsAutoDelete}> </div> {#if settings.downloadsAutoDelete} <div class="sub-group" transition:slide={slideMotion()}> <small class="row"> ℹ️ {t("offline.auto_delete_startup_notice")} </small> <div class="row"> <span >{t("offline.auto_delete_days")} {settings.downloadsAutoDeleteDays} {t( settings.downloadsAutoDeleteDays === 1 ? "common.day" : "common.days", )}</span > <div class="control"> <input type="range" min="1" max="60" step="1" bind:value={settings.downloadsAutoDeleteDays} > </div> </div> </div> {/if} </div>{/if}