mirror of
https://github.com/markuryy/shark.git
synced 2025-12-12 11:41:02 +00:00
feat(ui): dynamically load playlists and artists in library
This commit is contained in:
72
src/lib/library/scanner.ts
Normal file
72
src/lib/library/scanner.ts
Normal 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 [];
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import TitleBar from "$lib/TitleBar.svelte";
|
||||
import MenuBar from "$lib/MenuBar.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>
|
||||
|
||||
<div class="app-container">
|
||||
@@ -44,31 +68,31 @@
|
||||
</a>
|
||||
</div>
|
||||
</details>
|
||||
<details class="nav-collapsible">
|
||||
<details class="nav-collapsible" open>
|
||||
<summary class="nav-item">
|
||||
<img src="/icons/cassette-tape.png" alt="" class="nav-icon" />
|
||||
Playlists
|
||||
</summary>
|
||||
<div class="nav-submenu">
|
||||
<a href="/playlists/favorites" class="nav-item nav-subitem">
|
||||
<img src="/icons/eighthnote-white.svg" alt="" class="nav-icon" />
|
||||
Favorites
|
||||
</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>
|
||||
{#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" />
|
||||
{playlist.name}
|
||||
</a>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</details>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="content-area sunken-panel">
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<div>
|
||||
<img src="/Square150x150Logo.png" alt="cat" style="width: 128px; height: 128px;" />
|
||||
<h2>Welcome to Shark!</h2>
|
||||
<p>Your music library manager and player.</p>
|
||||
|
||||
|
||||
@@ -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>
|
||||
<h2>Library</h2>
|
||||
<p>Your music collection</p>
|
||||
|
||||
<section class="library-content">
|
||||
<div class="field-row-stacked">
|
||||
<!-- Library content will go here -->
|
||||
</div>
|
||||
</section>
|
||||
{#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">
|
||||
<table>
|
||||
<thead>
|
||||
<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>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -17,4 +71,18 @@
|
||||
.library-content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.path {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
BIN
static/Square150x150Logo.png
Normal file
BIN
static/Square150x150Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
Reference in New Issue
Block a user