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
2802 lines
95 KiB
HTML
2802 lines
95 KiB
HTML
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<script src="./support.js"></script>
|
||
</head>
|
||
<body>
|
||
<x-dc>
|
||
<helmet>
|
||
<link
|
||
href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap"
|
||
rel="stylesheet"
|
||
/>
|
||
<style>
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
html,
|
||
body {
|
||
margin: 0;
|
||
padding: 0;
|
||
background: #050507;
|
||
}
|
||
a {
|
||
color: #b4a4ea;
|
||
text-decoration: none;
|
||
}
|
||
a:hover {
|
||
color: #cdc0f5;
|
||
}
|
||
img {
|
||
display: block;
|
||
}
|
||
::-webkit-scrollbar {
|
||
width: 9px;
|
||
height: 9px;
|
||
}
|
||
::-webkit-scrollbar-thumb {
|
||
background: rgba(255, 255, 255, 0.07);
|
||
border-radius: 8px;
|
||
}
|
||
::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
@keyframes eq {
|
||
0%,
|
||
100% {
|
||
transform: scaleY(0.25);
|
||
}
|
||
50% {
|
||
transform: scaleY(1);
|
||
}
|
||
}
|
||
@keyframes shim {
|
||
0% {
|
||
background-position: -420px 0;
|
||
}
|
||
100% {
|
||
background-position: 420px 0;
|
||
}
|
||
}
|
||
@keyframes rise {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(12px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: none;
|
||
}
|
||
}
|
||
</style>
|
||
</helmet>
|
||
|
||
<div
|
||
style="position:relative;height:100vh;overflow:hidden;background:{{ pageBg }};transition:background .6s ease;color:#c9c8cc;font-family:'Plus Jakarta Sans',system-ui,sans-serif;-webkit-font-smoothing:antialiased"
|
||
>
|
||
<div style="position:absolute;inset:0;pointer-events:none;background:{{ ambient }}"></div>
|
||
<div
|
||
style="
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
background: radial-gradient(130% 85% at 50% 118%, rgba(0, 0, 0, 0.8), transparent 62%);
|
||
"
|
||
></div>
|
||
|
||
<div style="position: relative; display: flex; height: 100%">
|
||
<!-- SIDEBAR -->
|
||
<aside
|
||
data-screen-label="Sidebar"
|
||
style="display:{{ sidebarDisplay }};width:254px;flex:0 0 auto;flex-direction:column;padding:22px 12px 0 20px;border-right:1px solid {{ hairline }};background:{{ sidebarBg }};transition:background .6s ease"
|
||
>
|
||
<div style="display: flex; align-items: center; gap: 11px; padding: 0 6px 26px">
|
||
<div
|
||
style="width:24px;height:24px;border-radius:8px;background:{{ logoGrad }};box-shadow:{{ logoGlow }};transition:background .6s ease,box-shadow .6s ease"
|
||
></div>
|
||
<div
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-weight: 600;
|
||
font-size: 17.5px;
|
||
letter-spacing: -0.02em;
|
||
color: #d8d7db;
|
||
"
|
||
>
|
||
Musify
|
||
</div>
|
||
</div>
|
||
|
||
<nav style="display: flex; flex-direction: column; gap: 2px">
|
||
<sc-for list="{{ nav }}" as="n" hint-placeholder-count="5">
|
||
<div
|
||
onClick="{{ n.go }}"
|
||
style="display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:10px;cursor:pointer;font-size:13.5px;font-weight:500;color:{{ n.color }};background:{{ n.bg }};transition:background .16s ease,color .16s ease"
|
||
style-hover="background:rgba(255,255,255,.04);color:#d4d3d7"
|
||
>
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
width="17"
|
||
height="17"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.6"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<path d="{{ n.icon }}"></path>
|
||
</svg>
|
||
<span style="flex: 1">{{ n.label }}</span>
|
||
<span style="font-size: 11px; font-variant-numeric: tabular-nums; color: #5c5c60"
|
||
>{{ n.count }}</span
|
||
>
|
||
</div>
|
||
</sc-for>
|
||
</nav>
|
||
|
||
<div
|
||
style="margin: 24px 12px 14px; height: 1px; background: rgba(255, 255, 255, 0.05)"
|
||
></div>
|
||
<div
|
||
style="
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 12px 10px;
|
||
"
|
||
>
|
||
<div
|
||
style="
|
||
font-size: 10.5px;
|
||
letter-spacing: 0.13em;
|
||
text-transform: uppercase;
|
||
color: #646468;
|
||
font-weight: 600;
|
||
"
|
||
>
|
||
Tus playlists
|
||
</div>
|
||
<div
|
||
onClick="{{ goPlaylists }}"
|
||
style="font-size: 15px; color: #646468; cursor: pointer; line-height: 1"
|
||
style-hover="color:#cfccd8"
|
||
>
|
||
+
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
style="
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1px;
|
||
padding-bottom: 18px;
|
||
"
|
||
>
|
||
<sc-for list="{{ myPlaylists }}" as="p" hint-placeholder-count="6">
|
||
<div
|
||
onClick="{{ p.open }}"
|
||
style="display:flex;align-items:center;gap:11px;padding:7px 12px;border-radius:10px;cursor:pointer;background:{{ p.bg }};transition:background .16s ease"
|
||
style-hover="background:rgba(255,255,255,.04)"
|
||
>
|
||
<div
|
||
style="position:relative;width:34px;height:34px;border-radius:8px;flex:0 0 auto;overflow:hidden;background:{{ p.fallback }}"
|
||
>
|
||
<div
|
||
style="width:100%;height:100%;background-image:url({{ p.img }});background-size:cover;background-position:center;opacity:.88"
|
||
></div>
|
||
</div>
|
||
<div style="min-width: 0; flex: 1">
|
||
<div
|
||
style="font-size:12.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:{{ p.titleColor }}"
|
||
>
|
||
{{ p.name }}
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 11px;
|
||
color: #6c6a75;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-top: 2px;
|
||
"
|
||
>
|
||
{{ p.meta }}
|
||
</div>
|
||
</div>
|
||
<sc-if value="{{ p.isPlaying }}" hint-placeholder-val="{{ false }}">
|
||
<div
|
||
style="
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 2px;
|
||
height: 11px;
|
||
flex: 0 0 auto;
|
||
"
|
||
>
|
||
<div
|
||
style="width:2px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out infinite"
|
||
></div>
|
||
<div
|
||
style="width:2px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out .3s infinite"
|
||
></div>
|
||
<div
|
||
style="width:2px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out .6s infinite"
|
||
></div>
|
||
</div>
|
||
</sc-if>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- MAIN -->
|
||
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column">
|
||
<header
|
||
style="
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 16px 26px 12px;
|
||
flex: 0 0 auto;
|
||
"
|
||
>
|
||
<div style="display: flex; gap: 6px">
|
||
<div
|
||
onClick="{{ back }}"
|
||
style="
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 50%;
|
||
display: grid;
|
||
place-items: center;
|
||
background: rgba(255, 255, 255, 0.035);
|
||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||
cursor: pointer;
|
||
color: #8f8f93;
|
||
"
|
||
style-hover="color:#e4e2ea;background:rgba(255,255,255,.07)"
|
||
>
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
width="15"
|
||
height="15"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.9"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<path d="M15 18l-6-6 6-6"></path>
|
||
</svg>
|
||
</div>
|
||
<div
|
||
style="
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 50%;
|
||
display: grid;
|
||
place-items: center;
|
||
background: rgba(255, 255, 255, 0.035);
|
||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||
cursor: pointer;
|
||
color: #555559;
|
||
"
|
||
>
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
width="15"
|
||
height="15"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.9"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<path d="M9 18l6-6-6-6"></path>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
style="
|
||
flex: 1;
|
||
max-width: 480px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 0 14px;
|
||
height: 38px;
|
||
border-radius: 11px;
|
||
background: rgba(255, 255, 255, 0.035);
|
||
border: 1px solid rgba(255, 255, 255, 0.055);
|
||
"
|
||
>
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
width="15"
|
||
height="15"
|
||
fill="none"
|
||
stroke="#737377"
|
||
stroke-width="1.8"
|
||
stroke-linecap="round"
|
||
>
|
||
<circle cx="11" cy="11" r="7"></circle>
|
||
<path d="M20 20l-3.6-3.6"></path>
|
||
</svg>
|
||
<input
|
||
value="{{ query }}"
|
||
onChange="{{ onQuery }}"
|
||
placeholder="Canciones, artistas, álbumes o playlists"
|
||
style="
|
||
flex: 1;
|
||
background: transparent;
|
||
border: 0;
|
||
outline: none;
|
||
color: #c9c8cc;
|
||
font-family: inherit;
|
||
font-size: 13px;
|
||
"
|
||
/>
|
||
</div>
|
||
|
||
<div style="flex: 1"></div>
|
||
|
||
<div
|
||
onClick="{{ toggleQueue }}"
|
||
style="height:33px;display:flex;align-items:center;gap:8px;padding:0 13px;border-radius:10px;cursor:pointer;font-size:12.5px;font-weight:500;color:{{ queueBtnColor }};background:{{ queueBtnBg }};border:1px solid rgba(255,255,255,.055)"
|
||
style-hover="color:#dbdadf"
|
||
>
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
width="15"
|
||
height="15"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.7"
|
||
stroke-linecap="round"
|
||
>
|
||
<path d="M4 7h11M4 12h11M4 17h7M19 10v8"></path>
|
||
</svg>
|
||
Cola
|
||
</div>
|
||
<div
|
||
style="
|
||
width: 33px;
|
||
height: 33px;
|
||
border-radius: 50%;
|
||
overflow: hidden;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
cursor: pointer;
|
||
flex: 0 0 auto;
|
||
"
|
||
>
|
||
<img
|
||
src="https://picsum.photos/seed/musify-user-ari/120/120"
|
||
alt=""
|
||
style="width: 100%; height: 100%; object-fit: cover; opacity: 0.9"
|
||
/>
|
||
</div>
|
||
</header>
|
||
|
||
<main style="flex:1;overflow-y:auto;padding:6px 26px;padding-bottom:{{ bottomPad }}">
|
||
<!-- LOADING -->
|
||
<sc-if value="{{ isLoading }}" hint-placeholder-val="{{ false }}">
|
||
<div data-screen-label="Loading">
|
||
<div
|
||
style="
|
||
height: 260px;
|
||
border-radius: 22px;
|
||
background: linear-gradient(
|
||
90deg,
|
||
rgba(255, 255, 255, 0.025),
|
||
rgba(255, 255, 255, 0.055),
|
||
rgba(255, 255, 255, 0.025)
|
||
);
|
||
background-size: 420px 100%;
|
||
animation: shim 1.25s linear infinite;
|
||
margin-bottom: 38px;
|
||
"
|
||
></div>
|
||
<sc-for list="{{ skeletonRows }}" as="s" hint-placeholder-count="2">
|
||
<div style="margin-bottom: 34px">
|
||
<div
|
||
style="
|
||
width: 170px;
|
||
height: 14px;
|
||
border-radius: 6px;
|
||
background: linear-gradient(
|
||
90deg,
|
||
rgba(255, 255, 255, 0.025),
|
||
rgba(255, 255, 255, 0.055),
|
||
rgba(255, 255, 255, 0.025)
|
||
);
|
||
background-size: 420px 100%;
|
||
animation: shim 1.25s linear infinite;
|
||
margin-bottom: 18px;
|
||
"
|
||
></div>
|
||
<div
|
||
style="
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
|
||
gap: 20px;
|
||
"
|
||
>
|
||
<sc-for list="{{ skeletonCards }}" as="c" hint-placeholder-count="6">
|
||
<div>
|
||
<div
|
||
style="
|
||
aspect-ratio: 1;
|
||
border-radius: 13px;
|
||
background: linear-gradient(
|
||
90deg,
|
||
rgba(255, 255, 255, 0.025),
|
||
rgba(255, 255, 255, 0.055),
|
||
rgba(255, 255, 255, 0.025)
|
||
);
|
||
background-size: 420px 100%;
|
||
animation: shim 1.25s linear infinite;
|
||
"
|
||
></div>
|
||
<div
|
||
style="
|
||
width: 72%;
|
||
height: 10px;
|
||
border-radius: 5px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
margin: 14px 0 8px;
|
||
"
|
||
></div>
|
||
<div
|
||
style="
|
||
width: 42%;
|
||
height: 9px;
|
||
border-radius: 5px;
|
||
background: rgba(255, 255, 255, 0.032);
|
||
"
|
||
></div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</sc-if>
|
||
|
||
<!-- ERROR -->
|
||
<sc-if value="{{ isError }}" hint-placeholder-val="{{ false }}">
|
||
<div
|
||
data-screen-label="Error"
|
||
style="
|
||
max-width: 420px;
|
||
margin: 14vh auto;
|
||
text-align: center;
|
||
animation: rise 0.3s ease;
|
||
"
|
||
>
|
||
<div
|
||
style="
|
||
width: 52px;
|
||
height: 52px;
|
||
margin: 0 auto 22px;
|
||
border-radius: 15px;
|
||
background: rgba(226, 130, 130, 0.08);
|
||
border: 1px solid rgba(226, 130, 130, 0.18);
|
||
display: grid;
|
||
place-items: center;
|
||
color: #e09292;
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 21px;
|
||
"
|
||
>
|
||
!
|
||
</div>
|
||
<div
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.02em;
|
||
margin-bottom: 10px;
|
||
color: #d5d4d8;
|
||
"
|
||
>
|
||
No se pudo cargar tu música
|
||
</div>
|
||
<div
|
||
style="font-size: 13px; line-height: 1.65; color: #8a8a8e; margin-bottom: 26px"
|
||
>
|
||
La conexión se interrumpió. Lo que tengas descargado sigue disponible sin
|
||
conexión.
|
||
</div>
|
||
<div style="display: flex; gap: 10px; justify-content: center">
|
||
<div
|
||
onClick="{{ retry }}"
|
||
style="
|
||
padding: 11px 20px;
|
||
border-radius: 11px;
|
||
background: #c6c4cb;
|
||
color: #0a0a0d;
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
"
|
||
style-hover="background:#d6d5da"
|
||
>
|
||
Reintentar
|
||
</div>
|
||
<div
|
||
onClick="{{ goPlaylists }}"
|
||
style="
|
||
padding: 11px 20px;
|
||
border-radius: 11px;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
font-size: 12.5px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
color: #a9a7b2;
|
||
"
|
||
style-hover="color:#dbdadf;background:rgba(255,255,255,.04)"
|
||
>
|
||
Ver descargas
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</sc-if>
|
||
|
||
<!-- HOME -->
|
||
<sc-if value="{{ showHome }}" hint-placeholder-val="{{ true }}">
|
||
<div data-screen-label="Home">
|
||
<!-- HERO -->
|
||
<section
|
||
style="position:relative;overflow:hidden;border-radius:24px;margin-bottom:42px;background:{{ heroBg }}"
|
||
>
|
||
<div
|
||
style="
|
||
position: relative;
|
||
padding: 36px 36px 32px;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
gap: 30px;
|
||
flex-wrap: wrap;
|
||
"
|
||
>
|
||
<div style="max-width: 560px; min-width: 260px">
|
||
<h1
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 36px;
|
||
line-height: 1.1;
|
||
font-weight: 600;
|
||
letter-spacing: -0.035em;
|
||
margin: 0 0 14px;
|
||
color: #d9d8dc;
|
||
text-wrap: pretty;
|
||
"
|
||
>
|
||
{{ heroGreeting }}
|
||
</h1>
|
||
<div
|
||
style="
|
||
font-size: 13.5px;
|
||
color: #97979b;
|
||
line-height: 1.65;
|
||
max-width: 430px;
|
||
"
|
||
>
|
||
{{ heroBlurb }}
|
||
</div>
|
||
<div
|
||
style="
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: 26px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
"
|
||
>
|
||
<div
|
||
onClick="{{ playHero }}"
|
||
style="
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 9px;
|
||
padding: 12px 21px;
|
||
border-radius: 12px;
|
||
background: #c6c4cb;
|
||
color: #0a0a0d;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
|
||
"
|
||
style-hover="background:#d8d7dc"
|
||
>
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
||
<path d="M8 5v14l11-7z"></path>
|
||
</svg>
|
||
Reanudar {{ heroResume }}
|
||
</div>
|
||
<div
|
||
style="
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 9px;
|
||
padding: 12px 19px;
|
||
border-radius: 12px;
|
||
border: 1px solid rgba(255, 255, 255, 0.11);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
color: #aeaeb2;
|
||
"
|
||
style-hover="background:rgba(255,255,255,.05);color:#d9d8dd"
|
||
>
|
||
Ver resumen semanal
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; gap: 26px; padding: 4px 2px">
|
||
<sc-for list="{{ heroStats }}" as="s" hint-placeholder-count="3">
|
||
<div>
|
||
<div
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 25px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.03em;
|
||
color: #d4d3d7;
|
||
"
|
||
>
|
||
{{ s.v }}
|
||
</div>
|
||
<div style="font-size: 11.5px; color: #818185; margin-top: 4px">
|
||
{{ s.k }}
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- CONTINUE -->
|
||
<section style="margin-bottom: 46px">
|
||
<div style="margin-bottom: 16px">
|
||
<h2
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 17.5px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.025em;
|
||
margin: 0;
|
||
color: #d3d2d6;
|
||
"
|
||
>
|
||
Continuar escuchando
|
||
</h2>
|
||
</div>
|
||
<div
|
||
style="
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(268px, 1fr));
|
||
gap: 10px;
|
||
"
|
||
>
|
||
<sc-for list="{{ continueRow }}" as="c" hint-placeholder-count="6">
|
||
<div
|
||
onMouseEnter="{{ c.enter }}"
|
||
onMouseLeave="{{ c.leave }}"
|
||
onClick="{{ c.play }}"
|
||
style="display:flex;align-items:center;gap:13px;padding:9px 14px 9px 9px;border-radius:13px;cursor:pointer;background:{{ c.bg }};border:1px solid {{ c.border }};transition:background .16s ease"
|
||
>
|
||
<div
|
||
style="width:46px;height:46px;border-radius:9px;overflow:hidden;flex:0 0 auto;background:{{ c.fallback }}"
|
||
>
|
||
<div
|
||
style="width:100%;height:100%;background-image:url({{ c.img }});background-size:cover;background-position:center;opacity:.92"
|
||
></div>
|
||
</div>
|
||
<div style="min-width: 0; flex: 1">
|
||
<div
|
||
style="font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:{{ c.titleColor }}"
|
||
>
|
||
{{ c.title }}
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 11.5px;
|
||
color: #848488;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-top: 3px;
|
||
"
|
||
>
|
||
{{ c.sub }}
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="flex:0 0 auto;color:{{ accent }};opacity:{{ c.btnOpacity }};transition:opacity .16s ease"
|
||
>
|
||
<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor">
|
||
<path d="M8 5v14l11-7z"></path>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- SPOTLIGHT PLAYLIST -->
|
||
<section style="margin-bottom: 48px">
|
||
<div
|
||
style="display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));gap:0;border-radius:22px;overflow:hidden;background:{{ spotlightBg }}"
|
||
>
|
||
<div
|
||
style="
|
||
padding: 32px 30px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
gap: 16px;
|
||
"
|
||
>
|
||
<div style="display: flex; align-items: center; gap: 16px">
|
||
<div
|
||
style="
|
||
width: 88px;
|
||
height: 88px;
|
||
border-radius: 14px;
|
||
overflow: hidden;
|
||
flex: 0 0 auto;
|
||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
|
||
"
|
||
>
|
||
<div
|
||
style="width:100%;height:100%;background-image:url({{ spotlight.img }});background-size:cover;background-position:center"
|
||
></div>
|
||
</div>
|
||
<div style="min-width: 0">
|
||
<div
|
||
style="
|
||
font-size: 10.5px;
|
||
letter-spacing: 0.16em;
|
||
text-transform: uppercase;
|
||
color: #8f8f93;
|
||
font-weight: 600;
|
||
margin-bottom: 9px;
|
||
"
|
||
>
|
||
Playlist destacada
|
||
</div>
|
||
<div
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 23px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.03em;
|
||
color: #d8d7db;
|
||
line-height: 1.15;
|
||
"
|
||
>
|
||
{{ spotlight.name }}
|
||
</div>
|
||
<div style="font-size: 12px; color: #8a8a8e; margin-top: 7px">
|
||
{{ spotlight.meta }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 13px;
|
||
line-height: 1.7;
|
||
color: #919195;
|
||
max-width: 400px;
|
||
"
|
||
>
|
||
{{ spotlight.blurb }}
|
||
</div>
|
||
<div style="display: flex; gap: 9px; margin-top: 4px">
|
||
<div
|
||
onClick="{{ spotlight.play }}"
|
||
style="display:flex;align-items:center;gap:8px;padding:10px 18px;border-radius:11px;background:{{ accentBtnBg }};border:1px solid {{ accentLine }};color:{{ accentTitle }};font-size:12.5px;font-weight:600;cursor:pointer"
|
||
style-hover="background:{{ accentBtnBgHover }}"
|
||
>
|
||
<svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor">
|
||
<path d="M8 5v14l11-7z"></path></svg
|
||
>Reproducir
|
||
</div>
|
||
<div
|
||
style="
|
||
padding: 10px 16px;
|
||
border-radius: 11px;
|
||
border: 1px solid rgba(255, 255, 255, 0.09);
|
||
font-size: 12.5px;
|
||
font-weight: 500;
|
||
color: #a8a8ac;
|
||
cursor: pointer;
|
||
"
|
||
style-hover="color:#dbdadf;background:rgba(255,255,255,.04)"
|
||
>
|
||
Mezclar
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="
|
||
padding: 22px 24px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
background: linear-gradient(90deg, rgba(0, 0, 0, 0.26), rgba(0, 0, 0, 0));
|
||
"
|
||
>
|
||
<sc-for list="{{ spotlightTracks }}" as="t" hint-placeholder-count="5">
|
||
<div
|
||
onMouseEnter="{{ t.enter }}"
|
||
onMouseLeave="{{ t.leave }}"
|
||
onClick="{{ t.play }}"
|
||
style="display:flex;align-items:center;gap:12px;padding:8px 10px;border-radius:10px;cursor:pointer;background:{{ t.rowBg }};transition:background .15s ease"
|
||
>
|
||
<div
|
||
style="
|
||
width: 20px;
|
||
font-size: 11.5px;
|
||
font-variant-numeric: tabular-nums;
|
||
color: #5f5d68;
|
||
text-align: center;
|
||
"
|
||
>
|
||
{{ t.n }}
|
||
</div>
|
||
<div style="min-width: 0; flex: 1">
|
||
<div
|
||
style="font-size:12.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:{{ t.titleColor }}"
|
||
>
|
||
{{ t.title }}
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 11px;
|
||
color: #7f7f83;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-top: 2px;
|
||
"
|
||
>
|
||
{{ t.artist }}
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 11.5px;
|
||
font-variant-numeric: tabular-nums;
|
||
color: #6c6c70;
|
||
"
|
||
>
|
||
{{ t.dur }}
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ALBUM GRID -->
|
||
<section style="margin-bottom: 48px">
|
||
<div
|
||
style="
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
margin-bottom: 18px;
|
||
"
|
||
>
|
||
<div>
|
||
<h2
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 17.5px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.025em;
|
||
margin: 0;
|
||
color: #d3d2d6;
|
||
"
|
||
>
|
||
Mixes
|
||
</h2>
|
||
<div style="font-size: 12px; color: #7d7d81; margin-top: 5px">
|
||
Generados a partir de lo que más repites
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(164px, 1fr));
|
||
gap: 20px;
|
||
"
|
||
>
|
||
<sc-for list="{{ albums }}" as="it" hint-placeholder-count="6">
|
||
<div
|
||
onMouseEnter="{{ it.enter }}"
|
||
onMouseLeave="{{ it.leave }}"
|
||
onClick="{{ it.play }}"
|
||
style="cursor:pointer;padding:10px;margin:-10px;border-radius:16px;background:{{ it.cardBg }};transition:background .18s ease;transform:{{ it.lift }}"
|
||
>
|
||
<div
|
||
style="position:relative;aspect-ratio:1;border-radius:13px;overflow:hidden;background:{{ it.fallback }};box-shadow:0 14px 32px rgba(0,0,0,.45)"
|
||
>
|
||
<div
|
||
style="width:100%;height:100%;background-image:url({{ it.img }});background-size:cover;background-position:center;opacity:.94"
|
||
></div>
|
||
<div
|
||
style="
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(
|
||
180deg,
|
||
transparent 45%,
|
||
rgba(0, 0, 0, 0.42)
|
||
);
|
||
"
|
||
></div>
|
||
<div
|
||
style="position:absolute;right:10px;bottom:10px;width:38px;height:38px;border-radius:50%;background:rgba(10,10,13,.76);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.12);display:grid;place-items:center;color:#dad9de;transition:opacity .18s ease,transform .18s ease;opacity:{{ it.btnOpacity }};transform:{{ it.btnLift }}"
|
||
>
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
||
<path d="M8 5v14l11-7z"></path>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="
|
||
margin-top: 13px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.01em;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
color: #cbcacf;
|
||
"
|
||
>
|
||
{{ it.title }}
|
||
</div>
|
||
<div
|
||
style="
|
||
margin-top: 4px;
|
||
font-size: 11.5px;
|
||
color: #7f7f83;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
"
|
||
>
|
||
{{ it.sub }}
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ARTISTS -->
|
||
<section style="margin-bottom: 48px">
|
||
<h2
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 17.5px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.025em;
|
||
margin: 0 0 18px;
|
||
color: #d3d2d6;
|
||
"
|
||
>
|
||
Artistas que sigues
|
||
</h2>
|
||
<div
|
||
style="
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
||
gap: 18px;
|
||
"
|
||
>
|
||
<sc-for list="{{ artists }}" as="a" hint-placeholder-count="7">
|
||
<div
|
||
onMouseEnter="{{ a.enter }}"
|
||
onMouseLeave="{{ a.leave }}"
|
||
onClick="{{ a.play }}"
|
||
style="cursor:pointer;text-align:center;padding:10px;margin:-10px;border-radius:16px;background:{{ a.cardBg }};transition:background .18s ease;transform:{{ a.lift }}"
|
||
>
|
||
<div
|
||
style="aspect-ratio:1;border-radius:50%;overflow:hidden;background:{{ a.fallback }};box-shadow:0 12px 28px rgba(0,0,0,.4)"
|
||
>
|
||
<div
|
||
style="width:100%;height:100%;background-image:url({{ a.img }});background-size:cover;background-position:center;opacity:.92"
|
||
></div>
|
||
</div>
|
||
<div
|
||
style="
|
||
margin-top: 13px;
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
color: #cbcacf;
|
||
"
|
||
>
|
||
{{ a.title }}
|
||
</div>
|
||
<div
|
||
style="
|
||
margin-top: 4px;
|
||
font-size: 11px;
|
||
color: #7d7d81;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
"
|
||
>
|
||
{{ a.sub }}
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- POPULAR -->
|
||
<section style="margin-bottom: 48px">
|
||
<div
|
||
style="
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
margin-bottom: 16px;
|
||
"
|
||
>
|
||
<div>
|
||
<h2
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 17.5px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.025em;
|
||
margin: 0;
|
||
color: #d3d2d6;
|
||
"
|
||
>
|
||
Populares esta semana
|
||
</h2>
|
||
<div style="font-size: 12px; color: #7d7d81; margin-top: 5px">
|
||
Lo más escuchado entre quienes comparten tu gusto
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(330px, 1fr));
|
||
gap: 6px 34px;
|
||
"
|
||
>
|
||
<sc-for list="{{ popular }}" as="t" hint-placeholder-count="8">
|
||
<div
|
||
onMouseEnter="{{ t.enter }}"
|
||
onMouseLeave="{{ t.leave }}"
|
||
onClick="{{ t.play }}"
|
||
style="display:flex;align-items:center;gap:14px;padding:8px 12px;border-radius:11px;cursor:pointer;background:{{ t.rowBg }};transition:background .15s ease"
|
||
>
|
||
<div
|
||
style="
|
||
width: 18px;
|
||
font-size: 12px;
|
||
font-variant-numeric: tabular-nums;
|
||
color: #646468;
|
||
text-align: center;
|
||
display: flex;
|
||
justify-content: center;
|
||
height: 14px;
|
||
align-items: flex-end;
|
||
"
|
||
>
|
||
<sc-if value="{{ t.isCurrent }}" hint-placeholder-val="{{ false }}">
|
||
<div
|
||
style="display: flex; align-items: flex-end; gap: 2px; height: 13px"
|
||
>
|
||
<div
|
||
style="width:2px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out infinite"
|
||
></div>
|
||
<div
|
||
style="width:2px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out .3s infinite"
|
||
></div>
|
||
<div
|
||
style="width:2px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out .6s infinite"
|
||
></div>
|
||
</div>
|
||
</sc-if>
|
||
<sc-if value="{{ t.showIndex }}" hint-placeholder-val="{{ true }}">
|
||
<span>{{ t.n }}</span>
|
||
</sc-if>
|
||
</div>
|
||
<div
|
||
style="width:38px;height:38px;border-radius:8px;overflow:hidden;flex:0 0 auto;background:{{ t.fallback }}"
|
||
>
|
||
<div
|
||
style="width:100%;height:100%;background-image:url({{ t.img }});background-size:cover;background-position:center;opacity:.9"
|
||
></div>
|
||
</div>
|
||
<div style="min-width: 0; flex: 1">
|
||
<div
|
||
style="font-size:12.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:{{ t.titleColor }}"
|
||
>
|
||
{{ t.title }}
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 11.5px;
|
||
color: #7f7f83;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-top: 2px;
|
||
"
|
||
>
|
||
{{ t.artist }}
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 11px;
|
||
color: #6c6c70;
|
||
font-variant-numeric: tabular-nums;
|
||
"
|
||
>
|
||
{{ t.plays }}
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 11.5px;
|
||
color: #6c6c70;
|
||
font-variant-numeric: tabular-nums;
|
||
"
|
||
>
|
||
{{ t.dur }}
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- USER PLAYLISTS -->
|
||
<section style="margin-bottom: 20px">
|
||
<div
|
||
style="
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
margin-bottom: 18px;
|
||
"
|
||
>
|
||
<div>
|
||
<h2
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 17.5px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.025em;
|
||
margin: 0;
|
||
color: #d3d2d6;
|
||
"
|
||
>
|
||
Tus playlists
|
||
</h2>
|
||
<div style="font-size: 12px; color: #7d7d81; margin-top: 5px">
|
||
Creadas y guardadas por ti
|
||
</div>
|
||
</div>
|
||
<div
|
||
onClick="{{ goPlaylists }}"
|
||
style="
|
||
font-size: 11.5px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.07em;
|
||
text-transform: uppercase;
|
||
color: #747478;
|
||
cursor: pointer;
|
||
"
|
||
style-hover="color:#c9c8cd"
|
||
>
|
||
Ver todas
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(258px, 1fr));
|
||
gap: 14px;
|
||
"
|
||
>
|
||
<sc-for list="{{ myPlaylists }}" as="p" hint-placeholder-count="6">
|
||
<div
|
||
onMouseEnter="{{ p.enter }}"
|
||
onMouseLeave="{{ p.leave }}"
|
||
onClick="{{ p.open }}"
|
||
style="display:flex;align-items:center;gap:14px;padding:12px;border-radius:15px;cursor:pointer;background:{{ p.cardBg }};border:1px solid {{ p.border }};transition:background .18s ease;transform:{{ p.lift }}"
|
||
>
|
||
<div
|
||
style="width:62px;height:62px;border-radius:11px;overflow:hidden;flex:0 0 auto;background:{{ p.fallback }};box-shadow:0 10px 24px rgba(0,0,0,.42)"
|
||
>
|
||
<div
|
||
style="width:100%;height:100%;background-image:url({{ p.img }});background-size:cover;background-position:center;opacity:.92"
|
||
></div>
|
||
</div>
|
||
<div style="min-width: 0; flex: 1">
|
||
<div
|
||
style="font-size:13.5px;font-weight:600;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:{{ p.titleColor }}"
|
||
>
|
||
{{ p.name }}
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 11.5px;
|
||
color: #818185;
|
||
margin-top: 4px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
"
|
||
>
|
||
{{ p.meta }}
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 11px;
|
||
color: #696a6d;
|
||
margin-top: 3px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
"
|
||
>
|
||
{{ p.artistsLine }}
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="flex:0 0 auto;color:{{ accent }};opacity:{{ p.btnOpacity }};transition:opacity .16s ease"
|
||
>
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||
<path d="M8 5v14l11-7z"></path>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</sc-if>
|
||
|
||
<!-- EXPLORE -->
|
||
<sc-if value="{{ showExplore }}" hint-placeholder-val="{{ false }}">
|
||
<div data-screen-label="Explore" style="animation: rise 0.3s ease">
|
||
<h1
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.03em;
|
||
margin: 12px 0 6px;
|
||
color: #eeecf3;
|
||
"
|
||
>
|
||
Descubrir
|
||
</h1>
|
||
<div style="font-size: 13px; color: #8a8a8e; margin-bottom: 30px">
|
||
Rutas de escucha fuera de tu historial. Se renuevan cada 12 horas.
|
||
</div>
|
||
<div
|
||
style="
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(228px, 1fr));
|
||
gap: 14px;
|
||
margin-bottom: 46px;
|
||
"
|
||
>
|
||
<sc-for list="{{ genres }}" as="g" hint-placeholder-count="8">
|
||
<div
|
||
style="position:relative;overflow:hidden;height:112px;border-radius:16px;padding:17px;cursor:pointer;border:1px solid rgba(255,255,255,.05);background:{{ g.bg }};transition:background .18s ease"
|
||
style-hover="background:rgba(255,255,255,.03)"
|
||
>
|
||
<div
|
||
style="
|
||
position: relative;
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 15.5px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.02em;
|
||
color: #d4d3d7;
|
||
"
|
||
>
|
||
{{ g.name }}
|
||
</div>
|
||
<div
|
||
style="
|
||
position: relative;
|
||
font-size: 11.5px;
|
||
color: #919195;
|
||
margin-top: 6px;
|
||
"
|
||
>
|
||
{{ g.meta }}
|
||
</div>
|
||
<div
|
||
style="
|
||
position: absolute;
|
||
right: -16px;
|
||
bottom: -22px;
|
||
width: 80px;
|
||
height: 80px;
|
||
border-radius: 18px;
|
||
overflow: hidden;
|
||
transform: rotate(20deg);
|
||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.4);
|
||
"
|
||
>
|
||
<div
|
||
style="width:100%;height:100%;background-image:url({{ g.img }});background-size:cover;background-position:center;opacity:.82"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
<section style="margin-bottom: 44px">
|
||
<h2
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 17.5px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.025em;
|
||
margin: 0 0 5px;
|
||
color: #d3d2d6;
|
||
"
|
||
>
|
||
Fuera de tu burbuja
|
||
</h2>
|
||
<div style="font-size: 12px; color: #7d7d81; margin-bottom: 18px">
|
||
Álbumes que tu historial nunca te habría servido
|
||
</div>
|
||
<div
|
||
style="
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(164px, 1fr));
|
||
gap: 20px;
|
||
"
|
||
>
|
||
<sc-for list="{{ discover }}" as="it" hint-placeholder-count="6">
|
||
<div
|
||
onMouseEnter="{{ it.enter }}"
|
||
onMouseLeave="{{ it.leave }}"
|
||
onClick="{{ it.play }}"
|
||
style="cursor:pointer;padding:10px;margin:-10px;border-radius:16px;background:{{ it.cardBg }};transition:background .18s ease;transform:{{ it.lift }}"
|
||
>
|
||
<div
|
||
style="position:relative;aspect-ratio:1;border-radius:13px;overflow:hidden;background:{{ it.fallback }};box-shadow:0 14px 32px rgba(0,0,0,.45)"
|
||
>
|
||
<div
|
||
style="width:100%;height:100%;background-image:url({{ it.img }});background-size:cover;background-position:center;opacity:.94"
|
||
></div>
|
||
<div
|
||
style="position:absolute;right:10px;bottom:10px;width:38px;height:38px;border-radius:50%;background:rgba(10,10,13,.76);border:1px solid rgba(255,255,255,.12);display:grid;place-items:center;color:#dad9de;opacity:{{ it.btnOpacity }};transition:opacity .18s ease"
|
||
>
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
||
<path d="M8 5v14l11-7z"></path>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="
|
||
margin-top: 13px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
color: #cbcacf;
|
||
"
|
||
>
|
||
{{ it.title }}
|
||
</div>
|
||
<div
|
||
style="
|
||
margin-top: 4px;
|
||
font-size: 11.5px;
|
||
color: #7f7f83;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
"
|
||
>
|
||
{{ it.sub }}
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</sc-if>
|
||
|
||
<!-- TRACK LIST -->
|
||
<sc-if value="{{ showList }}" hint-placeholder-val="{{ false }}">
|
||
<div data-screen-label="Tracklist" style="animation: rise 0.3s ease">
|
||
<div
|
||
style="
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 26px;
|
||
flex-wrap: wrap;
|
||
padding: 14px 0 32px;
|
||
"
|
||
>
|
||
<div
|
||
style="width:164px;height:164px;border-radius:18px;flex:0 0 auto;overflow:hidden;background:{{ listFallback }};box-shadow:0 24px 56px rgba(0,0,0,.55)"
|
||
>
|
||
<div
|
||
style="width:100%;height:100%;background-image:url({{ listImg }});background-size:cover;background-position:center"
|
||
></div>
|
||
</div>
|
||
<div style="min-width: 240px">
|
||
<div
|
||
style="
|
||
font-size: 10.5px;
|
||
letter-spacing: 0.16em;
|
||
text-transform: uppercase;
|
||
color: #8a8a8e;
|
||
font-weight: 600;
|
||
margin-bottom: 12px;
|
||
"
|
||
>
|
||
{{ listKind }}
|
||
</div>
|
||
<h1
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 33px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.035em;
|
||
margin: 0 0 13px;
|
||
color: #d9d8dc;
|
||
text-wrap: pretty;
|
||
"
|
||
>
|
||
{{ listTitle }}
|
||
</h1>
|
||
<div style="font-size: 12.5px; color: #8a8a8e">{{ listMeta }}</div>
|
||
<div style="display: flex; gap: 10px; margin-top: 22px">
|
||
<div
|
||
onClick="{{ playHero }}"
|
||
style="
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 9px;
|
||
padding: 11px 21px;
|
||
border-radius: 11px;
|
||
background: #c6c4cb;
|
||
color: #0a0a0d;
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
"
|
||
style-hover="background:#e3e1ea"
|
||
>
|
||
<svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor">
|
||
<path d="M8 5v14l11-7z"></path></svg
|
||
>Reproducir
|
||
</div>
|
||
<div
|
||
style="
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 11px;
|
||
border: 1px solid rgba(255, 255, 255, 0.09);
|
||
display: grid;
|
||
place-items: center;
|
||
cursor: pointer;
|
||
color: #919195;
|
||
"
|
||
style-hover="color:#dbdadf;background:rgba(255,255,255,.04)"
|
||
>
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
width="15"
|
||
height="15"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.7"
|
||
stroke-linecap="round"
|
||
>
|
||
<path
|
||
d="M16 3l5 5-5 5M21 8H9a4 4 0 00-4 4M8 21l-5-5 5-5M3 16h12a4 4 0 004-4"
|
||
></path>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
style="
|
||
display: grid;
|
||
grid-template-columns: 32px 1fr 1fr 92px;
|
||
gap: 16px;
|
||
padding: 0 14px 10px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||
font-size: 10.5px;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
color: #616165;
|
||
font-weight: 600;
|
||
"
|
||
>
|
||
<div>#</div>
|
||
<div>Título</div>
|
||
<div>Álbum</div>
|
||
<div style="text-align: right">Duración</div>
|
||
</div>
|
||
<sc-for list="{{ listTracks }}" as="t" hint-placeholder-count="8">
|
||
<div
|
||
onMouseEnter="{{ t.enter }}"
|
||
onMouseLeave="{{ t.leave }}"
|
||
onClick="{{ t.play }}"
|
||
style="display:grid;grid-template-columns:32px 1fr 1fr 92px;gap:16px;align-items:center;padding:10px 14px;border-radius:12px;cursor:pointer;background:{{ t.rowBg }};transition:background .15s ease"
|
||
>
|
||
<div
|
||
style="
|
||
font-size: 12px;
|
||
font-variant-numeric: tabular-nums;
|
||
color: #646468;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 3px;
|
||
height: 14px;
|
||
"
|
||
>
|
||
<sc-if value="{{ t.isCurrent }}" hint-placeholder-val="{{ false }}">
|
||
<div style="display: flex; align-items: flex-end; gap: 2px; height: 13px">
|
||
<div
|
||
style="width:2.5px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out infinite"
|
||
></div>
|
||
<div
|
||
style="width:2.5px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out .25s infinite"
|
||
></div>
|
||
<div
|
||
style="width:2.5px;height:100%;border-radius:2px;background:{{ accent }};animation:eq .9s ease-in-out .5s infinite"
|
||
></div>
|
||
</div>
|
||
</sc-if>
|
||
<sc-if value="{{ t.showIndex }}" hint-placeholder-val="{{ true }}">
|
||
<span>{{ t.n }}</span>
|
||
</sc-if>
|
||
</div>
|
||
<div style="display: flex; align-items: center; gap: 13px; min-width: 0">
|
||
<div
|
||
style="width:40px;height:40px;border-radius:9px;flex:0 0 auto;overflow:hidden;background:{{ t.fallback }}"
|
||
>
|
||
<div
|
||
style="width:100%;height:100%;background-image:url({{ t.img }});background-size:cover;background-position:center;opacity:.9"
|
||
></div>
|
||
</div>
|
||
<div style="min-width: 0">
|
||
<div
|
||
style="font-size:13px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:{{ t.titleColor }}"
|
||
>
|
||
{{ t.title }}
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 11.5px;
|
||
color: #7f7f83;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-top: 3px;
|
||
"
|
||
>
|
||
{{ t.artist }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 12px;
|
||
color: #7f7f83;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
"
|
||
>
|
||
{{ t.album }}
|
||
</div>
|
||
<div
|
||
style="
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 14px;
|
||
"
|
||
>
|
||
<div
|
||
onClick="{{ t.like }}"
|
||
style="color:{{ t.likeColor }};display:grid;place-items:center;opacity:{{ t.likeOpacity }};transition:opacity .15s ease"
|
||
>
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
width="14"
|
||
height="14"
|
||
fill="{{ t.likeFill }}"
|
||
stroke="currentColor"
|
||
stroke-width="1.7"
|
||
stroke-linecap="round"
|
||
>
|
||
<path
|
||
d="M12 20s-7-4.6-7-9.4A3.9 3.9 0 0112 8a3.9 3.9 0 017 2.6C19 15.4 12 20 12 20z"
|
||
></path>
|
||
</svg>
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 12px;
|
||
font-variant-numeric: tabular-nums;
|
||
color: #7f7f83;
|
||
min-width: 34px;
|
||
text-align: right;
|
||
"
|
||
>
|
||
{{ t.dur }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</sc-if>
|
||
|
||
<!-- PLAYLISTS INDEX -->
|
||
<sc-if value="{{ showPlaylistIndex }}" hint-placeholder-val="{{ false }}">
|
||
<div data-screen-label="Playlists" style="animation: rise 0.3s ease">
|
||
<div
|
||
style="
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 14px;
|
||
margin: 12px 0 28px;
|
||
"
|
||
>
|
||
<div>
|
||
<h1
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.03em;
|
||
margin: 0 0 6px;
|
||
color: #eeecf3;
|
||
"
|
||
>
|
||
Tus playlists
|
||
</h1>
|
||
<div style="font-size: 13px; color: #8a8a8e">
|
||
7 playlists · 564 canciones · 38 h 12 min
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="
|
||
padding: 11px 18px;
|
||
border-radius: 11px;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
font-size: 12.5px;
|
||
font-weight: 500;
|
||
color: #aeaeb2;
|
||
cursor: pointer;
|
||
"
|
||
style-hover="color:#d9d8dd;background:rgba(255,255,255,.04)"
|
||
>
|
||
Nueva playlist
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(196px, 1fr));
|
||
gap: 22px;
|
||
"
|
||
>
|
||
<sc-for list="{{ myPlaylists }}" as="p" hint-placeholder-count="7">
|
||
<div
|
||
onMouseEnter="{{ p.enter }}"
|
||
onMouseLeave="{{ p.leave }}"
|
||
onClick="{{ p.open }}"
|
||
style="cursor:pointer;padding:10px;margin:-10px;border-radius:16px;background:{{ p.cardBg }};transition:background .18s ease;transform:{{ p.lift }}"
|
||
>
|
||
<div
|
||
style="position:relative;aspect-ratio:1;border-radius:14px;overflow:hidden;background:{{ p.fallback }};box-shadow:0 14px 32px rgba(0,0,0,.45)"
|
||
>
|
||
<div
|
||
style="width:100%;height:100%;background-image:url({{ p.img }});background-size:cover;background-position:center;opacity:.93"
|
||
></div>
|
||
<div
|
||
style="
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(
|
||
180deg,
|
||
transparent 50%,
|
||
rgba(0, 0, 0, 0.5)
|
||
);
|
||
"
|
||
></div>
|
||
<div
|
||
style="position:absolute;right:10px;bottom:10px;width:38px;height:38px;border-radius:50%;background:rgba(10,10,13,.78);border:1px solid rgba(255,255,255,.12);display:grid;place-items:center;color:#dad9de;opacity:{{ p.btnOpacity }};transition:opacity .18s ease"
|
||
>
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
||
<path d="M8 5v14l11-7z"></path>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="margin-top:13px;font-size:13.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:{{ p.titleColor }}"
|
||
>
|
||
{{ p.name }}
|
||
</div>
|
||
<div
|
||
style="
|
||
margin-top: 4px;
|
||
font-size: 11.5px;
|
||
color: #7f7f83;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
"
|
||
>
|
||
{{ p.meta }}
|
||
</div>
|
||
<div
|
||
style="
|
||
margin-top: 3px;
|
||
font-size: 11px;
|
||
color: #696a6d;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
"
|
||
>
|
||
{{ p.artistsLine }}
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</div>
|
||
</sc-if>
|
||
|
||
<!-- EMPTY -->
|
||
<sc-if value="{{ showEmpty }}" hint-placeholder-val="{{ false }}">
|
||
<div
|
||
data-screen-label="Empty"
|
||
style="
|
||
max-width: 420px;
|
||
margin: 13vh auto;
|
||
text-align: center;
|
||
animation: rise 0.3s ease;
|
||
"
|
||
>
|
||
<div
|
||
style="
|
||
width: 70px;
|
||
height: 70px;
|
||
margin: 0 auto 24px;
|
||
border-radius: 19px;
|
||
background: linear-gradient(
|
||
150deg,
|
||
rgba(156, 134, 240, 0.1),
|
||
rgba(79, 191, 174, 0.05)
|
||
);
|
||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||
display: grid;
|
||
place-items: center;
|
||
"
|
||
>
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
width="25"
|
||
height="25"
|
||
fill="none"
|
||
stroke="#919195"
|
||
stroke-width="1.5"
|
||
stroke-linecap="round"
|
||
>
|
||
<path d="M9 18V6l10-2v12"></path>
|
||
<circle cx="6.5" cy="18" r="2.5"></circle>
|
||
<circle cx="16.5" cy="16" r="2.5"></circle>
|
||
</svg>
|
||
</div>
|
||
<div
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.025em;
|
||
margin-bottom: 10px;
|
||
color: #d5d4d8;
|
||
"
|
||
>
|
||
{{ emptyTitle }}
|
||
</div>
|
||
<div
|
||
style="font-size: 13px; line-height: 1.65; color: #8a8a8e; margin-bottom: 24px"
|
||
>
|
||
{{ emptyBody }}
|
||
</div>
|
||
<div style="display: flex; gap: 10px; justify-content: center; flex-wrap: wrap">
|
||
<div
|
||
onClick="{{ goExplore }}"
|
||
style="
|
||
padding: 11px 20px;
|
||
border-radius: 11px;
|
||
background: #c6c4cb;
|
||
color: #0a0a0d;
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
"
|
||
style-hover="background:#d6d5da"
|
||
>
|
||
{{ emptyCta }}
|
||
</div>
|
||
<div
|
||
onClick="{{ goHome }}"
|
||
style="
|
||
padding: 11px 20px;
|
||
border-radius: 11px;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
font-size: 12.5px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
color: #aeaeb2;
|
||
"
|
||
style-hover="color:#dbdadf;background:rgba(255,255,255,.04)"
|
||
>
|
||
Volver al inicio
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</sc-if>
|
||
|
||
<!-- NO RESULTS -->
|
||
<sc-if value="{{ showNoResults }}" hint-placeholder-val="{{ false }}">
|
||
<div
|
||
data-screen-label="No results"
|
||
style="
|
||
max-width: 450px;
|
||
margin: 13vh auto;
|
||
text-align: center;
|
||
animation: rise 0.3s ease;
|
||
"
|
||
>
|
||
<div
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.025em;
|
||
margin-bottom: 10px;
|
||
color: #d5d4d8;
|
||
"
|
||
>
|
||
Sin resultados para “{{ query }}”
|
||
</div>
|
||
<div
|
||
style="font-size: 13px; line-height: 1.65; color: #8a8a8e; margin-bottom: 22px"
|
||
>
|
||
Revisa la ortografía o prueba con un término más corto. También puedes buscar
|
||
por álbum o por playlist.
|
||
</div>
|
||
<div style="display: flex; gap: 8px; justify-content: center; flex-wrap: wrap">
|
||
<sc-for list="{{ suggestions }}" as="s" hint-placeholder-count="4">
|
||
<div
|
||
onClick="{{ s.go }}"
|
||
style="
|
||
padding: 8px 14px;
|
||
border-radius: 999px;
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
font-size: 12px;
|
||
color: #9b9b9f;
|
||
cursor: pointer;
|
||
"
|
||
style-hover="color:#dbdadf;background:rgba(255,255,255,.05)"
|
||
>
|
||
{{ s.label }}
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</div>
|
||
</sc-if>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- QUEUE -->
|
||
<sc-if value="{{ queueOpen }}" hint-placeholder-val="{{ false }}">
|
||
<aside
|
||
data-screen-label="Queue"
|
||
style="width:{{ queueW }};flex:0 0 auto;display:flex;flex-direction:column;border-left:1px solid {{ hairline }};padding:20px 16px;padding-bottom:{{ bottomPad }};animation:rise .22s ease;background:{{ panelBg }};transition:background .6s ease"
|
||
>
|
||
<div
|
||
style="
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 20px;
|
||
"
|
||
>
|
||
<div
|
||
style="
|
||
font-family: 'Sora', sans-serif;
|
||
font-size: 14.5px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.02em;
|
||
color: #d3d2d6;
|
||
"
|
||
>
|
||
En cola
|
||
</div>
|
||
<div
|
||
onClick="{{ toggleQueue }}"
|
||
style="font-size: 16px; color: #747478; cursor: pointer; line-height: 1"
|
||
style-hover="color:#d0cfd4"
|
||
>
|
||
×
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 10.5px;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
color: #616165;
|
||
font-weight: 600;
|
||
margin-bottom: 11px;
|
||
"
|
||
>
|
||
Reproduciendo
|
||
</div>
|
||
<div
|
||
style="display:flex;align-items:center;gap:12px;padding:10px;border-radius:12px;background:{{ accentSoft }};border:1px solid {{ accentLine }};margin-bottom:24px"
|
||
>
|
||
<div
|
||
style="width:40px;height:40px;border-radius:9px;flex:0 0 auto;overflow:hidden;background:{{ curFallback }}"
|
||
>
|
||
<div
|
||
style="width:100%;height:100%;background-image:url({{ curImg }});background-size:cover;background-position:center"
|
||
></div>
|
||
</div>
|
||
<div style="min-width: 0">
|
||
<div
|
||
style="font-size:12.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:{{ accentTitle }}"
|
||
>
|
||
{{ curTitle }}
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 11px;
|
||
color: #848488;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-top: 3px;
|
||
"
|
||
>
|
||
{{ curArtist }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 11px;
|
||
"
|
||
>
|
||
<div
|
||
style="
|
||
min-width: 0;
|
||
flex: 1 1 auto;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
font-size: 10.5px;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
color: #616165;
|
||
font-weight: 600;
|
||
"
|
||
>
|
||
A continuación · {{ queueSource }}
|
||
</div>
|
||
<div
|
||
style="flex: 0 0 auto; font-size: 11px; color: #646468; cursor: pointer"
|
||
style-hover="color:#c9c8cd"
|
||
>
|
||
Vaciar
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 1px"
|
||
>
|
||
<sc-for list="{{ queue }}" as="q" hint-placeholder-count="6">
|
||
<div
|
||
onClick="{{ q.play }}"
|
||
style="
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 9px 10px;
|
||
border-radius: 11px;
|
||
cursor: pointer;
|
||
transition: background 0.15s ease;
|
||
"
|
||
style-hover="background:rgba(255,255,255,.04)"
|
||
>
|
||
<div
|
||
style="width:36px;height:36px;border-radius:8px;flex:0 0 auto;overflow:hidden;background:{{ q.fallback }}"
|
||
>
|
||
<div
|
||
style="width:100%;height:100%;background-image:url({{ q.img }});background-size:cover;background-position:center;opacity:.88"
|
||
></div>
|
||
</div>
|
||
<div style="min-width: 0; flex: 1">
|
||
<div
|
||
style="
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
color: #c2c1c6;
|
||
"
|
||
>
|
||
{{ q.title }}
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 11px;
|
||
color: #7d7d81;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-top: 2px;
|
||
"
|
||
>
|
||
{{ q.artist }}
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="font-size: 11px; font-variant-numeric: tabular-nums; color: #696a6d"
|
||
>
|
||
{{ q.dur }}
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</aside>
|
||
</sc-if>
|
||
</div>
|
||
|
||
<!-- PLAYER -->
|
||
<div
|
||
data-screen-label="Player"
|
||
style="
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
padding: 0 16px 16px;
|
||
pointer-events: none;
|
||
"
|
||
>
|
||
<div
|
||
style="pointer-events:auto;display:flex;align-items:center;gap:18px;padding:11px 16px;border-radius:18px;border:1px solid {{ hairline }};background:{{ barBg }};backdrop-filter:blur(22px);box-shadow:0 20px 56px rgba(0,0,0,.6);transition:background .6s ease"
|
||
>
|
||
<div
|
||
style="display:flex;align-items:center;gap:13px;min-width:0;width:{{ playerInfoW }}"
|
||
>
|
||
<div
|
||
style="width:48px;height:48px;border-radius:11px;flex:0 0 auto;overflow:hidden;background:{{ curFallback }};box-shadow:0 8px 20px rgba(0,0,0,.5)"
|
||
>
|
||
<div
|
||
style="width:100%;height:100%;background-image:url({{ curImg }});background-size:cover;background-position:center"
|
||
></div>
|
||
</div>
|
||
<div style="min-width: 0; flex: 1">
|
||
<div
|
||
style="
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.01em;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
color: #d4d3d7;
|
||
"
|
||
>
|
||
{{ curTitle }}
|
||
</div>
|
||
<div
|
||
style="
|
||
font-size: 11.5px;
|
||
color: #848488;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-top: 3px;
|
||
"
|
||
>
|
||
{{ curArtist }}
|
||
</div>
|
||
</div>
|
||
<div
|
||
onClick="{{ likeCurrent }}"
|
||
style="flex:0 0 auto;cursor:pointer;color:{{ curLikeColor }}"
|
||
style-hover="opacity:.8"
|
||
>
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
width="16"
|
||
height="16"
|
||
fill="{{ curLikeFill }}"
|
||
stroke="currentColor"
|
||
stroke-width="1.7"
|
||
stroke-linecap="round"
|
||
>
|
||
<path
|
||
d="M12 20s-7-4.6-7-9.4A3.9 3.9 0 0112 8a3.9 3.9 0 017 2.6C19 15.4 12 20 12 20z"
|
||
></path>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
style="
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
"
|
||
>
|
||
<div style="display: flex; align-items: center; gap: 14px">
|
||
<div
|
||
onClick="{{ toggleShuffle }}"
|
||
style="cursor:pointer;color:{{ shuffleColor }};display:{{ extrasDisplay }}"
|
||
style-hover="color:#d0cfd4"
|
||
>
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
width="15"
|
||
height="15"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.8"
|
||
stroke-linecap="round"
|
||
>
|
||
<path d="M16 3l4 4-4 4M4 7h16M8 21l-4-4 4-4M20 17H4"></path>
|
||
</svg>
|
||
</div>
|
||
<div
|
||
onClick="{{ prev }}"
|
||
style="cursor: pointer; color: #aaaaae"
|
||
style-hover="color:#dddce1"
|
||
>
|
||
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
||
<path d="M7 6h2v12H7zm3 6l9-6v12z"></path>
|
||
</svg>
|
||
</div>
|
||
<div
|
||
onClick="{{ toggle }}"
|
||
style="
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
background: #c6c4cb;
|
||
color: #0a0a0d;
|
||
display: grid;
|
||
place-items: center;
|
||
cursor: pointer;
|
||
transition: background 0.14s ease;
|
||
"
|
||
style-hover="background:#d6d5da"
|
||
>
|
||
<sc-if value="{{ playing }}" hint-placeholder-val="{{ true }}">
|
||
<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor">
|
||
<path d="M7 5h3.5v14H7zM13.5 5H17v14h-3.5z"></path>
|
||
</svg>
|
||
</sc-if>
|
||
<sc-if value="{{ paused }}" hint-placeholder-val="{{ false }}">
|
||
<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor">
|
||
<path d="M8 5v14l11-7z"></path>
|
||
</svg>
|
||
</sc-if>
|
||
</div>
|
||
<div
|
||
onClick="{{ next }}"
|
||
style="cursor: pointer; color: #aaaaae"
|
||
style-hover="color:#dddce1"
|
||
>
|
||
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
||
<path d="M15 6h2v12h-2zM5 6l9 6-9 6z"></path>
|
||
</svg>
|
||
</div>
|
||
<div
|
||
onClick="{{ toggleRepeat }}"
|
||
style="cursor:pointer;color:{{ repeatColor }};display:{{ extrasDisplay }}"
|
||
style-hover="color:#d0cfd4"
|
||
>
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
width="15"
|
||
height="15"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.8"
|
||
stroke-linecap="round"
|
||
>
|
||
<path
|
||
d="M17 2l3 3-3 3M20 5H8a4 4 0 00-4 4v1M7 22l-3-3 3-3M4 19h12a4 4 0 004-4v-1"
|
||
></path>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style="display: flex; align-items: center; gap: 11px; width: 100%; max-width: 540px"
|
||
>
|
||
<span
|
||
style="
|
||
font-size: 10.5px;
|
||
font-variant-numeric: tabular-nums;
|
||
color: #747478;
|
||
min-width: 32px;
|
||
text-align: right;
|
||
"
|
||
>{{ elapsed }}</span
|
||
>
|
||
<div
|
||
onClick="{{ seek }}"
|
||
style="flex: 1; height: 14px; display: flex; align-items: center; cursor: pointer"
|
||
>
|
||
<div
|
||
style="
|
||
width: 100%;
|
||
height: 3.5px;
|
||
border-radius: 4px;
|
||
background: rgba(255, 255, 255, 0.09);
|
||
overflow: hidden;
|
||
"
|
||
>
|
||
<div
|
||
style="height:100%;border-radius:4px;background:{{ accent }};width:{{ progressPct }}"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
<span
|
||
style="
|
||
font-size: 10.5px;
|
||
font-variant-numeric: tabular-nums;
|
||
color: #747478;
|
||
min-width: 32px;
|
||
"
|
||
>{{ total }}</span
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
style="display:{{ extrasDisplay }};align-items:center;gap:14px;width:{{ playerInfoW }};justify-content:flex-end"
|
||
>
|
||
<div style="cursor: pointer; color: #747478" style-hover="color:#d0cfd4">
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
width="15"
|
||
height="15"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.8"
|
||
stroke-linecap="round"
|
||
>
|
||
<path d="M4 6h9M4 12h9M4 18h5M17 10v8"></path>
|
||
</svg>
|
||
</div>
|
||
<div style="display: flex; align-items: center; gap: 9px">
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
width="15"
|
||
height="15"
|
||
fill="none"
|
||
stroke="#747478"
|
||
stroke-width="1.8"
|
||
stroke-linecap="round"
|
||
>
|
||
<path d="M4 9h3l4-3v12l-4-3H4z"></path>
|
||
<path d="M16 9a4 4 0 010 6"></path>
|
||
</svg>
|
||
<div
|
||
onClick="{{ setVol }}"
|
||
style="
|
||
width: 84px;
|
||
height: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
"
|
||
>
|
||
<div
|
||
style="
|
||
width: 100%;
|
||
height: 3.5px;
|
||
border-radius: 4px;
|
||
background: rgba(255, 255, 255, 0.09);
|
||
overflow: hidden;
|
||
"
|
||
>
|
||
<div
|
||
style="height:100%;border-radius:4px;background:#8e8c98;width:{{ volPct }}"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
onClick="{{ toggleQueue }}"
|
||
style="cursor:pointer;color:{{ queueBtnColor }}"
|
||
style-hover="color:#d0cfd4"
|
||
>
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
width="16"
|
||
height="16"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.8"
|
||
stroke-linecap="round"
|
||
>
|
||
<path d="M3 7h12M3 12h12M3 17h8M19 9v9"></path>
|
||
<circle cx="17" cy="18" r="2"></circle>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MOBILE TABS -->
|
||
<div
|
||
style="display:{{ tabsDisplay }};pointer-events:auto;margin-top:10px;padding:7px;border-radius:18px;border:1px solid {{ hairline }};background:{{ barBg }};backdrop-filter:blur(22px);justify-content:space-around;transition:background .6s ease"
|
||
>
|
||
<sc-for list="{{ nav }}" as="n" hint-placeholder-count="4">
|
||
<div
|
||
onClick="{{ n.go }}"
|
||
style="display:flex;flex-direction:column;align-items:center;gap:5px;padding:6px 10px;border-radius:12px;cursor:pointer;color:{{ n.color }}"
|
||
>
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
width="17"
|
||
height="17"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.6"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<path d="{{ n.icon }}"></path>
|
||
</svg>
|
||
<span style="font-size: 10px; font-weight: 500">{{ n.label }}</span>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</x-dc>
|
||
<script
|
||
type="text/x-dc"
|
||
data-dc-script
|
||
data-props='{"demoState":{"editor":"enum","options":["normal","loading","error","empty"],"default":"normal","tsType":"string","section":"Estados"}}'
|
||
>
|
||
const img = (seed, n) => 'https://picsum.photos/seed/' + seed + '/' + (n || 400) + '/' + (n || 400);
|
||
const grad = h => 'linear-gradient(150deg,oklch(0.40 0.09 ' + h + '),oklch(0.18 0.04 ' + ((h + 50) % 360) + '))';
|
||
|
||
const TR = (id, title, artist, album, dur, h, seed, plays) => ({ id, title, artist, album, dur, h, seed, plays });
|
||
|
||
const TRACKS = [
|
||
TR(1, 'Yankee', 'Quevedo', 'DONDE QUIERO ESTAR', '3:11', 42, 'yankee', '96 M'),
|
||
TR(2, 'Nights', 'Frank Ocean', 'Blonde', '5:07', 225, 'blonde', '2,4 M'),
|
||
TR(3, 'Redbone', 'Childish Gambino', 'Awaken, My Love!', '5:27', 320, 'awaken', '3,1 M'),
|
||
TR(4, 'Kyoto', 'Phoebe Bridgers', 'Punisher', '3:04', 140, 'punisher', '742 K'),
|
||
TR(5, 'Glue', 'Bicep', 'Bicep', '5:40', 265, 'bicep', '980 K'),
|
||
TR(6, 'Alright', 'Kendrick Lamar', 'To Pimp a Butterfly', '3:39', 15, 'tpab', '4,2 M'),
|
||
TR(7, 'Time Alone With You', 'Jacob Collier', 'Djesse Vol. 3', '4:46', 180, 'djesse', '412 K'),
|
||
TR(8, 'Weird Fishes / Arpeggi', 'Radiohead', 'In Rainbows', '5:18', 95, 'rainbows', '1,3 M'),
|
||
TR(9, 'Instant Crush', 'Daft Punk', 'Random Access Memories', '5:37', 290, 'ram', '2,9 M'),
|
||
TR(10, 'Supercut', 'Lorde', 'Melodrama', '4:38', 55, 'melodrama', '1,1 M'),
|
||
TR(11, 'Sun Bleached Flies', 'Big Thief', null, '5:00', 200, 'bigthief', '298 K'),
|
||
TR(12, 'August 10', 'Khruangbin', 'Mordechai', '3:58', 345, 'mordechai', '536 K')
|
||
];
|
||
|
||
const ALBUMS = [
|
||
['Currents', 'Tame Impala · 2015', 288, 'currents'],
|
||
['Blonde', 'Frank Ocean · 2016', 32, 'blonde'],
|
||
['Punisher', 'Phoebe Bridgers · 2020', 205, 'punisher'],
|
||
['In Rainbows', 'Radiohead · 2007', 18, 'rainbows'],
|
||
['Melodrama', 'Lorde · 2017', 340, 'melodrama'],
|
||
['Isles', 'Bicep · 2021', 232, 'isles'],
|
||
['Mordechai', 'Khruangbin · 2020', 168, 'mordechai'],
|
||
['Random Access Memories', 'Daft Punk · 2013', 260, 'ram']
|
||
];
|
||
|
||
const MIXES = [
|
||
['Mix Quevedo', 'Bad Gyal, Saiko, Mora', 42, 'yankee'],
|
||
['Mix Frank Ocean', 'Blood Orange, Steve Lacy', 32, 'blonde'],
|
||
['Indie de tarde', 'Big Thief, Alvvays, Adrianne Lenker', 142, 'punisher'],
|
||
['Mix electrónico', 'Bicep, Four Tet, Jamie xx', 232, 'isles'],
|
||
['Mix Khruangbin', 'Men I Trust, Mild High Club', 168, 'mordechai'],
|
||
['Clásicos que repites', 'Radiohead, Daft Punk, Lorde', 18, 'rainbows'],
|
||
['Concentración', 'Nala Sinephro, Floating Points', 262, 'd-space'],
|
||
['Soul de medianoche', 'Sault, Michael Kiwanuka', 58, 'awaken']
|
||
];
|
||
|
||
const ARTISTS = [
|
||
['Tame Impala', '18,2 M oyentes', 288, 'a-tame'],
|
||
['Frank Ocean', '22,6 M oyentes', 32, 'a-frank'],
|
||
['Phoebe Bridgers', '9,4 M oyentes', 205, 'a-phoebe'],
|
||
['Bicep', '4,1 M oyentes', 232, 'a-bicep'],
|
||
['Khruangbin', '6,8 M oyentes', 168, 'a-khruangbin'],
|
||
['Little Dragon', '2,3 M oyentes', 120, 'a-littledragon'],
|
||
['Jamie xx', '5,5 M oyentes', 60, 'a-jamiexx']
|
||
];
|
||
|
||
const PLAYLISTS = [
|
||
['Favorites', 214, 'Frank Ocean, Tame Impala, Lorde', 32, 'pl-fav'],
|
||
['Workout', 48, 'Kendrick Lamar, Bicep, Justice', 96, 'pl-workout'],
|
||
['Chill', 96, 'Khruangbin, Men I Trust, Homeshake', 168, 'pl-chill'],
|
||
['Late Night', 72, 'Burial, Bicep, Four Tet', 250, 'pl-latenight'],
|
||
['Driving', 63, 'The War on Drugs, Kavinsky', 42, 'pl-driving'],
|
||
['My Playlist #1', 31, 'Radiohead, Big Thief, Alvvays', 200, 'pl-mine'],
|
||
['Discover', 40, 'Sudan Archives, Yaeji, Nala Sinephro', 300, 'pl-discover']
|
||
];
|
||
|
||
const DISCOVER = [
|
||
['Natural Wonder Beauty Concept', 'DJ Python & Ana Roxanne', 190, 'd-nwbc'],
|
||
['Sundial', 'Noname · 2023', 72, 'd-sundial'],
|
||
['Space 1.8', 'Nala Sinephro', 262, 'd-space'],
|
||
['Natural Brown Prom Queen', 'Sudan Archives', 20, 'd-sudan'],
|
||
['Promises', 'Floating Points & Pharoah Sanders', 130, 'd-promises'],
|
||
['Cavalcade', 'Black Country, New Road', 340, 'd-cavalcade']
|
||
];
|
||
|
||
const GENRES = [
|
||
['Electrónica de madrugada', '412 playlists', 250, 'g-elec'],
|
||
['Jazz contemporáneo', '98 playlists', 44, 'g-jazz'],
|
||
['Indie latino', '1,1 K pistas', 18, 'g-latino'],
|
||
['Shoegaze y dream pop', '330 pistas', 300, 'g-shoegaze'],
|
||
['Instrumental para concentrarse', '86 mixes', 172, 'g-focus'],
|
||
['Soul y neo-soul', '520 pistas', 58, 'g-soul'],
|
||
['Piano moderno', '64 playlists', 214, 'g-piano'],
|
||
['Bass y club', '740 pistas', 280, 'g-club']
|
||
];
|
||
|
||
class Component extends DCLogic {
|
||
state = {
|
||
view: 'home', playing: true, idx: 0, pos: 78, vol: 0.72,
|
||
queueOpen: true, hover: null, query: '', liked: { 2: true, 4: true },
|
||
shuffle: false, repeat: false, w: 1400, listCtx: null, loadingOnce: true,
|
||
tone: null
|
||
};
|
||
|
||
sampleCover(seed) {
|
||
const im = new Image();
|
||
im.crossOrigin = 'anonymous';
|
||
im.onload = () => {
|
||
try {
|
||
const c = document.createElement('canvas');
|
||
c.width = 12; c.height = 12;
|
||
const x = c.getContext('2d');
|
||
x.drawImage(im, 0, 0, 12, 12);
|
||
const d = x.getImageData(0, 0, 12, 12).data;
|
||
let r = 0, g = 0, b = 0, n = 0;
|
||
for (let i = 0; i < d.length; i += 4) { r += d[i]; g += d[i + 1]; b += d[i + 2]; n++; }
|
||
this.setState({ tone: [Math.round(r / n), Math.round(g / n), Math.round(b / n)], toneSeed: seed });
|
||
} catch (e) { this.setState({ tone: null }); }
|
||
};
|
||
im.onerror = () => this.setState({ tone: null });
|
||
im.src = img(seed, 48) + '?cors=1';
|
||
}
|
||
|
||
componentDidMount() {
|
||
this.onResize = () => this.setState({ w: window.innerWidth });
|
||
this.onResize();
|
||
window.addEventListener('resize', this.onResize);
|
||
this.tick = setInterval(() => {
|
||
if (!this.state.playing) return;
|
||
const len = this.durSec(TRACKS[this.state.idx].dur);
|
||
this.setState(s => ({ pos: s.pos >= len ? 0 : s.pos + 1 }));
|
||
}, 1000);
|
||
this.boot = setTimeout(() => this.setState({ loadingOnce: false }), 900);
|
||
}
|
||
componentWillUnmount() {
|
||
window.removeEventListener('resize', this.onResize);
|
||
clearInterval(this.tick); clearTimeout(this.boot);
|
||
}
|
||
|
||
greeting() {
|
||
const name = 'Ariadna';
|
||
const d = new Date(), hh = d.getHours(), wd = d.getDay();
|
||
const days = ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'];
|
||
const day = days[wd];
|
||
const clock = d.getHours() + ':' + (d.getMinutes() < 10 ? '0' : '') + d.getMinutes();
|
||
let line;
|
||
if (hh < 5) line = '¿Sesión de madrugada, ' + name + '?';
|
||
else if (hh < 13) line = 'Buenos días, ' + name;
|
||
else if (hh < 20) line = wd === 5 ? 'Feliz viernes, ' + name : 'Buenas tardes, ' + name;
|
||
else line = 'Buenas noches, ' + name;
|
||
if (wd === 6 || wd === 0) line = hh < 5 ? '¿Madrugada de finde, ' + name + '?' : 'Que tengas buen finde, ' + name;
|
||
else if (wd === 3 && hh >= 5 && hh < 20) line = 'Feliz miércoles, ' + name;
|
||
return { line, eyebrow: day.charAt(0).toUpperCase() + day.slice(1) + ' · ' + clock };
|
||
}
|
||
|
||
durSec(d) { const p = String(d).split(':'); return (+p[0]) * 60 + (+p[1] || 0); }
|
||
|
||
mmss(s) { const m = Math.floor(s / 60), r = Math.floor(s % 60); return m + ':' + (r < 10 ? '0' : '') + r; }
|
||
hov(k) { return this.state.hover === k; }
|
||
hoverProps(k) {
|
||
return { enter: () => this.setState({ hover: k }), leave: () => this.setState({ hover: null }) };
|
||
}
|
||
|
||
card(title, sub, h, seed, round) {
|
||
const hv = this.hov('c' + seed);
|
||
return Object.assign({
|
||
title, sub: sub || 'Sin información', img: img(seed), fallback: grad(h),
|
||
cardBg: hv ? 'rgba(255,255,255,.035)' : 'transparent',
|
||
lift: 'none',
|
||
btnOpacity: hv ? 1 : 0, btnLift: hv ? 'translateY(0)' : 'translateY(6px)',
|
||
play: () => this.setState({ playing: true })
|
||
}, this.hoverProps('c' + seed));
|
||
}
|
||
|
||
trackRow(t, i, keyPrefix, withPlays) {
|
||
const cur = TRACKS[this.state.idx];
|
||
const isCur = t.id === cur.id;
|
||
const k = keyPrefix + t.id;
|
||
const hv = this.hov(k);
|
||
return Object.assign({
|
||
n: i + 1, title: t.title, artist: t.artist, album: t.album || '—', dur: t.dur,
|
||
plays: withPlays ? t.plays : '', img: img(t.seed), fallback: grad(t.h),
|
||
isCurrent: isCur, showIndex: !isCur,
|
||
titleColor: isCur ? (this.accTitle || '#bfaef5') : '#cbcacf',
|
||
rowBg: isCur ? (this.accSoft || 'rgba(156,134,240,.07)') : hv ? 'rgba(255,255,255,.032)' : 'transparent',
|
||
likeOpacity: this.state.liked[t.id] || hv ? 1 : 0,
|
||
likeColor: this.state.liked[t.id] ? (this.acc || '#9c86f0') : '#919195',
|
||
likeFill: this.state.liked[t.id] ? (this.acc || '#9c86f0') : 'none',
|
||
like: e => { if (e) e.stopPropagation(); this.setState(st => ({ liked: Object.assign({}, st.liked, { [t.id]: !st.liked[t.id] }) })); },
|
||
play: () => this.setState({ idx: TRACKS.indexOf(t), playing: true, pos: 0 })
|
||
}, this.hoverProps(k));
|
||
}
|
||
|
||
renderVals() {
|
||
const s = this.state;
|
||
const mob = s.w < 760, tab = s.w < 1180;
|
||
const cur = TRACKS[s.idx];
|
||
const demo = this.props.demoState || 'normal';
|
||
const loading = demo === 'loading' || s.loadingOnce;
|
||
const error = demo === 'error' && !loading;
|
||
const emptyForced = demo === 'empty';
|
||
const q = s.query.trim().toLowerCase();
|
||
const searching = q.length > 0;
|
||
const hits = TRACKS.filter(t => (t.title + ' ' + t.artist + ' ' + (t.album || '')).toLowerCase().includes(q));
|
||
const view = s.view;
|
||
|
||
const t = null;
|
||
let hue = cur.h;
|
||
if (t) {
|
||
const r = t[0] / 255, g = t[1] / 255, b = t[2] / 255;
|
||
const mx = Math.max(r, g, b), mn = Math.min(r, g, b), d0 = mx - mn;
|
||
if (d0 > 0.09) {
|
||
let hh;
|
||
if (mx === r) hh = ((g - b) / d0) % 6;
|
||
else if (mx === g) hh = (b - r) / d0 + 2;
|
||
else hh = (r - g) / d0 + 4;
|
||
hue = (hh * 60 + 360) % 360;
|
||
}
|
||
}
|
||
this.acc = 'oklch(0.74 0.145 ' + hue + ')';
|
||
this.accSoft = 'oklch(0.74 0.145 ' + hue + ' / .08)';
|
||
this.accLine = 'oklch(0.74 0.145 ' + hue + ' / .2)';
|
||
this.accTitle = 'oklch(0.87 0.07 ' + hue + ')';
|
||
this.accMuted = 'oklch(0.79 0.035 ' + hue + ')';
|
||
this.accHair = 'oklch(0.74 0.145 ' + hue + ' / .09)';
|
||
const surf = (l, c, a) => 'oklch(' + l + ' ' + c + ' ' + hue + (a === undefined ? '' : ' / ' + a) + ')';
|
||
|
||
const isEmptyView = emptyForced && (view === 'playlists' || view === 'liked');
|
||
const showList = !loading && !error && !searching && !isEmptyView && (view === 'liked' || view === 'recent' || view === 'playlist');
|
||
const showSearchList = !loading && !error && searching && hits.length > 0;
|
||
|
||
const navDef = [
|
||
['home', 'Inicio', 'M3 11l9-7 9 7v8a2 2 0 01-2 2h-4v-6H9v6H5a2 2 0 01-2-2z', ''],
|
||
['explore', 'Descubrir', 'M12 3a9 9 0 100 18 9 9 0 000-18zm3.5 5.5l-2 5-5 2 2-5z', ''],
|
||
['playlists', 'Playlists', 'M4 6h10M4 11h10M4 16h6M18 9v8', '7'],
|
||
['liked', 'Me gusta', 'M12 20s-7-4.6-7-9.4A3.9 3.9 0 0112 8a3.9 3.9 0 017 2.6C19 15.4 12 20 12 20z', '312'],
|
||
['recent', 'Reciente', 'M12 7v5l3 2M12 3a9 9 0 100 18 9 9 0 000-18z', '']
|
||
];
|
||
const nav = navDef.map(([id, label, icon, count]) => ({
|
||
label, icon, count,
|
||
color: view === id ? '#eae8f1' : '#8b8b8f',
|
||
bg: view === id ? 'rgba(255,255,255,.06)' : 'transparent',
|
||
go: () => this.setState({ view: id, query: '' })
|
||
}));
|
||
|
||
const playingPl = 'Late Night';
|
||
const myPlaylists = PLAYLISTS.map(([name, n, artistsLine, h, seed]) => {
|
||
const k = 'pl' + seed, hv = this.hov(k);
|
||
const active = view === 'playlist' && s.listCtx === name;
|
||
return Object.assign({
|
||
name, meta: n + ' canciones', artistsLine, img: img(seed), fallback: grad(h),
|
||
isPlaying: name === playingPl && s.playing,
|
||
bg: active ? 'rgba(255,255,255,.055)' : 'transparent',
|
||
titleColor: active || name === playingPl ? this.accTitle : '#cbcacf',
|
||
cardBg: hv ? 'rgba(255,255,255,.04)' : 'rgba(255,255,255,.02)',
|
||
border: hv ? 'rgba(255,255,255,.08)' : 'rgba(255,255,255,.045)',
|
||
lift: 'none',
|
||
btnOpacity: hv ? 1 : 0,
|
||
open: () => this.setState({ view: 'playlist', listCtx: name, query: '' })
|
||
}, this.hoverProps(k));
|
||
});
|
||
|
||
const continueRow = TRACKS.slice(0, 6).map(t => {
|
||
const k = 'cont' + t.id, hv = this.hov(k);
|
||
const isCur = t.id === cur.id;
|
||
return Object.assign({
|
||
title: t.title, sub: t.album ? t.artist + ' · ' + t.album : t.artist,
|
||
img: img(t.seed), fallback: grad(t.h),
|
||
titleColor: isCur ? this.accMuted : '#cdccd1',
|
||
bg: isCur ? this.accSoft : hv ? 'rgba(255,255,255,.045)' : 'rgba(255,255,255,.022)',
|
||
border: isCur ? this.accHair : hv ? 'rgba(255,255,255,.075)' : 'rgba(255,255,255,.04)',
|
||
btnOpacity: hv ? 1 : 0,
|
||
play: () => this.setState({ idx: TRACKS.indexOf(t), playing: true, pos: 0 })
|
||
}, this.hoverProps(k));
|
||
});
|
||
|
||
const spotTracks = [TRACKS[4], TRACKS[8], TRACKS[11], TRACKS[6], TRACKS[9]];
|
||
|
||
const listName = s.listCtx || 'Favorites';
|
||
const listPl = PLAYLISTS.find(p => p[0] === listName) || PLAYLISTS[0];
|
||
const listSrc = showSearchList ? hits : (view === 'liked' ? TRACKS.slice(0, 9) : view === 'recent' ? TRACKS.slice(2, 11) : TRACKS.slice(0, 8));
|
||
|
||
const emptyCopy = view === 'playlists'
|
||
? ['Aún no has creado ninguna playlist', 'Crea una desde cero o deja que Musify la construya a partir de un artista, un año o un estado de ánimo.', 'Crear playlist']
|
||
: ['Nada con me gusta todavía', 'Pulsa el corazón en cualquier canción y aparecerá aquí, disponible sin conexión.', 'Descubrir música'];
|
||
|
||
const tc = (a, l) => 'oklch(' + (0.34 - l / 380).toFixed(3) + ' ' + (0.11 - l / 1600).toFixed(3) + ' ' + hue + ' / ' + a + ')';
|
||
|
||
return {
|
||
ambient: 'radial-gradient(52% 38% at 14% 0%,' + tc(.26, 30) + ',transparent 54%),radial-gradient(46% 34% at 78% 0%,' + tc(.2, 40) + ',transparent 56%)',
|
||
heroBg: 'linear-gradient(105deg,' + tc(.34, 42) + ' 0%,' + tc(.11, 70) + ' 28%,' + tc(.03, 88) + ' 50%,rgba(6,6,9,0) 70%)',
|
||
heroEyebrow: this.greeting().eyebrow,
|
||
heroGreeting: this.greeting().line,
|
||
heroBlurb: 'Cinco selecciones nuevas construidas a partir de las 214 canciones que escuchaste esta semana.',
|
||
heroResume: 'Late Night',
|
||
heroStats: [
|
||
{ v: '214', k: 'Canciones esta semana' },
|
||
{ v: '18 h', k: 'Tiempo de escucha' },
|
||
{ v: '9', k: 'Artistas nuevos' }
|
||
],
|
||
sidebarDisplay: tab ? 'none' : 'flex',
|
||
queueW: tab ? '258px' : '304px',
|
||
queueOpen: s.queueOpen && !mob,
|
||
queueSource: playingPl,
|
||
queueBtnColor: s.queueOpen ? '#d6d5da' : '#8b8b8f',
|
||
queueBtnBg: s.queueOpen ? 'rgba(255,255,255,.06)' : 'rgba(255,255,255,.028)',
|
||
extrasDisplay: mob ? 'none' : 'flex',
|
||
tabsDisplay: tab ? 'flex' : 'none',
|
||
playerInfoW: mob ? 'auto' : '256px',
|
||
bottomPad: tab ? '206px' : '126px',
|
||
nav, myPlaylists, continueRow,
|
||
albums: MIXES.map(([t, sub, h, seed]) => this.card(t, sub, h, seed)),
|
||
artists: ARTISTS.map(([t, sub, h, seed]) => this.card(t, sub, h, seed, true)),
|
||
discover: DISCOVER.map(([t, sub, h, seed]) => this.card(t, sub, h, seed)),
|
||
popular: TRACKS.slice(0, 8).map((t, i) => this.trackRow(t, i, 'pop', true)),
|
||
genres: GENRES.map(([name, meta, h, seed]) => ({
|
||
name, meta, img: img(seed, 260),
|
||
bg: 'linear-gradient(140deg,oklch(0.24 0.07 ' + h + '),rgba(9,9,12,.95))'
|
||
})),
|
||
spotlightBg: 'linear-gradient(100deg,' + tc(.28, 46) + ' 0%,' + tc(.1, 74) + ' 30%,' + tc(.03, 88) + ' 52%,rgba(6,6,9,0) 72%)',
|
||
spotlight: {
|
||
name: 'Late Night', meta: '72 canciones · 4 h 51 min · privada',
|
||
img: img('pl-latenight'),
|
||
blurb: 'Tu playlist más escuchada de madrugada. Añadiste 6 canciones esta semana, casi todas después de la una.',
|
||
play: () => this.setState({ view: 'playlist', listCtx: 'Late Night', playing: true })
|
||
},
|
||
spotlightTracks: spotTracks.map((t, i) => this.trackRow(t, i, 'spot', false)),
|
||
isLoading: loading, isError: error,
|
||
showHome: !loading && !error && !searching && view === 'home',
|
||
showExplore: !loading && !error && !searching && view === 'explore',
|
||
showList: showList || showSearchList,
|
||
showPlaylistIndex: !loading && !error && !searching && view === 'playlists' && !isEmptyView,
|
||
showEmpty: !loading && !error && !searching && isEmptyView,
|
||
showNoResults: !loading && !error && searching && hits.length === 0,
|
||
skeletonRows: [1, 2], skeletonCards: [1, 2, 3, 4, 5, 6],
|
||
emptyTitle: emptyCopy[0], emptyBody: emptyCopy[1], emptyCta: emptyCopy[2],
|
||
suggestions: ['Frank Ocean', 'Currents', 'Late Night', 'Bicep'].map(label => ({ label, go: () => this.setState({ query: label }) })),
|
||
listTracks: listSrc.map((t, i) => this.trackRow(t, i, 'list', false)),
|
||
listTitle: showSearchList ? 'Resultados para “' + s.query + '”' : view === 'liked' ? 'Canciones que te gustan' : view === 'recent' ? 'Escuchado recientemente' : listName,
|
||
listKind: showSearchList ? 'Búsqueda' : view === 'recent' ? 'Historial' : view === 'liked' ? 'Colección' : 'Playlist',
|
||
listMeta: showSearchList
|
||
? hits.length + ' coincidencias en canciones, álbumes y artistas'
|
||
: view === 'liked' ? '312 canciones · 21 h 40 min'
|
||
: view === 'recent' ? 'Últimos 7 días · 148 reproducciones'
|
||
: listPl[1] + ' canciones · ' + listPl[2],
|
||
listImg: showSearchList || view === 'recent' ? img(cur.seed) : view === 'liked' ? img('pl-fav') : img(listPl[4]),
|
||
listFallback: grad(view === 'liked' ? 32 : listPl[3]),
|
||
query: s.query,
|
||
onQuery: e => this.setState({ query: e.target.value }),
|
||
curTitle: cur.title,
|
||
curArtist: cur.album ? cur.artist + ' · ' + cur.album : cur.artist,
|
||
curImg: img(cur.seed), curFallback: grad(cur.h),
|
||
pageBg: surf(0.068, 0.008),
|
||
sidebarBg: 'linear-gradient(180deg,' + surf(0.095, 0.012) + ',' + surf(0.068, 0.007) + ')',
|
||
panelBg: surf(0.09, 0.01, .28),
|
||
barBg: surf(0.105, 0.012, .9),
|
||
hairline: surf(0.62, 0.03, .08),
|
||
accent: this.acc, accentSoft: this.accSoft, accentLine: this.accLine, accentTitle: this.accTitle,
|
||
accentGrad: 'linear-gradient(90deg,' + this.accTitle + ',' + this.acc + ')',
|
||
logoGrad: 'linear-gradient(140deg,oklch(0.78 0.15 ' + hue + '),oklch(0.62 0.13 ' + ((hue + 40) % 360) + ') 92%)',
|
||
logoGlow: '0 0 20px oklch(0.74 0.145 ' + hue + ' / .3)',
|
||
accentBtnBg: 'oklch(0.74 0.145 ' + hue + ' / .15)',
|
||
accentBtnBgHover: 'oklch(0.74 0.145 ' + hue + ' / .24)',
|
||
curLikeColor: s.liked[cur.id] ? this.acc : '#818185',
|
||
curLikeFill: s.liked[cur.id] ? this.acc : 'none',
|
||
playing: s.playing, paused: !s.playing,
|
||
elapsed: this.mmss(s.pos), total: cur.dur,
|
||
progressPct: Math.min(100, (s.pos / this.durSec(cur.dur)) * 100) + '%',
|
||
volPct: (s.vol * 100) + '%',
|
||
shuffleColor: s.shuffle ? this.acc : '#747478',
|
||
repeatColor: s.repeat ? this.acc : '#747478',
|
||
queue: TRACKS.slice(1, 9).map(t => ({
|
||
title: t.title, artist: t.artist, dur: t.dur, img: img(t.seed), fallback: grad(t.h),
|
||
play: () => this.setState({ idx: TRACKS.indexOf(t), playing: true, pos: 0 })
|
||
})),
|
||
toggle: () => this.setState(st => ({ playing: !st.playing })),
|
||
next: () => this.setState(st => ({ idx: (st.idx + 1) % TRACKS.length, pos: 0, playing: true })),
|
||
prev: () => this.setState(st => ({ idx: (st.idx + TRACKS.length - 1) % TRACKS.length, pos: 0, playing: true })),
|
||
toggleShuffle: () => this.setState(st => ({ shuffle: !st.shuffle })),
|
||
toggleRepeat: () => this.setState(st => ({ repeat: !st.repeat })),
|
||
toggleQueue: () => this.setState(st => ({ queueOpen: !st.queueOpen })),
|
||
likeCurrent: () => this.setState(st => ({ liked: Object.assign({}, st.liked, { [cur.id]: !st.liked[cur.id] }) })),
|
||
seek: e => { const r = e.currentTarget.getBoundingClientRect(); this.setState({ pos: Math.round(((e.clientX - r.left) / r.width) * this.durSec(cur.dur)) }); },
|
||
setVol: e => { const r = e.currentTarget.getBoundingClientRect(); this.setState({ vol: Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)) }); },
|
||
playHero: () => this.setState({ playing: true }),
|
||
retry: () => this.setState({ loadingOnce: true }, () => setTimeout(() => this.setState({ loadingOnce: false }), 800)),
|
||
back: () => this.setState({ view: 'home', query: '' }),
|
||
goHome: () => this.setState({ view: 'home', query: '' }),
|
||
goExplore: () => this.setState({ view: 'explore', query: '' }),
|
||
goRecent: () => this.setState({ view: 'recent', query: '' }),
|
||
goPlaylists: () => this.setState({ view: 'playlists', query: '' })
|
||
};
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|