Add likes, follows and playlist visibility; redesign web-player UI

Backend:
- New Likes feature (toggle/get liked tracks) and endpoints
- User follow/unfollow, listening stats, public user profile
- Playlist visibility (public/private) support
- Various auth and DTO/response adjustments to support the above

Frontend (web-player):
- New design system pass: theme tokens/mode (light/dark), glass menu,
  tabs bar, player dock/extras/queue components, mix tiles, artist link
- New routes: Liked, public user profile (/u/[id]), likes API proxy
- Search refactor: shared SearchBox component used by TopBar and
  MobileHeader (search now reachable on mobile), live results as you
  type instead of requiring Enter, dropped the "Buscando…" quick-result
  preview
- Removed now-unused components (MobileNav, CoverBadge, MixBentoTile,
  PlayPauseOverlay, SuggestionsTable, userDirectory)
- Design handoff reference docs added under web-player/design_handoff_musify_player
  and web-player/docs/design-system-refactor-plan.md
This commit is contained in:
2026-09-14 23:42:45 +02:00
parent 389b3b9244
commit dc1ba89a6d
166 changed files with 14271 additions and 1982 deletions
+41 -44
View File
@@ -30,9 +30,9 @@
let audioInput = $state<HTMLInputElement>();
const steps = [
{ key: 'upload', label: 'Subir', hint: 'Portada y audio al almacenamiento' },
{ key: 'process', label: 'Procesar', hint: 'Transcodificado para streaming' },
{ key: 'publish', label: 'Publicar', hint: 'Disponible en el catálogo' }
{ key: 'upload', label: 'Subir', hint: 'Portada y audio.' },
{ key: 'process', label: 'Procesar', hint: 'Transcodificado para streaming.' },
{ key: 'publish', label: 'Publicar', hint: 'Disponible en el catálogo.' }
];
const busy = $derived(status === 'uploading');
@@ -47,14 +47,8 @@
return ['idle', 'idle', 'idle'];
});
const dropClass = $derived(
dragging || audioName !== '' ? 'border-accent/60' : 'border-line hover:border-accent/60'
);
function stepClass(s: StepState) {
if (s === 'active') return 'border-accent/40 bg-accent/5';
if (s === 'done') return 'border-accent/40';
return '';
return s === 'idle' ? '' : 'bg-accent-tint';
}
function formatSize(bytes: number) {
@@ -112,37 +106,39 @@
subtitle="Añade una canción con su portada y título. Nosotros la procesamos para streaming."
/>
<ol class="mt-8 grid gap-3 sm:mt-10 sm:grid-cols-3 sm:gap-4">
<ol class="mt-7 grid gap-3 sm:mt-8 sm:grid-cols-3 sm:gap-4">
{#each steps as step, i (step.key)}
<li>
<Surface class={stepClass(stepStates[i])}>
<Surface padding="sm" class={stepClass(stepStates[i])}>
<div class="flex items-center gap-3">
<span
class="grid h-8 w-8 place-items-center rounded-full bg-accent-soft text-sm font-bold text-on-accent"
class="grid h-7 w-7 shrink-0 place-items-center rounded-full text-xs font-bold {stepStates[
i
] === 'idle'
? 'bg-white/[3.5%] text-fg-2'
: 'bg-accent-btn text-accent-soft'}"
>
{#if stepStates[i] === 'done'}
<Check class="h-4 w-4" strokeWidth={2.5} />
<Check class="h-3.5 w-3.5" strokeWidth={2.5} />
{:else}
{i + 1}
{/if}
</span>
<span class="text-fg">{step.label}</span>
<span class="text-sm font-medium text-fg">{step.label}</span>
</div>
<p class="mt-3 text-base text-muted">{step.hint}</p>
<p class="mt-2.5 text-xs text-muted">{step.hint}</p>
</Surface>
</li>
{/each}
</ol>
{#if status === 'done'}
<Surface padding="lg" class="mt-10 border-accent/30 bg-accent/10 text-center">
<span
class="mx-auto grid h-16 w-16 place-items-center rounded-full bg-accent-soft text-on-accent"
>
<Check class="h-8 w-8" strokeWidth={2.5} />
<Surface padding="lg" class="mt-10 bg-accent-tint text-center">
<span class="mx-auto grid h-14 w-14 place-items-center rounded-full bg-accent-soft text-ink">
<Check class="h-7 w-7" strokeWidth={2.5} />
</span>
<h2 class="mt-6 text-2xl font-semibold tracking-tight text-fg">¡Subida!</h2>
<p class="mt-3 text-fg-2">
<h2 class="mt-6 font-display text-2xl font-semibold tracking-[-0.02em] text-fg">¡Subida!</h2>
<p class="mt-3 text-sm text-fg-2">
«{publishedTitle}» se está procesando. Aparecerá lista para reproducir en tu biblioteca en
unos momentos.
</p>
@@ -184,7 +180,9 @@
}}
ondragleave={() => (dragging = false)}
ondrop={onDrop}
class="relative flex min-h-56 flex-col items-center justify-center rounded-panel border border-dashed bg-surface p-6 text-center transition sm:min-h-72 sm:p-10 {dropClass}"
class="relative flex min-h-48 flex-col items-center justify-center rounded-panel p-6 text-center transition sm:min-h-64 sm:p-10 {dragging
? 'bg-surface-2 ring-2 ring-accent/50'
: 'bg-surface'}"
>
<input
bind:this={audioInput}
@@ -192,33 +190,32 @@
name="audio"
accept="audio/*"
onchange={onAudioInput}
class="absolute inset-0 cursor-pointer opacity-0"
class="absolute inset-0 cursor-pointer opacity-0 focus:outline-none"
aria-label="Seleccionar archivo de audio"
/>
<span
class="grid h-16 w-16 place-items-center rounded-full bg-accent-soft text-on-accent sm:h-20 sm:w-20"
>
{#if audioName === ''}
<Headphones class="h-8 w-8 sm:h-9 sm:w-9" />
{:else}
<Music class="h-8 w-8 sm:h-9 sm:w-9" />
{/if}
</span>
{#if audioName === ''}
<p class="mt-5 text-fg sm:text-lg">Arrastra tu audio aquí o haz clic para elegir</p>
<p class="mt-2 text-sm text-muted">MP3, FLAC, WAV…</p>
<Headphones class="h-9 w-9 text-muted sm:h-10 sm:w-10" strokeWidth={1.4} />
{:else}
<p class="mt-5 max-w-full truncate text-fg sm:text-lg">
<Music class="h-9 w-9 text-accent-soft sm:h-10 sm:w-10" strokeWidth={1.4} />
{/if}
{#if audioName === ''}
<p class="mt-4 text-sm font-medium text-fg">
Arrastra tu audio aquí o haz clic para elegir
</p>
<p class="mt-1.5 text-xs text-muted">MP3, FLAC, WAV…</p>
{:else}
<p class="mt-4 max-w-full truncate text-sm font-medium text-fg">
{audioName}
</p>
<p class="mt-2 text-sm text-muted">{formatSize(audioSize)} · Listo para subir</p>
<p class="mt-1.5 text-xs text-muted">{formatSize(audioSize)} · Listo para subir</p>
{/if}
</div>
<div class="grid items-start gap-6 sm:grid-cols-[auto_1fr] sm:gap-8">
<Field label="Portada">
<label
class="relative grid h-36 w-36 cursor-pointer place-items-center overflow-hidden rounded-control border border-line bg-surface transition hover:border-accent/50 sm:h-44 sm:w-44"
class="relative grid h-36 w-36 cursor-pointer place-items-center overflow-hidden rounded-control transition sm:h-44 sm:w-44"
style={coverPreview === '' ? 'background:var(--mf-cover-grad)' : undefined}
>
<input
type="file"
@@ -231,7 +228,7 @@
{#if coverPreview !== ''}
<img src={coverPreview} alt="Portada" class="h-full w-full object-cover" />
{:else}
<ImageIcon class="h-12 w-12 text-muted" />
<ImageIcon class="h-11 w-11 text-white/70" strokeWidth={1.3} />
{/if}
</label>
</Field>
@@ -263,12 +260,12 @@
</Checkbox>
{#if status === 'uploading'}
<Surface>
<div class="flex items-center justify-between text-base">
<Surface padding="sm">
<div class="flex items-center justify-between text-sm">
<span class="font-medium text-fg">Subiendo y creando la pista…</span>
<span class="text-muted">un momento</span>
<span class="text-xs text-muted">un momento</span>
</div>
<div class="mt-4 h-2.5 overflow-hidden rounded-full bg-surface-2">
<div class="mt-3 h-2 overflow-hidden rounded-full bg-surface-2">
<div class="h-full w-full animate-pulse rounded-full bg-accent-soft"></div>
</div>
</Surface>