Files
shark/src/routes/albums/[artist]/[album]/+page.svelte

264 lines
6.0 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { convertFileSrc } from '@tauri-apps/api/core';
import { settings, loadSettings } from '$lib/stores/settings';
import { loadAlbumTracks, findAlbumArt } from '$lib/library/album';
import type { Track } from '$lib/types/track';
let tracks = $state<Track[]>([]);
let coverArtPath = $state<string | undefined>(undefined);
let loading = $state(true);
let error = $state<string | null>(null);
let selectedTrackIndex = $state<number | null>(null);
let artistName = $derived(decodeURIComponent($page.params.artist ?? ''));
let albumName = $derived(decodeURIComponent($page.params.album ?? ''));
onMount(async () => {
await loadSettings();
await loadAlbum();
});
async function loadAlbum() {
loading = true;
error = null;
if (!$settings.musicFolder) {
error = 'Music folder not configured. Please set it in Settings.';
loading = false;
return;
}
try {
const albumPath = `${$settings.musicFolder}/${artistName}/${albumName}`;
const [tracksData, coverPath] = await Promise.all([
loadAlbumTracks(albumPath),
findAlbumArt(albumPath)
]);
tracks = tracksData;
coverArtPath = coverPath;
if (tracks.length === 0) {
error = 'No tracks found in this album.';
}
loading = false;
} catch (e) {
error = 'Error loading album: ' + (e as Error).message;
loading = false;
}
}
function getThumbnailUrl(coverPath?: string): string {
if (!coverPath) {
return '';
}
return convertFileSrc(coverPath);
}
function handleTrackClick(index: number) {
selectedTrackIndex = index;
}
function getAlbumYear(): string {
const year = tracks.find(t => t.metadata.year)?.metadata.year;
return year ? year.toString() : '—';
}
</script>
<div class="album-wrapper">
{#if loading}
<p style="padding: 8px;">Loading album...</p>
{:else if error}
<p class="error" style="padding: 8px;">{error}</p>
{:else}
<!-- Album Header -->
<div class="album-header">
{#if coverArtPath}
<img
src={getThumbnailUrl(coverArtPath)}
alt="{albumName} cover"
class="album-cover"
/>
{:else}
<div class="album-cover-placeholder"></div>
{/if}
<div class="album-info">
<h2>{albumName}</h2>
<p class="album-artist">{artistName}</p>
<p class="album-meta">
{getAlbumYear()}{tracks.length} track{tracks.length !== 1 ? 's' : ''}
</p>
</div>
</div>
<section class="album-content">
<!-- Tabs (single tab for tracks) -->
<menu role="tablist">
<li role="tab" aria-selected={true}>
<button>Tracks</button>
</li>
</menu>
<!-- Track Listing -->
<div class="window tab-content" role="tabpanel">
<div class="window-body">
<div class="sunken-panel table-container">
<table class="interactive">
<thead>
<tr>
<th style="width: 50px;">#</th>
<th>Title</th>
<th>Duration</th>
<th>Format</th>
</tr>
</thead>
<tbody>
{#each tracks as track, i}
<tr
class:highlighted={selectedTrackIndex === i}
onclick={() => handleTrackClick(i)}
>
<td class="track-number">
{track.metadata.trackNumber ?? i + 1}
</td>
<td>{track.metadata.title || track.filename}</td>
<td class="duration">
{#if track.metadata.duration}
{Math.floor(track.metadata.duration / 60)}:{String(Math.floor(track.metadata.duration % 60)).padStart(2, '0')}
{:else}
{/if}
</td>
<td class="format">{track.format.toUpperCase()}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
</section>
{/if}
</div>
<style>
.album-wrapper {
height: 100%;
display: flex;
flex-direction: column;
}
.error {
color: #ff6b6b;
}
.album-header {
display: flex;
gap: 16px;
padding: 8px 8px 0 8px;
margin-bottom: 8px;
flex-shrink: 0;
}
.album-cover {
width: 160px;
height: 160px;
object-fit: cover;
image-rendering: auto;
flex-shrink: 0;
}
.album-cover-placeholder {
width: 160px;
height: 160px;
background: linear-gradient(135deg, #c0c0c0 25%, #808080 25%, #808080 50%, #c0c0c0 50%, #c0c0c0 75%, #808080 75%);
background-size: 8px 8px;
flex-shrink: 0;
}
.album-info {
display: flex;
flex-direction: column;
justify-content: center;
}
h2 {
margin: 0 0 4px 0;
font-size: 1.5em;
}
.album-artist {
margin: 0 0 8px 0;
font-size: 1.1em;
opacity: 0.8;
}
.album-meta {
margin: 0;
opacity: 0.6;
font-size: 0.9em;
}
.album-content {
margin: 0;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.tab-content {
margin-top: -2px;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.window-body {
padding: 0;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.table-container {
flex: 1;
overflow-y: auto;
min-height: 0;
}
table {
width: 100%;
}
th {
text-align: left;
}
.track-number {
text-align: center;
opacity: 0.6;
}
.duration {
font-family: monospace;
font-size: 0.9em;
text-align: center;
width: 80px;
}
.format {
font-family: monospace;
font-size: 0.85em;
text-transform: uppercase;
text-align: center;
width: 80px;
}
</style>