forked from Keelrleek/SoulStream
Baseline: full tree snapshot after git repository loss
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.
This commit is contained in:
commit
a667b4b28e
161 files changed
+53170
No files matched your search
@@ -0,0 +1,308 @@
|
||||
<script>
|
||||
import { downloads, network, playLocalFile } from "../lib/state.svelte.js";
|
||||
import * as api from "../lib/api.js";
|
||||
import Icon from "../components/Icon.svelte";
|
||||
|
||||
const uploads = $derived(
|
||||
Object.values(network.uploads)
|
||||
.sort((a, b) => b.id - a.id)
|
||||
.slice(0, 20),
|
||||
);
|
||||
|
||||
const items = $derived(
|
||||
Object.values(downloads.items).sort((a, b) => b.id - a.id),
|
||||
);
|
||||
|
||||
const active = $derived(
|
||||
items.filter((d) => !["complete", "failed", "cancelled"].includes(d.state)),
|
||||
);
|
||||
const finished = $derived(
|
||||
items.filter((d) => ["complete", "failed", "cancelled"].includes(d.state)),
|
||||
);
|
||||
|
||||
function pct(d) {
|
||||
return d.total > 0 ? Math.min(100, (d.received / d.total) * 100) : 0;
|
||||
}
|
||||
|
||||
function fmtSize(bytes) {
|
||||
if (bytes >= 1e9) return (bytes / 1e9).toFixed(2) + " GB";
|
||||
return (bytes / 1e6).toFixed(1) + " MB";
|
||||
}
|
||||
|
||||
function stateLabel(d) {
|
||||
switch (d.state) {
|
||||
case "connecting": return "connecting…";
|
||||
case "requested": return "requested…";
|
||||
case "queued":
|
||||
return d.queue_place != null ? `in queue (#${d.queue_place})` : "in queue";
|
||||
case "transferring":
|
||||
return `${fmtSize(d.received)} / ${fmtSize(d.total)} · ${(d.speed_bps / 1e6).toFixed(1)} MB/s`;
|
||||
case "complete": return "done";
|
||||
case "failed": return d.error || "failed";
|
||||
case "cancelled": return "cancelled";
|
||||
default: return d.state;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="downloads">
|
||||
<h1>Downloads</h1>
|
||||
|
||||
{#if network.shareFiles > 0}
|
||||
<div class="sharing-banner">
|
||||
<span class="dot"></span>
|
||||
Sharing {network.shareFiles} files with the network
|
||||
{#if network.respondedCount > 0}
|
||||
· answered {network.respondedCount} searches
|
||||
{/if}
|
||||
{#if uploads.length > 0}
|
||||
· {uploads.filter((u) => u.state === "transferring").length} uploading
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if uploads.length > 0}
|
||||
<h2>Uploads</h2>
|
||||
<div class="list">
|
||||
{#each uploads as u (u.id)}
|
||||
<div class="row">
|
||||
<div class="info">
|
||||
<div class="name">{u.name}</div>
|
||||
<div class="sub">
|
||||
to {u.username} ·
|
||||
<span class:ok={u.state === "complete"} class:err={u.state === "failed"}>
|
||||
{u.state === "transferring" && u.total > 0
|
||||
? `${((u.sent / u.total) * 100).toFixed(0)}%`
|
||||
: u.state}
|
||||
</span>
|
||||
</div>
|
||||
{#if u.state === "transferring"}
|
||||
<div class="bar">
|
||||
<div class="fill up" style="width: {u.total > 0 ? (u.sent / u.total) * 100 : 0}%"></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if items.length === 0}
|
||||
{#if uploads.length === 0}
|
||||
<div class="empty">
|
||||
<p class="big">No downloads yet</p>
|
||||
<p class="sub">Grab something from Search — files land in your library folder.</p>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{#if active.length > 0}
|
||||
<h2>Active</h2>
|
||||
<div class="list">
|
||||
{#each active as d (d.id)}
|
||||
<div class="row">
|
||||
<div class="info">
|
||||
<div class="name">{d.name}</div>
|
||||
<div class="sub">{d.username} · {stateLabel(d)}</div>
|
||||
<div class="bar">
|
||||
<div
|
||||
class="fill"
|
||||
class:indeterminate={d.state !== "transferring"}
|
||||
style="width: {d.state === 'transferring' ? pct(d) : 100}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="cancel" title="Cancel" onclick={() => api.cancelDownload(d.id)}>
|
||||
<Icon name="x" size={16} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if finished.length > 0}
|
||||
<h2>Finished</h2>
|
||||
<div class="list">
|
||||
{#each finished as d (d.id)}
|
||||
<div class="row done-row">
|
||||
<div class="info">
|
||||
<div class="name" class:dim={d.state !== "complete"}>{d.name}</div>
|
||||
<div class="sub">
|
||||
{d.username} ·
|
||||
<span class:ok={d.state === "complete"} class:err={d.state === "failed"}>
|
||||
{stateLabel(d)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{#if d.state === "complete" && d.local_path}
|
||||
<button
|
||||
class="reveal"
|
||||
title="Play"
|
||||
onclick={() =>
|
||||
playLocalFile(
|
||||
d.local_path,
|
||||
d.name.replace(/\.[^.]+$/, ""),
|
||||
d.username,
|
||||
)}
|
||||
><Icon name="play" size={15} /></button>
|
||||
<button
|
||||
class="reveal"
|
||||
title="Show in folder"
|
||||
onclick={() => api.showInFolder(d.local_path)}
|
||||
><Icon name="folder-open" size={16} /></button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin: 18px 0 10px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 15vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.big {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sub {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.row.done-row {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.row.done-row:hover {
|
||||
background: var(--bg-highlight);
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.name.dim {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.bar {
|
||||
margin-top: 8px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: hsla(0, 0%, 100%, 0.15);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
background: var(--accent);
|
||||
transition: width 0.25s linear;
|
||||
}
|
||||
|
||||
.fill.indeterminate {
|
||||
opacity: 0.25;
|
||||
animation: pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.fill.up {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.sharing-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--accent-soft);
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.sharing-banner .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
50% { opacity: 0.55; }
|
||||
}
|
||||
|
||||
.ok {
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
|
||||
.err {
|
||||
color: #f15e6c;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.cancel:hover {
|
||||
color: #f15e6c;
|
||||
}
|
||||
|
||||
.reveal {
|
||||
font-size: 15px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.reveal:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user