mirror of
https://github.com/markuryy/shark.git
synced 2025-12-12 19:51:01 +00:00
feat(ui): tabbed settings and onboarding prompts
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user