The previous .git directory was found gutted (only info/ remained), so history is unrecoverable locally. This commit restores version control with the tree exactly as it stands: the working app, the preview-tracks launcher on the artist hero, the scrollable-track-previews design bundle (docs + approved mockups), and the Phase 0 validation artifacts.
277 lines
6.8 KiB
Svelte
277 lines
6.8 KiB
Svelte
<script>
|
|
import { app, ui, network, playlistStore, refreshPlaylists, toggleSidebar } from "../lib/state.svelte.js";
|
|
import * as api from "../lib/api.js";
|
|
import Icon from "./Icon.svelte";
|
|
|
|
function navigate(id) {
|
|
ui.view = id;
|
|
if (id !== "library") ui.albumId = null;
|
|
}
|
|
|
|
async function newPlaylist() {
|
|
const name = `New Playlist ${playlistStore.playlists.length + 1}`;
|
|
const id = await api.createPlaylist(name);
|
|
await refreshPlaylists();
|
|
ui.view = "playlist";
|
|
ui.playlistId = id;
|
|
}
|
|
|
|
function openPlaylist(id) {
|
|
ui.view = "playlist";
|
|
ui.playlistId = id;
|
|
}
|
|
|
|
const items = [
|
|
{ id: "home", label: "Home", icon: "home" },
|
|
{ id: "search", label: "Search", icon: "search" },
|
|
{ id: "library", label: "Your Library", icon: "library" },
|
|
{ id: "downloads", label: "Downloads", icon: "download" },
|
|
{ id: "settings", label: "Settings", icon: "settings" },
|
|
];
|
|
|
|
</script>
|
|
|
|
<nav class="sidebar" class:collapsed={ui.sidebarCollapsed}>
|
|
<div class="sb-top">
|
|
<button
|
|
class="collapse-btn"
|
|
title={ui.sidebarCollapsed ? "Expand" : "Collapse"}
|
|
onclick={toggleSidebar}
|
|
>
|
|
<Icon name="chevron-left" size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
<ul>
|
|
{#each items as item (item.id)}
|
|
<li>
|
|
<button
|
|
class="nav-item"
|
|
class:active={ui.view === item.id}
|
|
title={ui.sidebarCollapsed ? item.label : null}
|
|
onclick={() => navigate(item.id)}
|
|
>
|
|
<span class="icon"><Icon name={item.icon} size={22} /></span>
|
|
<span class="label">{item.label}</span>
|
|
</button>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
|
|
<div class="playlists">
|
|
<div class="pl-head">
|
|
<span class="label">Playlists</span>
|
|
<button class="pl-new" title="New playlist" onclick={newPlaylist}>
|
|
<Icon name="plus" size={16} />
|
|
</button>
|
|
</div>
|
|
<div class="pl-list">
|
|
{#each playlistStore.playlists as pl (pl.id)}
|
|
<button
|
|
class="pl-item"
|
|
class:active={ui.view === "playlist" && ui.playlistId === pl.id}
|
|
onclick={() => openPlaylist(pl.id)}
|
|
>
|
|
<span class="pl-name">{pl.name}</span>
|
|
<span class="pl-count">{pl.track_count}</span>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
{#if app.connection === "online" && network.shareFiles > 0}
|
|
<div class="share-info" title="Files you share with the network">
|
|
sharing {network.shareFiles} files
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</nav>
|
|
|
|
<style>
|
|
.sidebar {
|
|
grid-area: sidebar;
|
|
background: var(--bg-base);
|
|
border-radius: var(--radius);
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 12px 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
ul {
|
|
list-style: none;
|
|
}
|
|
|
|
.sb-top {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
padding: 0 4px 6px;
|
|
}
|
|
|
|
.collapse-btn {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 6px;
|
|
display: grid;
|
|
place-items: center;
|
|
color: var(--text-secondary);
|
|
flex: 0 0 auto;
|
|
transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1), color 0.15s;
|
|
}
|
|
.collapse-btn:hover {
|
|
color: var(--text-primary);
|
|
background: var(--bg-highlight);
|
|
}
|
|
|
|
/* Label collapse: fades and zeroes width so icons stay put. */
|
|
.label {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
max-width: 160px;
|
|
opacity: 1;
|
|
transition: opacity 0.15s ease, max-width 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
/* ---- collapsed icon-rail state ---- */
|
|
.sidebar.collapsed .sb-top {
|
|
justify-content: center;
|
|
}
|
|
.sidebar.collapsed .collapse-btn {
|
|
transform: rotate(180deg);
|
|
}
|
|
.sidebar.collapsed .label {
|
|
opacity: 0;
|
|
max-width: 0;
|
|
}
|
|
.sidebar.collapsed .nav-item {
|
|
justify-content: center;
|
|
gap: 0;
|
|
padding: 10px 0;
|
|
}
|
|
.sidebar.collapsed .pl-head {
|
|
justify-content: center;
|
|
padding: 0 0 8px;
|
|
}
|
|
.sidebar.collapsed .pl-list {
|
|
display: none;
|
|
}
|
|
.sidebar.collapsed .footer {
|
|
opacity: 0;
|
|
}
|
|
|
|
.playlists {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
margin-top: 12px;
|
|
padding-top: 12px;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.pl-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 12px 8px;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.pl-new {
|
|
width: 22px;
|
|
height: 22px;
|
|
border-radius: 50%;
|
|
background: var(--bg-highlight);
|
|
color: var(--text-secondary);
|
|
font-size: 16px;
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
|
|
.pl-new:hover {
|
|
color: var(--text-primary);
|
|
background: var(--bg-press);
|
|
}
|
|
|
|
.pl-list {
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
}
|
|
|
|
.pl-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
width: 100%;
|
|
padding: 7px 12px;
|
|
border-radius: 5px;
|
|
color: var(--text-secondary);
|
|
font-size: 13px;
|
|
text-align: left;
|
|
}
|
|
|
|
.pl-item:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.pl-item.active {
|
|
background: var(--bg-highlight);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.pl-name {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.pl-count {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
border-radius: 6px;
|
|
color: var(--text-secondary);
|
|
font-weight: 600;
|
|
transition: color 0.15s;
|
|
}
|
|
|
|
.nav-item:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.nav-item.active {
|
|
color: var(--text-primary);
|
|
background: var(--bg-highlight);
|
|
}
|
|
|
|
.icon {
|
|
width: 22px;
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
|
|
.footer {
|
|
padding: 8px 4px;
|
|
}
|
|
|
|
.share-info {
|
|
padding: 2px 8px;
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
}
|
|
</style>
|