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
@@ -1,30 +1,71 @@
<script lang="ts">
import { player } from '$lib/player/player.svelte';
import { liked } from '$lib/player/liked.svelte';
import Cover from '$lib/components/ui/Cover.svelte';
import ExplicitBadge from '$lib/components/ui/ExplicitBadge.svelte';
import ArtistLink from '$lib/components/ui/ArtistLink.svelte';
import Heart from '@lucide/svelte/icons/heart';
interface Props {
compact?: boolean;
}
let { compact = false }: Props = $props();
const isLiked = $derived(player.current.id !== '' && liked.isLiked(player.current.id));
function toggleLike() {
if (!player.current.id) return;
liked.toggle({
id: player.current.id,
title: player.current.title,
artist: player.current.artist,
coverUrl: player.current.coverUrl,
source: player.current.source,
explicit: player.current.explicit,
ownerUserId: player.current.ownerUserId
});
}
</script>
<div class="flex min-w-0 items-center gap-2.5 md:gap-3">
<div class="flex min-w-0 items-center gap-2.5 md:gap-3.25">
{#if player.current.id}
<Cover
trackId={player.current.id}
src={player.current.coverUrl}
size="small"
alt={player.current.title}
class="h-11 w-11 shrink-0 rounded-lg shadow-[0_6px_16px_-6px_rgba(0,0,0,0.6)] md:h-13 md:w-13"
class="h-11 w-11 shrink-0 rounded-[11px] shadow-[0_8px_20px_rgba(0,0,0,0.5)] md:h-12 md:w-12"
/>
{:else}
<div
class="h-11 w-11 shrink-0 rounded-lg bg-surface shadow-[0_6px_16px_-6px_rgba(0,0,0,0.6)] md:h-13 md:w-13"
class="h-11 w-11 shrink-0 rounded-[11px] bg-surface shadow-[0_8px_20px_rgba(0,0,0,0.5)] md:h-12 md:w-12"
></div>
{/if}
<div class="min-w-0">
<div class="min-w-0 flex-1">
<div class="flex min-w-0 items-center gap-1.5">
{#if player.current.explicit}
<ExplicitBadge />
{/if}
<span class="truncate text-fg">{player.current.title}</span>
<span class="truncate text-[13px] font-semibold tracking-[-0.01em] text-fg"
>{player.current.title}</span
>
</div>
<div class="truncate text-sm text-muted">{player.current.artist || '—'}</div>
<ArtistLink
name={player.current.artist || '—'}
ownerUserId={player.current.ownerUserId}
class="mt-0.5 text-[11.5px] text-fg-3"
/>
</div>
{#if !compact}
<button
type="button"
onclick={toggleLike}
aria-label={isLiked ? 'Quitar de Me gusta' : 'Añadir a Me gusta'}
aria-pressed={isLiked}
class="shrink-0 transition hover:opacity-80 {isLiked ? 'text-accent' : 'text-fg-3'}"
>
<Heart class="h-4 w-4" fill={isLiked ? 'currentColor' : 'none'} strokeWidth={1.7} />
</button>
{/if}
</div>