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">
|
<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}
|
||||||
<img src="/icons/eighthnote-white.svg" alt="" class="nav-icon" />
|
<div class="nav-item nav-subitem" style="opacity: 0.5;">
|
||||||
Favorites
|
No playlists found
|
||||||
</a>
|
</div>
|
||||||
<a href="/playlists/recently-played" class="nav-item nav-subitem">
|
{:else}
|
||||||
<img src="/icons/eighthnote-white.svg" alt="" class="nav-icon" />
|
{#each playlists as playlist}
|
||||||
Recently Played
|
<a href="/playlists/{encodeURIComponent(playlist.name)}" class="nav-item nav-subitem">
|
||||||
</a>
|
<img src="/icons/eighthnote-white.svg" alt="" class="nav-icon" />
|
||||||
<a href="/playlists/workout" class="nav-item nav-subitem">
|
{playlist.name}
|
||||||
<img src="/icons/eighthnote-white.svg" alt="" class="nav-icon" />
|
</a>
|
||||||
Workout Mix
|
{/each}
|
||||||
</a>
|
{/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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|
||||||
<section class="library-content">
|
{#if loading}
|
||||||
<div class="field-row-stacked">
|
<p>Loading artists...</p>
|
||||||
<!-- Library content will go here -->
|
{:else if error}
|
||||||
</div>
|
<p class="error">{error}</p>
|
||||||
</section>
|
{: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>
|
</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>
|
||||||
|
|||||||
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