feat(ui): tabbed settings and onboarding prompts

This commit is contained in:
2025-10-01 10:40:10 -04:00
parent 6af3603c7d
commit de04dbc323
2 changed files with 180 additions and 99 deletions

View File

@@ -1,8 +1,38 @@
<script lang="ts">
import { settings } from '$lib/stores/settings';
</script>
<div> <div>
<img src="/Square150x150Logo.png" alt="cat" style="width: 128px; height: 128px;" /> <img src="/Square150x150Logo.png" alt="cat" style="width: 128px; height: 128px;" />
<h2>Welcome to Shark!</h2> <h2>Welcome to Shark!</h2>
<p>Your music library manager and player.</p> <p>Your music library manager and player.</p>
{#if !$settings.musicFolder}
<div class="window welcome-prompt">
<div class="title-bar">
<div class="title-bar-text">Ahoy!</div>
</div>
<div class="window-body">
<p>It seems you're new here! To get started, set up your music downloads folder.</p>
<div class="button-row">
<a href="/settings"><button>Go to Settings</button></a>
</div>
</div>
</div>
{:else if !$settings.playlistsFolder}
<div class="window welcome-prompt">
<div class="title-bar">
<div class="title-bar-text">Welcome Back!</div>
</div>
<div class="window-body">
<p>Head to settings to set up your playlists folder! Life is more fun with playlists.</p>
<div class="button-row">
<a href="/settings"><button>Go to Settings</button></a>
</div>
</div>
</div>
{/if}
<section class="quick-stats"> <section class="quick-stats">
<div class="field-row-stacked"> <div class="field-row-stacked">
</div> </div>
@@ -17,4 +47,20 @@
.quick-stats { .quick-stats {
margin-top: 20px; margin-top: 20px;
} }
.welcome-prompt {
margin-top: 20px;
max-width: 500px;
}
.welcome-prompt p {
line-height: 1.5;
letter-spacing: 0.01em;
word-spacing: normal;
}
.button-row {
margin-top: 12px;
text-align: center;
}
</style> </style>

View File

@@ -16,6 +16,7 @@
let currentDeezerConcurrency = $state<number>(1); let currentDeezerConcurrency = $state<number>(1);
let currentDeezerFormat = $state<'FLAC' | 'MP3_320' | 'MP3_128'>('FLAC'); let currentDeezerFormat = $state<'FLAC' | 'MP3_320' | 'MP3_128'>('FLAC');
let currentDeezerOverwrite = $state<boolean>(false); let currentDeezerOverwrite = $state<boolean>(false);
let activeTab = $state<'library' | 'deezer' | 'advanced'>('library');
onMount(async () => { onMount(async () => {
await loadSettings(); await loadSettings();
@@ -57,105 +58,140 @@
await setPlaylistsFolder(selected); await setPlaylistsFolder(selected);
} }
} }
async function clearAllPaths() {
await setMusicFolder(null);
await setPlaylistsFolder(null);
}
</script> </script>
<div> <div>
<h2>Settings</h2> <h2>Settings</h2>
<section class="library-content">
<h3>Library Folders</h3>
<p class="info-note">
<strong>Note:</strong> Music and Playlists folders should be in the same parent directory for playlists to work properly in other apps.
</p>
<div class="field-row-stacked"> <menu role="tablist">
<label for="music-folder">Music Folder</label> <li role="tab" aria-selected={activeTab === 'library'}>
<div class="folder-selector"> <a href="#library" onclick={(e) => { e.preventDefault(); activeTab = 'library'; }}>Library</a>
<input </li>
type="text" <li role="tab" aria-selected={activeTab === 'deezer'}>
id="music-folder" <a href="#deezer" onclick={(e) => { e.preventDefault(); activeTab = 'deezer'; }}>Deezer</a>
value={currentMusicFolder || 'No folder selected'} </li>
readonly <li role="tab" aria-selected={activeTab === 'advanced'}>
class="folder-path" <a href="#advanced" onclick={(e) => { e.preventDefault(); activeTab = 'advanced'; }}>Advanced</a>
/> </li>
<button onclick={selectMusicFolder}>Browse...</button> </menu>
</div>
{#if currentMusicFolder} <div class="window" role="tabpanel">
<small class="help-text">Currently using: {currentMusicFolder}</small> <div class="window-body">
{:else} {#if activeTab === 'library'}
<small class="help-text">No music folder selected. Click Browse to choose one.</small> <section class="tab-content">
<h3>Library Folders</h3>
<p class="info-note">
<strong>Note:</strong> Music and Playlists folders should be in the same parent directory for playlists to work properly in other apps.
</p>
<div class="field-row-stacked">
<label for="music-folder">Music Folder</label>
<div class="folder-selector">
<input
type="text"
id="music-folder"
value={currentMusicFolder || 'No folder selected'}
readonly
class="folder-path"
/>
<button onclick={selectMusicFolder}>Browse...</button>
</div>
{#if currentMusicFolder}
<small class="help-text">Currently using: {currentMusicFolder}</small>
{:else}
<small class="help-text">No music folder selected. Click Browse to choose one.</small>
{/if}
</div>
<div class="field-row-stacked">
<label for="playlists-folder">Playlists Folder</label>
<div class="folder-selector">
<input
type="text"
id="playlists-folder"
value={currentPlaylistsFolder || 'No folder selected'}
readonly
class="folder-path"
/>
<button onclick={selectPlaylistsFolder}>Browse...</button>
</div>
{#if currentPlaylistsFolder}
<small class="help-text">Currently using: {currentPlaylistsFolder}</small>
{:else}
<small class="help-text">No playlists folder selected. Click Browse to choose one.</small>
{/if}
</div>
</section>
{:else if activeTab === 'deezer'}
<section class="tab-content">
<h3>Deezer Download Settings</h3>
<div class="field-row-stacked">
<label for="deezer-format">Audio Quality</label>
<select
id="deezer-format"
bind:value={currentDeezerFormat}
onchange={() => setDeezerFormat(currentDeezerFormat)}
>
<option value="FLAC">FLAC (Lossless)</option>
<option value="MP3_320">MP3 320kbps</option>
<option value="MP3_128">MP3 128kbps</option>
</select>
<small class="help-text">Select the audio quality for downloaded tracks</small>
</div>
<div class="field-row-stacked">
<label for="deezer-concurrency">Download Concurrency</label>
<div class="slider-container">
<input
id="deezer-concurrency"
type="range"
min="1"
max="10"
bind:value={currentDeezerConcurrency}
onchange={() => setDeezerConcurrency(currentDeezerConcurrency)}
/>
<span class="slider-value">{currentDeezerConcurrency}</span>
</div>
<small class="help-text">Number of tracks to download simultaneously (default: 1)</small>
</div>
<div class="field-row-stacked">
<div class="field-row">
<input
id="deezer-overwrite"
type="checkbox"
bind:checked={currentDeezerOverwrite}
onchange={() => setDeezerOverwrite(currentDeezerOverwrite)}
/>
<label for="deezer-overwrite">Overwrite existing files</label>
</div>
</div>
</section>
{:else if activeTab === 'advanced'}
<section class="tab-content">
<h3>Advanced Settings</h3>
<div class="field-row-stacked">
<label>Clear All Paths</label>
<small class="help-text">This will reset your music and playlists folder paths. You'll need to set them up again.</small>
<button onclick={clearAllPaths}>Clear All Paths</button>
</div>
</section>
{/if} {/if}
</div> </div>
</div>
<div class="field-row-stacked">
<label for="playlists-folder">Playlists Folder</label>
<div class="folder-selector">
<input
type="text"
id="playlists-folder"
value={currentPlaylistsFolder || 'No folder selected'}
readonly
class="folder-path"
/>
<button onclick={selectPlaylistsFolder}>Browse...</button>
</div>
{#if currentPlaylistsFolder}
<small class="help-text">Currently using: {currentPlaylistsFolder}</small>
{:else}
<small class="help-text">No playlists folder selected. Click Browse to choose one.</small>
{/if}
</div>
</section>
<section class="library-content">
<h3>Deezer Download Settings</h3>
<div class="field-row-stacked">
<label for="deezer-format">Audio Quality</label>
<select
id="deezer-format"
bind:value={currentDeezerFormat}
onchange={() => setDeezerFormat(currentDeezerFormat)}
>
<option value="FLAC">FLAC (Lossless)</option>
<option value="MP3_320">MP3 320kbps</option>
<option value="MP3_128">MP3 128kbps</option>
</select>
<small class="help-text">Select the audio quality for downloaded tracks</small>
</div>
<div class="field-row-stacked">
<label for="deezer-concurrency">Download Concurrency (tracks within album/playlist)</label>
<div class="slider-container">
<input
id="deezer-concurrency"
type="range"
min="1"
max="10"
bind:value={currentDeezerConcurrency}
onchange={() => setDeezerConcurrency(currentDeezerConcurrency)}
/>
<span class="slider-value">{currentDeezerConcurrency}</span>
</div>
<small class="help-text">
Number of tracks to download simultaneously within collections (default: 1)
</small>
</div>
<div class="field-row">
<input
id="deezer-overwrite"
type="checkbox"
bind:checked={currentDeezerOverwrite}
onchange={() => setDeezerOverwrite(currentDeezerOverwrite)}
/>
<label for="deezer-overwrite">Overwrite existing files</label>
</div>
</section>
</div> </div>
<style> <style>
h2 { h2 {
margin-top: 0; margin-top: 0;
margin-bottom: 12px;
} }
h3 { h3 {
@@ -164,8 +200,12 @@
font-size: 1.1em; font-size: 1.1em;
} }
.library-content { menu[role="tablist"] {
margin-top: 20px; margin-bottom: 0;
}
.tab-content {
margin: 0;
} }
.info-note { .info-note {
@@ -173,7 +213,8 @@
padding: 12px; padding: 12px;
margin-bottom: 20px; margin-bottom: 20px;
border-left: 3px solid var(--button-highlight, #606060); border-left: 3px solid var(--button-highlight, #606060);
font-size: 0.9em; font-size: 11px;
line-height: 1.4;
} }
.info-note strong { .info-note strong {
@@ -202,17 +243,11 @@
.help-text { .help-text {
color: var(--text-color, #FFFFFF); color: var(--text-color, #FFFFFF);
opacity: 0.7; opacity: 0.7;
font-size: 0.9em; font-size: 11px;
line-height: 1.4;
} }
.field-row { .field-row-stacked .field-row {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 12px;
}
.field-row label {
margin: 0; margin: 0;
} }