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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227<script lang="ts">import { logout } from "../api/auth.svelte.js";import { api } from "../api/client.svelte.js";import { deploymentConfig } from "../api/connection.js";import { installPwa, pwa } from "../app/pwa.svelte.js";import { t } from "../languages/translate.svelte.js";import { offlineManager } from "../offline/offlineManager.svelte.js";import { ui } from "../ui/state.svelte.js";import Modal from "./Modal.svelte";import SettingsAppearanceTab from "./SettingsAppearanceTab.svelte";import SettingsColumnsTab from "./SettingsColumnsTab.svelte";import SettingsDataTab from "./SettingsDataTab.svelte";import SettingsGeneralTab from "./SettingsGeneralTab.svelte";
const tabs = ["general", "appearance", "columns", "data"] as const;type Tab = (typeof tabs)[number];
let activeTab = $state<Tab>("general");
let scanBusy = $state(false);
async function handleScan() { scanBusy = true; try { await api.startScan(); } catch (err) { console.error("failed to start scan:", err); } finally { scanBusy = false; }}</script>
<Modal bind:open={ui.showSettings} title={t("common.settings")} class="settings-modal"> <div class="body"> <header> {#each tabs as tab} <button type="button" class="tab pressable pressable--gentle" class:active={activeTab === tab} onclick={() => (activeTab = tab)} > {t(`settings.${tab}`)} </button> {/each} </header>
<div class="tab-panel"> {#if activeTab === "general"} <SettingsGeneralTab /> {:else if activeTab === "appearance"} <SettingsAppearanceTab /> {:else if activeTab === "columns"} <SettingsColumnsTab /> {:else if activeTab === "data"} <SettingsDataTab /> {/if} </div> </div>
<footer> <span> {#if deploymentConfig.name} {deploymentConfig.name} /{" "} {/if} <a target="_blank" href="https://tangled.org/devins.page/tinysub"> tinysub 2.80.3 </a> </span> <div class="actions"> {#if pwa.installPrompt} <button class="native" type="button" onclick={installPwa}> {t("common.install")} </button> {/if} {#if !offlineManager.isOfflineMode} <button class="native" type="button" disabled={scanBusy} onclick={handleScan} > {t("settings.scan")} </button> {/if} <button class="native" type="button" onclick={() => { if (confirm(t("message.logout_confirm"))) void logout(); }} > {t("common.logout")} </button> </div> </footer></Modal>
<style>.body { display: flex; flex-direction: column; flex: 1; overflow: auto;
@container (width > 32rem) { flex-direction: row; }}header { display: flex; gap: 0.25rem; overflow: auto hidden; border-block-end: var(--border-subtle); padding: 0.5rem 1rem 0;
@container (width > 32rem) { gap: 0; flex-direction: column; overflow: hidden auto; border-block-end: none; border-inline-end: var(--border-subtle); padding: 0; inline-size: 12rem; }}.tab { --button-transform-origin: center bottom; border-radius: var(--radius-sm) var(--radius-sm) 0 0; corner-shape: squircle; padding: 0.25rem 0.75rem; white-space: nowrap;
&.active { background-color: var(--selected-background); color: var(--selected-text); }
@container (width <= 32rem) { background-color: var(--layer); }
@container (width > 32rem) { --button-transform-origin: left center; text-align: start;
:global(:dir(rtl)) & { --button-transform-origin: right center; } padding: 0.5rem 1rem;
border-radius: 0; }}:global(dialog.settings-modal) { display: flex; flex-direction: column; inline-size: min(80dvw, 48rem); block-size: min(80dvh, 32rem); container-type: inline-size;}@media (width <= 32rem) { :global(dialog.settings-modal) { inline-size: var(--viewport-inline-size); block-size: var(--viewport-block-size); }}.tab-panel { flex: 1; overflow-y: auto; padding-block: 0.5rem; container-type: inline-size;}.body :global(.row) { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; min-block-size: 2rem; padding-inline: 1rem;}.body :global(.control) { display: flex;}.body :global(.sub-group) { padding-inline-start: 1rem;}.body :global(.num) { font-family: var(--font-family-monospace);}.body :global(h3) { border-block-end: var(--border-text); padding-block-end: 0.25rem; padding-inline: 1rem; margin-block-end: 0.5rem;}.body :global(small) { opacity: 0.75;}.body :global(.row input),.body :global(.row select),.body :global(.control input) { max-inline-size: min(10rem, 40cqw);}footer { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: 0.5rem 1rem; border-block-start: var(--border-subtle);}footer a { text-decoration: none;
&:hover { text-decoration: underline; }}</style>