feat(ui): dynamically load playlists and artists in library

This commit is contained in:
2025-09-30 20:21:29 -04:00
parent a8f8e4602a
commit b5d14a71d6
5 changed files with 185 additions and 20 deletions

View File

@@ -0,0 +1,72 @@
import { readDir } from '@tauri-apps/plugin-fs';
export interface Artist {
name: string;
path: string;
}
export interface Playlist {
name: string;
path: string;
}
/**
* Scan music folder for artists (each folder is an artist)
*/
export async function scanArtists(musicFolderPath: string): Promise<Artist[]> {
try {
const entries = await readDir(musicFolderPath);
const artists: Artist[] = [];
for (const entry of entries) {
if (entry.isDirectory) {
artists.push({
name: entry.name,
path: `${musicFolderPath}/${entry.name}`
});
}
}
// Sort alphabetically
artists.sort((a, b) => a.name.localeCompare(b.name));
return artists;
} catch (error) {
console.error('Error scanning artists:', error);
return [];
}
}
/**
* Scan playlists folder for m3u/m3u8 files
*/
export async function scanPlaylists(playlistsFolderPath: string): Promise<Playlist[]> {
try {
const entries = await readDir(playlistsFolderPath);
const playlists: Playlist[] = [];
for (const entry of entries) {
if (!entry.isDirectory) {
const isPlaylist = entry.name.endsWith('.m3u') || entry.name.endsWith('.m3u8');
if (isPlaylist) {
// Remove extension for display name
const nameWithoutExt = entry.name.replace(/\.(m3u8?|M3U8?)$/, '');
playlists.push({
name: nameWithoutExt,
path: `${playlistsFolderPath}/${entry.name}`
});
}
}
}
// Sort alphabetically
playlists.sort((a, b) => a.name.localeCompare(b.name));
return playlists;
} catch (error) {
console.error('Error scanning playlists:', error);
return [];
}
}

View File

@@ -1,7 +1,31 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte';
import TitleBar from "$lib/TitleBar.svelte"; import TitleBar from "$lib/TitleBar.svelte";
import MenuBar from "$lib/MenuBar.svelte"; import MenuBar from "$lib/MenuBar.svelte";
import ToolBar from "$lib/ToolBar.svelte"; import ToolBar from "$lib/ToolBar.svelte";
import { settings, loadSettings } from '$lib/stores/settings';
import { scanPlaylists, type Playlist } from '$lib/library/scanner';
let { children } = $props();
let playlists = $state<Playlist[]>([]);
onMount(async () => {
await loadSettings();
await loadPlaylists();
});
async function loadPlaylists() {
if (!$settings.playlistsFolder) {
return;
}
try {
playlists = await scanPlaylists($settings.playlistsFolder);
} catch (e) {
console.error('Error loading playlists:', e);
}
}
</script> </script>
<div class="app-container"> <div class="app-container">
@@ -44,31 +68,31 @@
</a> </a>
</div> </div>
</details> </details>
<details class="nav-collapsible"> <details class="nav-collapsible" open>
<summary class="nav-item"> <summary class="nav-item">
<img src="/icons/cassette-tape.png" alt="" class="nav-icon" /> <img src="/icons/cassette-tape.png" alt="" class="nav-icon" />
Playlists Playlists
</summary> </summary>
<div class="nav-submenu"> <div class="nav-submenu">
<a href="/playlists/favorites" class="nav-item nav-subitem"> {#if playlists.length === 0}
<div class="nav-item nav-subitem" style="opacity: 0.5;">
No playlists found
</div>
{:else}
{#each playlists as playlist}
<a href="/playlists/{encodeURIComponent(playlist.name)}" class="nav-item nav-subitem">
<img src="/icons/eighthnote-white.svg" alt="" class="nav-icon" /> <img src="/icons/eighthnote-white.svg" alt="" class="nav-icon" />
Favorites {playlist.name}
</a>
<a href="/playlists/recently-played" class="nav-item nav-subitem">
<img src="/icons/eighthnote-white.svg" alt="" class="nav-icon" />
Recently Played
</a>
<a href="/playlists/workout" class="nav-item nav-subitem">
<img src="/icons/eighthnote-white.svg" alt="" class="nav-icon" />
Workout Mix
</a> </a>
{/each}
{/if}
</div> </div>
</details> </details>
</nav> </nav>
</aside> </aside>
<main class="content-area sunken-panel"> <main class="content-area sunken-panel">
<slot /> {@render children?.()}
</main> </main>
</div> </div>

View File

@@ -1,4 +1,5 @@
<div> <div>
<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>

View File

@@ -1,12 +1,66 @@
<script lang="ts">
import { onMount } from 'svelte';
import { settings, loadSettings } from '$lib/stores/settings';
import { scanArtists, type Artist } from '$lib/library/scanner';
let artists = $state<Artist[]>([]);
let loading = $state(true);
let error = $state<string | null>(null);
onMount(async () => {
await loadSettings();
await loadArtists();
});
async function loadArtists() {
loading = true;
error = null;
if (!$settings.musicFolder) {
error = 'No music folder configured. Please set one in Settings.';
loading = false;
return;
}
try {
artists = await scanArtists($settings.musicFolder);
loading = false;
} catch (e) {
error = 'Error loading artists: ' + (e as Error).message;
loading = false;
}
}
</script>
<div> <div>
<h2>Library</h2> <h2>Library</h2>
<p>Your music collection</p>
{#if loading}
<p>Loading artists...</p>
{:else if error}
<p class="error">{error}</p>
{:else if artists.length === 0}
<p>No artists found in your music folder.</p>
{:else}
<section class="library-content"> <section class="library-content">
<div class="field-row-stacked"> <table>
<!-- Library content will go here --> <thead>
</div> <tr>
<th>Artist</th>
<th>Path</th>
</tr>
</thead>
<tbody>
{#each artists as artist}
<tr>
<td>{artist.name}</td>
<td class="path">{artist.path}</td>
</tr>
{/each}
</tbody>
</table>
</section> </section>
{/if}
</div> </div>
<style> <style>
@@ -17,4 +71,18 @@
.library-content { .library-content {
margin-top: 20px; margin-top: 20px;
} }
.error {
color: #ff6b6b;
}
table {
width: 100%;
}
.path {
font-family: monospace;
font-size: 0.9em;
opacity: 0.7;
}
</style> </style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB