- Explore: replace per-kind colored badges/tints in search results with the app's neutral accent, add an always-visible Usuarios chip, and keep the top-result type label as plain text - Move SearchBox into TopBar/MobileHeader as an inline debounced search form, dropping the standalone SearchBox component - Sync design handoff artboards with the current search UI
1128 lines
82 KiB
HTML
1128 lines
82 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,.07);border-radius:8px}
|
||
::-webkit-scrollbar-track{background:transparent}
|
||
@keyframes eq{0%,100%{transform:scaleY(.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,.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:-.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,.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:.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,.035);border:1px solid rgba(255,255,255,.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,.035);border:1px solid rgba(255,255,255,.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,.035);border:1px solid rgba(255,255,255,.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,.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:.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,.025),rgba(255,255,255,.055),rgba(255,255,255,.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,.025),rgba(255,255,255,.055),rgba(255,255,255,.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,.025),rgba(255,255,255,.055),rgba(255,255,255,.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,.05);margin:14px 0 8px"></div>
|
||
<div style="width:42%;height:9px;border-radius:5px;background:rgba(255,255,255,.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 .3s ease">
|
||
<div style="width:52px;height:52px;margin:0 auto 22px;border-radius:15px;background:rgba(226,130,130,.08);border:1px solid rgba(226,130,130,.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:-.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,.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:-.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,.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,.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:-.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:-.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,.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:.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:-.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,.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,.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:-.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,.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:-.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:-.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:-.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:-.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:.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 .3s ease">
|
||
<h1 style="font-family:'Sora',sans-serif;font-size:28px;font-weight:600;letter-spacing:-.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:-.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,.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:-.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 .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:.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:-.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,.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,.05);font-size:10.5px;letter-spacing:.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 .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:-.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,.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,.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 .3s ease">
|
||
<div style="width:70px;height:70px;margin:0 auto 24px;border-radius:19px;background:linear-gradient(150deg,rgba(156,134,240,.10),rgba(79,191,174,.05));border:1px solid rgba(255,255,255,.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:-.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,.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>
|
||
|
||
<!-- SEARCH RESULTS -->
|
||
<sc-if value="{{ showSearch }}" hint-placeholder-val="{{ false }}">
|
||
<div data-screen-label="Search results" style="animation:rise .3s ease;padding-bottom:10px">
|
||
<div style="margin:12px 0 18px">
|
||
<div style="font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;color:#8a8a8e;font-weight:600;margin-bottom:11px">Resultados de búsqueda</div>
|
||
<h1 style="font-family:'Sora',sans-serif;font-size:26px;font-weight:600;letter-spacing:-.03em;margin:0 0 7px;color:#d9d8dc;text-wrap:pretty">{{ searchHeading }}</h1>
|
||
<div style="font-size:12.5px;color:#8a8a8e">{{ searchMeta }}</div>
|
||
</div>
|
||
|
||
<div style="display:flex;gap:7px;flex-wrap:wrap;margin-bottom:26px">
|
||
<sc-for list="{{ searchChips }}" as="c" hint-placeholder-count="5">
|
||
<div onClick="{{ c.pick }}" style="display:flex;align-items:center;gap:7px;padding:7px 14px;border-radius:999px;font-size:12px;font-weight:500;cursor:pointer;background:{{ c.bg }};color:{{ c.fg }};border:1px solid {{ c.bd }};transition:background .16s ease,color .16s ease">
|
||
<span>{{ c.label }}</span>
|
||
<span style="font-size:10.5px;font-variant-numeric:tabular-nums;color:{{ c.numFg }}">{{ c.count }}</span>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
|
||
<sc-if value="{{ hasTop }}" hint-placeholder-val="{{ true }}">
|
||
<div onMouseEnter="{{ top.enter }}" onMouseLeave="{{ top.leave }}" onClick="{{ top.open }}" style="display:flex;align-items:center;gap:20px;padding:18px;border-radius:18px;cursor:pointer;margin-bottom:34px;border:1px solid rgba(255,255,255,.06);background:{{ top.wrapBg }};transition:background .2s ease">
|
||
<div style="width:88px;height:88px;flex:0 0 auto;overflow:hidden;border-radius:{{ top.radius }};background:{{ top.fallback }};box-shadow:0 14px 34px rgba(0,0,0,.5)">
|
||
<div style="width:100%;height:100%;background-image:url({{ top.img }});background-size:cover;background-position:center"></div>
|
||
</div>
|
||
<div style="min-width:0;flex:1">
|
||
<div style="font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;font-weight:600;color:{{ top.tint }};margin-bottom:8px">Mejor resultado · {{ top.kind }}</div>
|
||
<div style="font-family:'Sora',sans-serif;font-size:22px;font-weight:600;letter-spacing:-.03em;color:#dcdbe0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ top.title }}</div>
|
||
<div style="font-size:12.5px;color:#84848a;margin-top:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ top.sub }}</div>
|
||
</div>
|
||
<div style="flex:0 0 auto;width:46px;height:46px;border-radius:50%;display:grid;place-items:center;background:#c6c4cb;color:#0a0a0d;opacity:{{ top.btnOpacity }};transition:opacity .18s ease">
|
||
<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M8 5v14l11-7z"></path></svg>
|
||
</div>
|
||
</div>
|
||
</sc-if>
|
||
|
||
<sc-for list="{{ searchGroups }}" as="g" hint-placeholder-count="3">
|
||
<div style="margin-bottom:30px">
|
||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:6px">
|
||
<div style="font-family:'Sora',sans-serif;font-size:14.5px;font-weight:600;letter-spacing:-.015em;color:#cfccd9">{{ g.label }}</div>
|
||
<div style="height:1px;flex:1;background:rgba(255,255,255,.06)"></div>
|
||
<div style="font-size:11px;color:#6e6e74;font-variant-numeric:tabular-nums">{{ g.count }}</div>
|
||
</div>
|
||
<sc-for list="{{ g.items }}" as="r" hint-placeholder-count="4">
|
||
<div onMouseEnter="{{ r.enter }}" onMouseLeave="{{ r.leave }}" onClick="{{ r.open }}" style="display:grid;grid-template-columns:52px 1fr auto;gap:15px;align-items:center;padding:9px 12px;border-radius:12px;cursor:pointer;background:{{ r.rowBg }};border-bottom:1px solid rgba(255,255,255,.035);transition:background .15s ease">
|
||
<div style="position:relative;width:52px;height:52px;overflow:hidden;border-radius:{{ r.radius }};background:{{ r.fallback }}">
|
||
<div style="width:100%;height:100%;background-image:url({{ r.img }});background-size:cover;background-position:center;opacity:.92"></div>
|
||
<div style="position:absolute;inset:0;display:grid;place-items:center;background:rgba(6,6,9,.55);color:#e2e1e6;opacity:{{ r.btnOpacity }};transition:opacity .15s ease">
|
||
<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M8 5v14l11-7z"></path></svg>
|
||
</div>
|
||
</div>
|
||
<div style="min-width:0">
|
||
<div style="display:flex;align-items:center;gap:9px;min-width:0">
|
||
<div style="font-size:13.5px;font-weight:500;color:{{ r.titleColor }};white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ r.title }}</div>
|
||
<div style="flex:0 0 auto;font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;font-weight:600;padding:3px 7px;border-radius:5px;color:{{ r.tint }};background:{{ r.tintSoft }}">{{ r.badge }}</div>
|
||
</div>
|
||
<div style="font-size:11.5px;color:#7f7f83;margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ r.sub }}</div>
|
||
</div>
|
||
<div style="font-size:11.5px;color:#76767c;font-variant-numeric:tabular-nums;text-align:right">{{ r.meta }}</div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</sc-for>
|
||
</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 .3s ease">
|
||
<div style="font-family:'Sora',sans-serif;font-size:20px;font-weight:600;letter-spacing:-.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,.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:-.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:.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:.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 .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:-.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 .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,.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,.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, sfilter: 'Todo'
|
||
};
|
||
|
||
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 albumHits = ALBUMS.filter(a => (a[0] + ' ' + a[1]).toLowerCase().includes(q));
|
||
const artistHits = ARTISTS.filter(a => a[0].toLowerCase().includes(q));
|
||
const plHits = PLAYLISTS.filter(p => (p[0] + ' ' + p[2]).toLowerCase().includes(q));
|
||
const totalHits = searching ? hits.length + albumHits.length + artistHits.length + plHits.length : 0;
|
||
const showSearch = !loading && !error && searching && totalHits > 0;
|
||
|
||
const tint = o => 'oklch(0.80 0.10 ' + ((hue + o + 360) % 360) + ')';
|
||
const tintSoft = o => 'oklch(0.80 0.10 ' + ((hue + o + 360) % 360) + ' / .11)';
|
||
const sRow = (kind, badge, off, radius, key, title, sub, meta, seed, h, open) => {
|
||
const hv = this.hov(key);
|
||
return Object.assign({
|
||
title, sub, meta, badge, kind, radius,
|
||
img: img(seed), fallback: grad(h),
|
||
tint: tint(off), tintSoft: tintSoft(off),
|
||
titleColor: hv ? '#e0dfe4' : '#cbcacf',
|
||
rowBg: hv ? 'rgba(255,255,255,.035)' : 'transparent',
|
||
btnOpacity: hv ? 1 : 0,
|
||
open
|
||
}, this.hoverProps(key));
|
||
};
|
||
const songRows = hits.map(t => sRow('Canción', 'Canción', 0, '9px', 'sr-t' + t.id, t.title, t.artist + (t.album ? ' · ' + t.album : ''), t.dur, t.seed, t.h,
|
||
() => this.setState({ idx: TRACKS.indexOf(t), playing: true, pos: 0 })));
|
||
const albumRows = albumHits.map((a, i) => sRow('Álbum', 'Álbum', 46, '5px', 'sr-a' + i, a[0], a[1], 'Álbum', a[3], a[2],
|
||
() => this.setState({ playing: true })));
|
||
const artistRows = artistHits.map((a, i) => sRow('Artista', 'Artista', -52, '50%', 'sr-r' + i, a[0], a[1], 'Artista', a[3], a[2],
|
||
() => this.setState({ playing: true })));
|
||
const plRows = plHits.map((p, i) => sRow('Playlist', 'Playlist', 92, '13px', 'sr-p' + i, p[0], p[2], p[1] + ' canciones', p[4], p[3],
|
||
() => this.setState({ view: 'playlist', listCtx: p[0], query: '' })));
|
||
|
||
const allGroups = [
|
||
['Canciones', songRows], ['Álbumes', albumRows],
|
||
['Artistas', artistRows], ['Playlists', plRows]
|
||
];
|
||
const sf = s.sfilter;
|
||
const chipMap = { 'Canciones': songRows, 'Álbumes': albumRows, 'Artistas': artistRows, 'Playlists': plRows };
|
||
const searchGroups = allGroups
|
||
.filter(g => g[1].length && (sf === 'Todo' || sf === g[0]))
|
||
.map(g => ({ label: g[0], count: g[1].length + (g[1].length === 1 ? ' resultado' : ' resultados'), items: sf === 'Todo' ? g[1].slice(0, 4) : g[1] }));
|
||
|
||
const topSrc = [['Artista', artistRows], ['Canción', songRows], ['Álbum', albumRows], ['Playlist', plRows]];
|
||
let topItem = null;
|
||
for (const [, rows] of topSrc) { const m = rows.find(r => r.title.toLowerCase().startsWith(q)); if (m) { topItem = m; break; } }
|
||
if (!topItem) { const g = topSrc.find(x => x[1].length); topItem = g ? g[1][0] : null; }
|
||
const topVal = topItem ? Object.assign({}, topItem, {
|
||
wrapBg: this.hov(topItem.kind === 'x' ? '' : 'top') ? 'rgba(255,255,255,.05)' : 'rgba(255,255,255,.025)',
|
||
btnOpacity: this.hov('top') ? 1 : 0.55
|
||
}, this.hoverProps('top')) : {};
|
||
|
||
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 = (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,
|
||
showSearch,
|
||
searchHeading: 'Resultados para «' + s.query.trim() + '»',
|
||
searchMeta: totalHits + (totalHits === 1 ? ' coincidencia' : ' coincidencias') + ' en canciones, álbumes, artistas y playlists',
|
||
searchChips: ['Todo', 'Canciones', 'Álbumes', 'Artistas', 'Playlists'].map(label => {
|
||
const on = sf === label;
|
||
return {
|
||
label,
|
||
count: label === 'Todo' ? totalHits : (chipMap[label] || []).length,
|
||
bg: on ? '#cfccd9' : 'rgba(255,255,255,.03)',
|
||
fg: on ? '#0a0a0d' : '#9b9b9f',
|
||
numFg: on ? 'rgba(10,10,13,.55)' : '#65656a',
|
||
bd: on ? 'transparent' : 'rgba(255,255,255,.07)',
|
||
pick: () => this.setState({ sfilter: label })
|
||
};
|
||
}),
|
||
searchGroups,
|
||
hasTop: showSearch && !!topItem && sf === 'Todo',
|
||
top: topVal,
|
||
showPlaylistIndex: !loading && !error && !searching && view === 'playlists' && !isEmptyView,
|
||
showEmpty: !loading && !error && !searching && isEmptyView,
|
||
showNoResults: !loading && !error && searching && totalHits === 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: view === 'liked' ? 'Canciones que te gustan' : view === 'recent' ? 'Escuchado recientemente' : listName,
|
||
listKind: view === 'recent' ? 'Historial' : view === 'liked' ? 'Colección' : 'Playlist',
|
||
listMeta: view === 'liked' ? '312 canciones · 21 h 40 min'
|
||
: view === 'recent' ? 'Últimos 7 días · 148 reproducciones'
|
||
: listPl[1] + ' canciones · ' + listPl[2],
|
||
listImg: 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>
|