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:
@@ -1,16 +1,23 @@
|
||||
<script lang="ts">
|
||||
import '@fontsource-variable/manrope/index.css';
|
||||
import '@fontsource-variable/bricolage-grotesque/opsz.css';
|
||||
import '@fontsource-variable/sora/index.css';
|
||||
import '@fontsource-variable/plus-jakarta-sans/index.css';
|
||||
import '$lib/theme/theme.css';
|
||||
import './layout.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import PlayerBar from '$lib/components/player/PlayerBar.svelte';
|
||||
import PlayerDock from '$lib/components/player/PlayerDock.svelte';
|
||||
import Queue from '$lib/components/player/Queue.svelte';
|
||||
import Sidebar from '$lib/components/Sidebar.svelte';
|
||||
import TopBar from '$lib/components/TopBar.svelte';
|
||||
import MobileHeader from '$lib/components/MobileHeader.svelte';
|
||||
import MobileNav from '$lib/components/MobileNav.svelte';
|
||||
import Modal from '$lib/components/ui/Modal.svelte';
|
||||
import PlaylistForm from '$lib/components/ui/PlaylistForm.svelte';
|
||||
import { player } from '$lib/player/player.svelte';
|
||||
import { ACCENT_LIGHTNESS, ACCENT_CHROMA, ACCENT_HUE } from '$lib/theme/color';
|
||||
import { queuePanel } from '$lib/player/queuePanel.svelte';
|
||||
import { liked } from '$lib/player/liked.svelte';
|
||||
import { createPlaylistModal } from '$lib/playlists.svelte';
|
||||
import { ACCENT_HUE } from '$lib/theme/color';
|
||||
import { buildThemeTokens, applyThemeTokens } from '$lib/theme/tokens';
|
||||
import { themeMode } from '$lib/theme/mode.svelte';
|
||||
import { trackNavigation } from '$lib/navigation.svelte';
|
||||
import { page } from '$app/state';
|
||||
|
||||
@@ -18,9 +25,19 @@
|
||||
|
||||
trackNavigation();
|
||||
|
||||
$effect(() => {
|
||||
if (data.likedTracks?.length) liked.hydrate(data.likedTracks);
|
||||
});
|
||||
|
||||
const isAuthPage = $derived(page.url.pathname === '/login');
|
||||
const hasTrack = $derived(player.currentId !== null);
|
||||
|
||||
let hadTrack = false;
|
||||
$effect(() => {
|
||||
if (hasTrack && !hadTrack) queuePanel.show();
|
||||
hadTrack = hasTrack;
|
||||
});
|
||||
|
||||
function isTypingTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) return false;
|
||||
if (target.isContentEditable) return true;
|
||||
@@ -33,37 +50,30 @@
|
||||
player.toggle();
|
||||
}
|
||||
|
||||
function parseAccent(value: string): { c: number; h: number } | null {
|
||||
const m = /oklch\(\s*[\d.]+%?\s+([\d.]+)\s+([\d.]+)/.exec(value);
|
||||
return m ? { c: parseFloat(m[1]), h: parseFloat(m[2]) } : null;
|
||||
function parseHue(value: string): number | null {
|
||||
const m = /oklch\(\s*[\d.]+%?\s+[\d.]+\s+([\d.]+)/.exec(value);
|
||||
return m ? parseFloat(m[1]) : null;
|
||||
}
|
||||
|
||||
let displayed = { c: ACCENT_CHROMA, h: ACCENT_HUE };
|
||||
let displayedHue = ACCENT_HUE;
|
||||
let rafId = 0;
|
||||
|
||||
$effect(() => {
|
||||
const target = parseAccent(player.accent);
|
||||
if (!target) {
|
||||
document.documentElement.style.setProperty('--mf-accent', player.accent);
|
||||
return;
|
||||
}
|
||||
const targetHue = parseHue(player.accent) ?? ACCENT_HUE;
|
||||
const mode = themeMode.current;
|
||||
cancelAnimationFrame(rafId);
|
||||
const from = { ...displayed };
|
||||
let dh = target.h - from.h;
|
||||
const fromHue = displayedHue;
|
||||
let dh = targetHue - fromHue;
|
||||
if (dh > 180) dh -= 360;
|
||||
else if (dh < -180) dh += 360;
|
||||
const start = performance.now();
|
||||
const duration = 350;
|
||||
const duration = 600;
|
||||
const tick = (now: number) => {
|
||||
const t = Math.min(1, (now - start) / duration);
|
||||
const e = 1 - Math.pow(1 - t, 3);
|
||||
const c = from.c + (target.c - from.c) * e;
|
||||
const h = (((from.h + dh * e) % 360) + 360) % 360;
|
||||
displayed = { c, h };
|
||||
document.documentElement.style.setProperty(
|
||||
'--mf-accent',
|
||||
`oklch(${ACCENT_LIGHTNESS}% ${c.toFixed(3)} ${h.toFixed(1)})`
|
||||
);
|
||||
const eased = 1 - Math.pow(1 - t, 3);
|
||||
const hue = (((fromHue + dh * eased) % 360) + 360) % 360;
|
||||
displayedHue = hue;
|
||||
applyThemeTokens(buildThemeTokens(hue, mode));
|
||||
if (t < 1) rafId = requestAnimationFrame(tick);
|
||||
};
|
||||
rafId = requestAnimationFrame(tick);
|
||||
@@ -80,25 +90,53 @@
|
||||
{@render children()}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex min-h-screen bg-bg text-fg antialiased">
|
||||
<Sidebar user={data.user} />
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<MobileHeader user={data.user} />
|
||||
<TopBar user={data.user} />
|
||||
<main
|
||||
class="flex-1 transition-[padding]"
|
||||
style="padding-bottom:calc(var(--mf-nav-h) + var(--mf-safe-b) + {hasTrack
|
||||
? 'var(--mf-player-h)'
|
||||
: '0px'})"
|
||||
>
|
||||
{#key page.url.pathname}
|
||||
<div class="animate-fade">{@render children()}</div>
|
||||
{/key}
|
||||
</main>
|
||||
{#if hasTrack}
|
||||
<PlayerBar />
|
||||
{/if}
|
||||
<MobileNav user={data.user} />
|
||||
<div class="relative flex min-h-screen text-fg antialiased">
|
||||
<div
|
||||
class="pointer-events-none fixed inset-0 -z-10 bg-bg"
|
||||
style="background-image:var(--mf-ambient)"
|
||||
></div>
|
||||
<div
|
||||
class="pointer-events-none fixed inset-0 -z-10"
|
||||
style="background:radial-gradient(130% 85% at 50% 118%, color-mix(in oklch, var(--mf-scrim) 80%, transparent), transparent 62%)"
|
||||
></div>
|
||||
|
||||
<Sidebar user={data.user} playlists={data.sidebarPlaylists ?? []} />
|
||||
|
||||
<div class="flex min-w-0 flex-1">
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<MobileHeader user={data.user} accountUrl={data.accountUrl} />
|
||||
<TopBar user={data.user} accountUrl={data.accountUrl} />
|
||||
<main
|
||||
class="flex-1 transition-[padding]"
|
||||
style="padding-bottom:calc(var(--mf-nav-h) + var(--mf-safe-b) + {hasTrack
|
||||
? 'var(--mf-player-h)'
|
||||
: '0px'})"
|
||||
>
|
||||
{#key page.url.pathname}
|
||||
<div class="animate-fade">{@render children()}</div>
|
||||
{/key}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<Queue />
|
||||
</div>
|
||||
|
||||
<PlayerDock user={data.user} />
|
||||
|
||||
<Modal
|
||||
open={createPlaylistModal.open}
|
||||
onClose={() => createPlaylistModal.close()}
|
||||
title="Crear playlist"
|
||||
>
|
||||
<PlaylistForm
|
||||
action="/playlists?/create"
|
||||
formMessage={page.form?.message}
|
||||
submitLabel="Crear"
|
||||
submittingLabel="Creando…"
|
||||
helperText="Podrás añadir canciones más tarde."
|
||||
onCancel={() => createPlaylistModal.close()}
|
||||
onSuccess={() => createPlaylistModal.close()}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user