This commit is contained in:
2026-07-25 01:46:59 +02:00
parent a58464d523
commit f54ebec0eb
66 changed files with 3245 additions and 416 deletions
@@ -18,7 +18,7 @@
</script>
<svelte:head>
<title>Álbumes · Musify</title>
<title>Álbumes</title>
<meta name="description" content="Los álbumes que has publicado con tu música subida." />
</svelte:head>
@@ -38,7 +38,9 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch }) => {
(track) => track.source === 'Local' && !inAlbum.has(track.id)
);
return { album, tracks, library, isOwner: Number(album.ownerUserId) === Number(user.sub) };
const isOwner = Number(album.ownerUserId) === Number(user.sub);
return { album, tracks, library, isOwner, section: isOwner ? '/albums' : null };
};
export const actions: Actions = {
@@ -31,13 +31,12 @@
</script>
<svelte:head>
<title>{album.title} · Musify</title>
<meta name="description" content="Álbum {album.title} en Musify." />
<title>{album.title}</title>
<meta name="description" content="Álbum {album.title}." />
</svelte:head>
<Page>
<AlbumHeader
id={album.id}
title={album.title}
description={album.description ?? undefined}
releaseYear={album.releaseYear === null ? undefined : Number(album.releaseYear)}
@@ -1,15 +1,13 @@
<script lang="ts">
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
import Play from '@lucide/svelte/icons/play';
import Pause from '@lucide/svelte/icons/pause';
import SquarePencil from '@lucide/svelte/icons/square-pen';
import Trash from '@lucide/svelte/icons/trash';
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
import BackLink from '$lib/components/ui/BackLink.svelte';
import Button from '$lib/components/ui/Button.svelte';
import { hueFor } from '$lib/theme/color';
interface Props {
id: string;
title: string;
description?: string;
releaseYear?: number;
@@ -22,7 +20,6 @@
}
let {
id,
title,
description,
releaseYear,
@@ -44,20 +41,14 @@
);
</script>
<a
href="/albums"
class="group inline-flex items-center text-fg-3 transition-colors hover:text-accent-soft"
>
<span class="grid h-8 w-8 place-items-center transition-colors group-hover:text-accent-soft">
<ArrowLeft class="h-4 w-4" />
</span>
Volver a tus álbumes
</a>
<BackLink
href={isOwner ? '/albums' : '/'}
label={isOwner ? 'Volver a tus álbumes' : 'Volver al inicio'}
/>
<div class="mt-5 flex flex-col gap-5 sm:mt-6 sm:flex-row sm:items-end sm:gap-6">
<PlaylistArt
{trackIds}
hue={hueFor(id)}
size="large"
class="h-36 w-36 shrink-0 rounded-art-lg shadow-art-lg sm:h-44 sm:w-44"
/>
@@ -7,7 +7,6 @@
toQueueItems,
type ApiTrackLike
} from '$lib/player/player.svelte';
import { hueFor } from '$lib/theme/color';
import { fmtTime } from '$lib/format';
import IconButton from '$lib/components/ui/IconButton.svelte';
import TrackTable from '$lib/components/ui/TrackTable.svelte';
@@ -43,7 +42,6 @@
trackId={track.id}
title={track.title}
artist={track.artist}
hue={hueFor(track.id)}
explicit={track.isExplicit}
onClick={() => playFrom(i)}
/>
@@ -2,7 +2,6 @@
import { enhance } from '$app/forms';
import Plus from '@lucide/svelte/icons/plus';
import { player, toQueueItems, type ApiTrackLike } from '$lib/player/player.svelte';
import { hueFor } from '$lib/theme/color';
import { fmtTime } from '$lib/format';
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
import IconButton from '$lib/components/ui/IconButton.svelte';
@@ -34,7 +33,6 @@
trackId={track.id}
title={track.title}
artist={track.artist}
hue={hueFor(track.id)}
explicit={track.isExplicit}
onClick={() => playFrom(i)}
>
@@ -18,7 +18,7 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch }) => {
const album = unwrapOrError(albumRes, 'No se pudo cargar el álbum de YouTube Music.', 404);
return { album, playlists: playlistsRes.data?.items ?? [] };
return { album, playlists: playlistsRes.data?.items ?? [], section: null };
};
export const actions: Actions = {
@@ -1,11 +1,10 @@
<script lang="ts">
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
import Play from '@lucide/svelte/icons/play';
import Pause from '@lucide/svelte/icons/pause';
import { player, type QueueItem } from '$lib/player/player.svelte';
import { hueFor } from '$lib/theme/color';
import { fmtTime } from '$lib/format';
import Page from '$lib/components/ui/Page.svelte';
import BackLink from '$lib/components/ui/BackLink.svelte';
import Button from '$lib/components/ui/Button.svelte';
import Cover from '$lib/components/ui/Cover.svelte';
import Alert from '$lib/components/ui/Alert.svelte';
@@ -73,26 +72,17 @@
</script>
<svelte:head>
<title>{album.title} · Musify</title>
<meta name="description" content="Álbum {album.title} de {album.artist} en YouTube Music." />
<title>{album.title}</title>
<meta name="description" content="Álbum {album.title} de {album.artist}," />
</svelte:head>
<Page>
<a
href="/explore"
class="group inline-flex items-center text-fg-3 transition-colors hover:text-accent-soft"
>
<span class="grid h-8 w-8 place-items-center transition-colors group-hover:text-accent-soft">
<ArrowLeft class="h-4 w-4" />
</span>
Volver a explorar
</a>
<BackLink href="/explore" label="Volver a explorar" />
<div class="mt-5 flex flex-col gap-5 sm:mt-6 sm:flex-row sm:items-end sm:gap-6">
<Cover
trackId={album.albumId}
src={album.thumbnailUrl}
hue={hueFor(album.albumId)}
size="large"
alt={album.title}
class="h-36 w-36 shrink-0 rounded-art-lg shadow-art-lg sm:h-44 sm:w-44"
@@ -100,7 +90,7 @@
/>
<div class="min-w-0 flex-1">
<p class="text-sm tracking-[0.14em] text-fg-3 uppercase">
{album.isSingle ? 'Single' : album.isEp ? 'EP' : 'Álbum'} · YouTube Music
{album.isSingle ? 'Single' : album.isEp ? 'EP' : 'Álbum'}
</p>
<h1
class="mt-1.5 text-3xl font-semibold tracking-tight break-words text-fg sm:text-5xl md:text-7xl"
@@ -154,7 +144,6 @@
title={track.title}
artist={album.artist}
coverSrc={album.thumbnailUrl}
hue={hueFor(track.videoId)}
explicit={track.isExplicit}
onClick={() => playFrom(i)}
/>
@@ -19,8 +19,7 @@
import NowPlaying from '$lib/components/ui/NowPlaying.svelte';
import ExplicitBadge from '$lib/components/ui/ExplicitBadge.svelte';
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
import { EXPLORE_PAGE_SIZE, SEARCH_DEBOUNCE_MS } from '$lib/config';
import { hueFor } from '$lib/theme/color';
import { EXPLORE_ALBUMS_LIMIT, EXPLORE_PAGE_SIZE, SEARCH_DEBOUNCE_MS } from '$lib/config';
import type { YouTubeSong } from '$lib/types';
import TrackContextMenu, {
contextMenuStateFor,
@@ -113,10 +112,12 @@
type YouTubeAlbum = (typeof data.youtubeAlbums)[number];
type AlbumEntry = { kind: 'local'; album: LocalAlbum } | { kind: 'youtube'; album: YouTubeAlbum };
const albumEntries = $derived<AlbumEntry[]>([
...albums.map((album) => ({ kind: 'local' as const, album })),
...youtubeAlbums.map((album) => ({ kind: 'youtube' as const, album }))
]);
const albumEntries = $derived<AlbumEntry[]>(
[
...albums.map((album) => ({ kind: 'local' as const, album })),
...youtubeAlbums.map((album) => ({ kind: 'youtube' as const, album }))
].slice(0, EXPLORE_ALBUMS_LIMIT)
);
const hasAlbums = $derived(albumEntries.length > 0);
const hasUsers = $derived(users.length > 0);
@@ -215,7 +216,7 @@
</script>
<svelte:head>
<title>Buscar · Musify</title>
<title>Buscar</title>
<meta name="description" content="Busca canciones, álbumes y usuarios en Musify." />
</svelte:head>
@@ -261,7 +262,6 @@
<div class="relative">
<PlaylistArt
trackIds={entry.album.coverTrackIds}
hue={hueFor(entry.album.id)}
class="aspect-square w-full rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:shadow-art-lg"
/>
</div>
@@ -285,7 +285,6 @@
<Cover
trackId={entry.album.albumId}
src={entry.album.thumbnailUrl}
hue={hueFor(entry.album.albumId)}
size="large"
alt={entry.album.title}
class="aspect-square w-full rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:shadow-art-lg"
@@ -316,7 +315,7 @@
class="group/artist animate-enter shrink-0"
style="animation-delay:{Math.min(i, 10) * 45}ms"
>
<ArtistAvatar id={u.id} name={u.name} size={96} />
<ArtistAvatar name={u.name} size={96} />
<div class="mt-1 w-24 truncate text-center text-xs text-fg-3">@{u.handle}</div>
</div>
{/each}
@@ -339,7 +338,6 @@
<Cover
trackId={targetId(item)}
src={targetCoverSrc(item)}
hue={hueFor(targetId(item))}
size="small"
alt={targetTitle(item)}
class="h-11 w-11 shrink-0 rounded-control shadow-art ring-1 ring-line ring-inset"
@@ -4,7 +4,6 @@
import X from '@lucide/svelte/icons/x';
import Music from '@lucide/svelte/icons/music';
import { player, toQueueItems } from '$lib/player/player.svelte';
import { hueFor } from '$lib/theme/color';
import { fmtTime, fmtDate } from '$lib/format';
import Page from '$lib/components/ui/Page.svelte';
import PageHeader from '$lib/components/ui/PageHeader.svelte';
@@ -28,7 +27,7 @@
</script>
<svelte:head>
<title>Tus canciones subidas · Musify</title>
<title>Tus canciones subidas</title>
<meta name="description" content="Todas tus canciones subidas a Musify." />
</svelte:head>
@@ -65,12 +64,7 @@
playing={player.playing}
onToggle={() => togglePlay(i)}
/>
<TrackTitleCell
trackId={track.id}
title={track.title}
hue={hueFor(track.id)}
onClick={() => togglePlay(i)}
/>
<TrackTitleCell trackId={track.id} title={track.title} onClick={() => togglePlay(i)} />
<TrackMeta class="hidden sm:block">{fmtDate(track.createdAt)}</TrackMeta>
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
<TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta>
@@ -1,13 +1,12 @@
<script lang="ts">
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
import Play from '@lucide/svelte/icons/play';
import Pause from '@lucide/svelte/icons/pause';
import Shuffle from '@lucide/svelte/icons/shuffle';
import { player } from '$lib/player/player.svelte';
import { hueFor } from '$lib/theme/color';
import { fmtTime } from '$lib/format';
import { shuffle } from '$lib/collections';
import Page from '$lib/components/ui/Page.svelte';
import BackLink from '$lib/components/ui/BackLink.svelte';
import Button from '$lib/components/ui/Button.svelte';
import Alert from '$lib/components/ui/Alert.svelte';
import MixArt from '$lib/components/ui/MixArt.svelte';
@@ -61,27 +60,15 @@
</script>
<svelte:head>
<title>{mix.title} · Musify</title>
<title>{mix.title}</title>
<meta name="description" content="Mezcla {mix.title}, hecha para ti." />
</svelte:head>
<Page>
<a
href="/"
class="group inline-flex items-center text-fg-3 transition-colors hover:text-accent-soft"
>
<span class="grid h-8 w-8 place-items-center transition-colors group-hover:text-accent-soft">
<ArrowLeft class="h-4 w-4" />
</span>
Volver al inicio
</a>
<BackLink href="/" label="Volver al inicio" />
<div class="mt-5 flex flex-col gap-5 sm:mt-6 sm:flex-row sm:items-end sm:gap-6">
<MixArt
{items}
hue={hueFor(mix.id)}
class="h-36 w-36 shrink-0 rounded-art-lg shadow-art-lg sm:h-44 sm:w-44"
/>
<MixArt {items} class="h-36 w-36 shrink-0 rounded-art-lg shadow-art-lg sm:h-44 sm:w-44" />
<div class="min-w-0 flex-1">
<p class="text-sm tracking-[0.14em] text-fg-3 uppercase">Mezcla</p>
<h1
@@ -133,7 +120,6 @@
title={targetTitle(targets[i])}
artist={targetArtist(targets[i])}
coverSrc={targetCoverSrc(targets[i])}
hue={hueFor(mixItemKey(item))}
explicit={item.isExplicit}
onClick={() => playFrom(i)}
/>
@@ -41,7 +41,7 @@
</script>
<svelte:head>
<title>Mis listas · Musify</title>
<title>Mis listas</title>
<meta name="description" content="Mis listas en Musify." />
</svelte:head>
@@ -36,7 +36,7 @@
</script>
<svelte:head>
<title>{playlist.name} · Musify</title>
<title>{playlist.name}</title>
<meta name="description" content="Playlist {playlist.name} en Musify." />
</svelte:head>
@@ -1,12 +1,11 @@
<script lang="ts">
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
import Play from '@lucide/svelte/icons/play';
import Pause from '@lucide/svelte/icons/pause';
import SquarePencil from '@lucide/svelte/icons/square-pen';
import Trash from '@lucide/svelte/icons/trash';
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
import BackLink from '$lib/components/ui/BackLink.svelte';
import Button from '$lib/components/ui/Button.svelte';
import { hueFor } from '$lib/theme/color';
interface Props {
id: string;
@@ -24,21 +23,12 @@
$props();
</script>
<a
href="/playlists"
class="group inline-flex items-center text-fg-3 transition-colors hover:text-accent-soft"
>
<span class="grid h-8 w-8 place-items-center transition-colors group-hover:text-accent-soft">
<ArrowLeft class="h-4 w-4" />
</span>
Volver a tus listas
</a>
<BackLink href="/playlists" label="Volver a tus listas" />
<div class="mt-5 flex flex-col gap-5 sm:mt-6 sm:flex-row sm:items-end sm:gap-6">
<PlaylistArt
playlistId={id}
{trackIds}
hue={hueFor(id)}
size="large"
version={updatedAt}
class="h-36 w-36 shrink-0 rounded-art-lg shadow-art-lg sm:h-44 sm:w-44"
@@ -9,7 +9,6 @@
youTubeThumbnailUrl,
type ApiTrackLike
} from '$lib/player/player.svelte';
import { hueFor } from '$lib/theme/color';
import { fmtTime, fmtDate } from '$lib/format';
import Badge from '$lib/components/ui/Badge.svelte';
import IconButton from '$lib/components/ui/IconButton.svelte';
@@ -48,7 +47,6 @@
coverSrc={isPendingYouTubeTrack(track) && track.externalId
? youTubeThumbnailUrl(track.externalId)
: undefined}
hue={hueFor(track.id)}
explicit={track.isExplicit}
onClick={() => playFrom(i)}
>
@@ -7,7 +7,6 @@
type ApiTrackLike,
type QueueItem
} from '$lib/player/player.svelte';
import { hueFor } from '$lib/theme/color';
import { fmtTime } from '$lib/format';
import type { YouTubeSong } from '$lib/types';
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
@@ -92,7 +91,6 @@
title={item.kind === 'youtube' ? item.song.title : item.track.title}
artist={item.kind === 'youtube' ? item.song.artist : item.track.artist}
coverSrc={item.kind === 'youtube' ? item.song.thumbnailUrl : undefined}
hue={hueFor(idFor(item))}
explicit={item.kind === 'youtube' && item.song.isExplicit}
onClick={() => playFrom(i)}
>
@@ -102,7 +102,7 @@
</script>
<svelte:head>
<title>Subir música · Musify</title>
<title>Subir música</title>
<meta name="description" content="Sube tus canciones a Musify: portada, audio y título." />
</svelte:head>
+17
View File
@@ -10,13 +10,28 @@
import MobileNav from '$lib/components/MobileNav.svelte';
import { player } from '$lib/player/player.svelte';
import { ACCENT_LIGHTNESS, ACCENT_CHROMA, ACCENT_HUE } from '$lib/theme/color';
import { trackNavigation } from '$lib/navigation.svelte';
import { page } from '$app/state';
let { children, data } = $props();
trackNavigation();
const isAuthPage = $derived(page.url.pathname === '/login');
const hasTrack = $derived(player.currentId !== null);
function isTypingTarget(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return false;
if (target.isContentEditable) return true;
return ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'A'].includes(target.tagName);
}
function onWindowKeydown(event: KeyboardEvent) {
if (event.code !== 'Space' || isTypingTarget(event.target)) return;
event.preventDefault();
player.toggle();
}
$effect(() => {
document.documentElement.classList.toggle('home', page.url.pathname === '/');
});
@@ -59,6 +74,8 @@
});
</script>
<svelte:window onkeydown={onWindowKeydown} />
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
{#if isAuthPage || !data.user}
+193 -180
View File
@@ -1,5 +1,4 @@
<script lang="ts">
import Music from '@lucide/svelte/icons/music';
import ArrowRight from '@lucide/svelte/icons/arrow-right';
import { player, toQueueItems, type QueueItem } from '$lib/player/player.svelte';
import {
@@ -13,14 +12,12 @@
targetTitle,
type TrackTarget
} from '$lib/tracks';
import { hueFor } from '$lib/theme/color';
import Cover from '$lib/components/ui/Cover.svelte';
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
import MixBentoTile from '$lib/components/ui/MixBentoTile.svelte';
import Rail from '$lib/components/ui/Rail.svelte';
import CoverBadge from '$lib/components/ui/CoverBadge.svelte';
import NowPlaying from '$lib/components/ui/NowPlaying.svelte';
import EmptyState from '$lib/components/ui/EmptyState.svelte';
import ExplicitBadge from '$lib/components/ui/ExplicitBadge.svelte';
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
import TrackContextMenu, {
@@ -30,6 +27,8 @@
import type { YouTubeSong } from '$lib/types';
import { appendUnique } from '$lib/collections';
import { HOME_LATEST_PAGE_SIZE } from '$lib/config';
import EmptyState from '$lib/components/ui/EmptyState.svelte';
import Music from '@lucide/svelte/icons/music';
let { data } = $props();
@@ -138,189 +137,204 @@
</div>
</section>
{#if recentlyPlayed.length > 0}
<section class="page-x pt-6 sm:pt-8">
<div
class="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"
>
{#each recentlyPlayed as track, i (track.id)}
<button
type="button"
onclick={() => playRecent(i)}
oncontextmenu={(e) => openContextMenu(e, targetForQueueItem(track))}
aria-label="{player.current.id === track.id && player.playing
? 'Pausar'
: 'Reproducir'} {track.title}"
class="group/row flex min-w-0 items-center gap-3 rounded-control bg-surface p-2.5 text-left transition duration-200"
>
<Cover
trackId={track.id}
src={track.coverUrl}
hue={hueFor(track.id)}
size="small"
alt={track.title}
class="h-14 w-14 shrink-0 rounded-control shadow-art transition duration-200"
>
{#if player.current.id === track.id}
<NowPlaying paused={!player.playing} />
{/if}
</Cover>
<div class="min-w-0">
<div class="flex min-w-0 items-center gap-1.5">
{#if track.explicit}
<ExplicitBadge />
{/if}
<span class="truncate text-fg transition-colors group-hover/row:text-accent-soft">
{track.title}
</span>
</div>
{#if track.artist}
<div class="truncate text-sm text-muted">{track.artist}</div>
{/if}
</div>
</button>
{/each}
</div>
</section>
{/if}
{#if albums.length > 0}
<section class="page-x pt-10">
<SectionHeading title="Álbumes recién escuchados">
{#snippet actions()}
<a href="/albums" class="text-sm text-fg-3 transition hover:text-accent-soft">Ver todos</a>
{/snippet}
</SectionHeading>
<Rail>
{#each albums as album, i (album.id)}
<a
href="/albums/{album.id}"
class="group/card animate-enter block w-[168px] shrink-0 rounded-art focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none"
style="animation-delay:{Math.min(i, 10) * 45}ms"
>
<div class="relative">
<PlaylistArt
trackIds={album.coverTrackIds}
hue={hueFor(album.id)}
class="h-[168px] w-[168px] rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:shadow-art-lg"
/>
<CoverBadge label="Álbum" />
</div>
<div class="mt-2.5 truncate text-fg transition-colors group-hover/card:text-accent-soft">
{album.title}
</div>
<div class="mt-0.5 truncate text-sm text-fg-3">{albumCaption(album)}</div>
</a>
{/each}
</Rail>
</section>
{/if}
{#if mixes.length > 0}
<section class="page-x pt-10">
<div class="grid grid-cols-2 gap-3 sm:auto-rows-25 sm:grid-cols-4 sm:gap-4">
{#each mixes as mix, i (mix.id)}
<MixBentoTile
{mix}
index={i}
featured={i === 0}
banner={i === 4}
class={i === 0
? 'col-span-2 min-h-42 sm:row-span-2 sm:min-h-0'
: i === 1
? 'col-span-2 min-h-30 sm:min-h-0'
: i === 4
? 'col-span-2 min-h-24 sm:col-span-4 sm:min-h-0'
: 'min-h-30 sm:min-h-0'}
/>
{/each}
</div>
</section>
{/if}
{#if playlists.length > 0}
<section class="page-x pt-10">
<Rail>
{#each playlists as playlist, i (playlist.id)}
<a
href="/playlists/{playlist.id}"
class="group/card animate-enter block w-36 shrink-0 rounded-art focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none"
style="animation-delay:{Math.min(i, 10) * 45}ms"
>
<PlaylistArt
playlistId={playlist.id}
trackIds={playlist.coverTrackIds}
hue={hueFor(playlist.id)}
version={playlist.updatedAt}
class="h-36 w-36 rounded-art"
/>
<div class="mt-2.5 truncate text-fg transition-colors group-hover/card:text-accent-soft">
{playlist.name}
</div>
<div class="mt-0.5 truncate text-sm text-muted">{playlist.description}</div>
</a>
{/each}
<a
href="/playlists"
class="grid h-36 w-36 shrink-0 place-items-center text-fg-3 transition-colors hover:text-accent-soft"
<div class="flex flex-col gap-10 page-x py-4 sm:py-6">
{#if recentlyPlayed.length > 0}
<section>
<SectionHeading title="Recientemente reproducidas" />
<div
class="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"
>
<div class="flex flex-col items-center gap-2">
<span class="grid h-8 w-8 place-items-center">
<ArrowRight class="h-4 w-4" />
</span>
Ver todas
</div>
</a>
</Rail>
</section>
{/if}
{#if newTargets.length > 0}
<section class="page-x pt-10">
<SectionHeading title="Canciones nuevas" />
<Rail>
{#each newTargets as target, i (targetId(target))}
<button
type="button"
onclick={() => playNew(i)}
oncontextmenu={(e) => openContextMenu(e, target)}
aria-label="Reproducir {targetTitle(target)}"
class="group/card animate-enter block w-[148px] shrink-0 rounded-art text-left focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none"
style="animation-delay:{Math.min(i, 10) * 45}ms"
>
<div class="relative h-[198px] w-[148px]">
{#each recentlyPlayed as track, i (track.id)}
<button
type="button"
onclick={() => playRecent(i)}
oncontextmenu={(e) => openContextMenu(e, targetForQueueItem(track))}
aria-label="{player.current.id === track.id && player.playing
? 'Pausar'
: 'Reproducir'} {track.title}"
class="group/row flex min-w-0 items-center gap-3 rounded-control bg-surface p-2.5 text-left transition duration-200"
>
<Cover
trackId={targetId(target)}
src={targetCoverSrc(target)}
hue={hueFor(targetId(target))}
size="medium"
alt={targetTitle(target)}
class="h-full w-full rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:shadow-art-lg"
trackId={track.id}
src={track.coverUrl}
size="small"
alt={track.title}
class="h-14 w-14 shrink-0 rounded-control shadow-art transition duration-200"
>
{#if isTargetCurrent(target)}
{#if player.current.id === track.id}
<NowPlaying paused={!player.playing} />
{/if}
</Cover>
<CoverBadge label="Nuevo" />
</div>
<div class="mt-2.5 flex min-w-0 items-center gap-1.5">
{#if targetExplicit(target)}
<ExplicitBadge />
{/if}
<span class="truncate text-fg transition-colors group-hover/card:text-accent-soft">
{targetTitle(target)}
</span>
</div>
{#if targetArtist(target)}
<div class="mt-0.5 truncate text-sm text-fg-3">{targetArtist(target)}</div>
{/if}
</button>
{/each}
</Rail>
</section>
{/if}
<div class="min-w-0">
<div class="flex min-w-0 items-center gap-1.5">
{#if track.explicit}
<ExplicitBadge />
{/if}
<span class="truncate text-fg transition-colors group-hover/row:text-accent-soft">
{track.title}
</span>
</div>
{#if track.artist}
<div class="truncate text-sm text-muted">{track.artist}</div>
{/if}
</div>
</button>
{/each}
</div>
</section>
{/if}
<!---
{#if albums.length > 0}
<section>
<SectionHeading title="Álbumes recientes" />
<Rail>
{#each albums as album, i (album.id)}
<a
href={album.youTubeAlbumId
? `/albums/youtube/${album.youTubeAlbumId}`
: `/albums/${album.id}`}
class="group/card animate-enter block w-42 shrink-0 rounded-art focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none"
style="animation-delay:{Math.min(i, 10) * 45}ms"
>
<div class="relative">
{#if album.thumbnailUrl}
<Cover
trackId={album.id}
src={album.thumbnailUrl}
size="large"
alt={album.title}
class="h-52 w-42 rounded-art shadow-art ring-line transition duration-300 ease-out"
/>
{:else}
<PlaylistArt
trackIds={album.coverTrackIds}
class="h-56 w-48 rounded-art shadow-art ring-line transition duration-300 ease-out"
/>
{/if}
</div>
<div class="mt-2.5 text-fg transition-colors group-hover/card:text-accent-soft">
{album.title}
</div>
<div class="mt-0.5 text-sm text-fg-3">{albumCaption(album)}</div>
</a>
{/each}
</Rail>
</section>
{/if}
{#if mixes.length > 0}
<section>
<SectionHeading title="Mezclas" />
<div class="grid grid-cols-2 gap-3 sm:auto-rows-25 sm:grid-cols-4 sm:gap-4">
{#each mixes as mix, i (mix.id)}
<MixBentoTile
{mix}
index={i}
featured={i === 0}
banner={i === 4}
class={i === 0
? 'col-span-2 min-h-42 sm:row-span-2 sm:min-h-0'
: i === 1
? 'col-span-2 min-h-30 sm:min-h-0'
: i === 4
? 'col-span-2 min-h-24 sm:col-span-4 sm:min-h-0'
: 'min-h-30 sm:min-h-0'}
/>
{/each}
</div>
</section>
{/if}
{#if playlists.length > 0}
<section>
<SectionHeading title="Listas de reproducción" />
<Rail>
{#each playlists as playlist, i (playlist.id)}
<a
href="/playlists/{playlist.id}"
class="group/card animate-enter block w-38 shrink-0 rounded-art"
style="animation-delay:{Math.min(i, 10) * 45}ms"
>
<PlaylistArt
playlistId={playlist.id}
trackIds={playlist.coverTrackIds}
version={playlist.updatedAt}
class="h-38 w-38 rounded-art"
/>
<div
class="mt-2.5 truncate text-fg transition-colors group-hover/card:text-accent-soft"
>
{playlist.name}
</div>
{#if playlist.description}
<div class="mt-0.5 truncate text-sm text-muted">{playlist.description}</div>
{/if}
</a>
{/each}
<a
href="/playlists"
class="grid h-36 w-36 shrink-0 place-items-center text-fg-3 transition-colors hover:text-accent-soft"
>
<div class="flex flex-col items-center gap-2">
<span class="grid h-8 w-8 place-items-center">
<ArrowRight class="h-4 w-4" />
</span>
Ver todas
</div>
</a>
</Rail>
</section>
{/if}
{#if newTargets.length > 0}
<section>
<SectionHeading title="Canciones nuevas" />
<Rail>
{#each newTargets as target, i (targetId(target))}
<button
type="button"
onclick={() => playNew(i)}
oncontextmenu={(e) => openContextMenu(e, target)}
aria-label="Reproducir {targetTitle(target)}"
class="group/card animate-enter block w-[148px] shrink-0 rounded-art text-left focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none"
style="animation-delay:{Math.min(i, 10) * 45}ms"
>
<div class="relative h-[198px] w-[148px]">
<Cover
trackId={targetId(target)}
src={targetCoverSrc(target)}
size="medium"
alt={targetTitle(target)}
class="h-full w-full rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:shadow-art-lg"
>
{#if isTargetCurrent(target)}
<NowPlaying paused={!player.playing} />
{/if}
</Cover>
<CoverBadge label="Nuevo" />
</div>
<div class="mt-2.5 flex min-w-0 items-center gap-1.5">
{#if targetExplicit(target)}
<ExplicitBadge />
{/if}
<span class="truncate text-fg transition-colors group-hover/card:text-accent-soft">
{targetTitle(target)}
</span>
</div>
{#if targetArtist(target)}
<div class="mt-0.5 truncate text-sm text-fg-3">{targetArtist(target)}</div>
{/if}
</button>
{/each}
</Rail>
</section>
{/if}
{#if recentlyPlayed.length === 0 && albums.length === 0 && mixes.length === 0 && playlists.length === 0 && newTargets.length === 0}
<EmptyState icon={Music} title="Todavía no hay música" description="Sube tu música o explora nuevas canciones, álbumes y listas de reproducción." />
{/if}
</div>
<!---
<section class="page-x pt-10 pb-16">
{#if recTargets.length > 0}
<Rail>
@@ -337,7 +351,6 @@
<Cover
trackId={targetId(target)}
src={targetCoverSrc(target)}
hue={hueFor(targetId(target))}
size="medium"
alt={targetTitle(target)}
class="h-full w-full rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:shadow-art-lg"
+2 -2
View File
@@ -37,10 +37,10 @@
}
@utility page-x {
padding-inline: --spacing(6);
padding-inline: --spacing(4);
@media (width >= 40rem) {
padding-inline: --spacing(10);
padding-inline: --spacing(8);
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
</script>
<svelte:head>
<title>Iniciar sesión · Musify</title>
<title>Iniciar sesión</title>
<meta name="description" content="Inicia sesión o crea una cuenta en Musify." />
</svelte:head>