Initial commit: party-mix-app with prefetch cache, audio preload optimizations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
771
index.html
Normal file
771
index.html
Normal file
@@ -0,0 +1,771 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
<title>Party Mix</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0a0a0f;--surface:#12121a;--surface2:#1a1a26;
|
||||
--border:rgba(255,255,255,0.07);--accent:#c8ff00;
|
||||
--text:#f0f0f0;--muted:#555;--radius:16px;
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
|
||||
html{scroll-behavior:smooth}
|
||||
body{
|
||||
font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);
|
||||
min-height:100vh;padding:1.25rem 1rem 5rem;
|
||||
background-image:
|
||||
radial-gradient(ellipse 80% 50% at 20% -10%,rgba(200,255,0,0.05) 0%,transparent 60%),
|
||||
radial-gradient(ellipse 60% 40% at 80% 110%,rgba(255,60,172,0.05) 0%,transparent 60%);
|
||||
}
|
||||
.app{max-width:660px;margin:0 auto}
|
||||
@keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
|
||||
/* ── HEADER ── */
|
||||
.header{display:flex;align-items:center;gap:12px;margin-bottom:1.25rem;padding-bottom:1.25rem;border-bottom:1px solid var(--border)}
|
||||
.logo{width:40px;height:40px;border-radius:11px;background:var(--accent);display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.logo svg{width:20px;height:20px}
|
||||
h1{font-family:'Syne',sans-serif;font-size:20px;font-weight:800;letter-spacing:-.5px}
|
||||
.header-sub{font-size:12px;color:var(--muted);margin-top:1px}
|
||||
|
||||
/* ── TABS ── */
|
||||
.tabs{display:flex;gap:0;margin-bottom:1rem;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:3px}
|
||||
.tab-btn{flex:1;padding:8px;font-size:13px;font-family:'Syne',sans-serif;font-weight:700;letter-spacing:.2px;border:none;border-radius:9px;cursor:pointer;background:transparent;color:var(--muted);transition:all .2s;display:flex;align-items:center;justify-content:center;gap:6px}
|
||||
.tab-btn.active{background:var(--surface2);color:var(--text)}
|
||||
.tab-btn .badge{background:var(--accent);color:#0a0a0f;font-size:10px;font-weight:800;padding:1px 5px;border-radius:4px;min-width:18px;text-align:center}
|
||||
.tab-content{display:none}
|
||||
.tab-content.active{display:block;animation:fadeUp .25s ease both}
|
||||
|
||||
/* ── PLAYER ── */
|
||||
.player-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:1rem;display:none;overflow:hidden;animation:fadeUp .3s ease both}
|
||||
.player-hero{display:flex;gap:0;position:relative;overflow:hidden;border-bottom:1px solid var(--border);min-height:110px}
|
||||
.cover-wrap{position:relative;flex-shrink:0;width:110px;height:110px;overflow:hidden;background:var(--surface2)}
|
||||
.cover-img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .5s ease}
|
||||
.cover-img.playing{animation:coverPulse 2s ease-in-out infinite alternate}
|
||||
@keyframes coverPulse{from{transform:scale(1)}to{transform:scale(1.06)}}
|
||||
.cover-canvas{position:absolute;inset:0;width:100%;height:100%;opacity:0;transition:opacity .4s;pointer-events:none}
|
||||
.cover-canvas.active{opacity:1}
|
||||
.cover-glow{position:absolute;inset:-20px;border-radius:50%;background:radial-gradient(circle,var(--glow-color,rgba(200,255,0,.25)) 0%,transparent 70%);opacity:0;transition:opacity .5s;filter:blur(12px);pointer-events:none;z-index:0}
|
||||
.cover-wrap.playing .cover-glow{opacity:1;animation:glowPulse 1.5s ease-in-out infinite alternate}
|
||||
@keyframes glowPulse{from{opacity:.4}to{opacity:.9}}
|
||||
.player-info{flex:1;padding:.9rem 1rem;display:flex;flex-direction:column;justify-content:center;gap:4px;background:linear-gradient(135deg,var(--surface2) 0%,var(--surface) 100%);min-width:0}
|
||||
.now-eyebrow{font-size:10px;font-weight:500;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);display:flex;align-items:center;gap:5px}
|
||||
.eq-bars{display:flex;align-items:flex-end;gap:2px;height:10px}
|
||||
.eq-bar{width:2.5px;border-radius:2px;background:var(--accent);animation:eqAnim .7s ease-in-out infinite alternate;height:100%}
|
||||
.eq-bar:nth-child(2){animation-delay:.15s;height:60%}
|
||||
.eq-bar:nth-child(3){animation-delay:.3s;height:80%}
|
||||
.eq-bar:nth-child(4){animation-delay:.1s;height:50%}
|
||||
@keyframes eqAnim{from{transform:scaleY(.3)}to{transform:scaleY(1)}}
|
||||
.now-title{font-family:'Syne',sans-serif;font-size:15px;font-weight:800;letter-spacing:-.3px;line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.owner-badge{display:inline-flex;align-items:center;gap:5px;font-size:11px;padding:3px 9px;border-radius:7px;align-self:flex-start}
|
||||
.search-status{padding:.75rem 1rem;display:none;align-items:center;gap:10px;font-size:13px;color:var(--muted);border-bottom:1px solid var(--border)}
|
||||
.spinner{width:14px;height:14px;border-radius:50%;border:2px solid var(--surface2);border-top-color:var(--accent);animation:spin .6s linear infinite;flex-shrink:0}
|
||||
.audio-wrap{padding:.75rem 1rem;border-bottom:1px solid var(--border);display:none}
|
||||
.audio-meta{font-size:12px;color:var(--muted);margin-bottom:7px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
audio{width:100%;height:34px;border-radius:8px;outline:none;filter:invert(1) hue-rotate(180deg) saturate(0.5)}
|
||||
.results-wrap{display:none}
|
||||
.results-header{padding:7px 1rem;font-size:10px;font-weight:600;letter-spacing:1.2px;text-transform:uppercase;color:var(--muted);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;cursor:pointer;user-select:none;transition:background .12s}
|
||||
.results-header:hover{background:var(--surface2)}
|
||||
.results-chevron{font-size:10px;transition:transform .2s}
|
||||
.results-chevron.open{transform:rotate(180deg)}
|
||||
.results-body{max-height:0;overflow:hidden;transition:max-height .3s ease}
|
||||
.results-body.open{max-height:600px}
|
||||
.result-item{display:flex;align-items:center;gap:9px;padding:9px 1rem;border-bottom:1px solid var(--border);cursor:pointer;transition:background .12s}
|
||||
.result-item:last-child{border-bottom:none}
|
||||
.result-item:hover{background:var(--surface2)}
|
||||
.result-item.active{background:rgba(200,255,0,0.04)}
|
||||
.res-cover{width:32px;height:32px;border-radius:6px;object-fit:cover;flex-shrink:0;background:var(--surface2)}
|
||||
.result-num{font-size:11px;color:var(--muted);width:14px;flex-shrink:0;text-align:right;font-family:'Syne',sans-serif}
|
||||
.result-info{flex:1;min-width:0}
|
||||
.result-name{font-size:13px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500}
|
||||
.result-artist{font-size:11px;color:var(--muted);margin-top:1px}
|
||||
.result-dur{font-size:11px;color:var(--muted);flex-shrink:0;font-family:'Syne',sans-serif}
|
||||
.result-play-btn{width:26px;height:26px;border-radius:50%;border:1px solid var(--border);background:transparent;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .15s}
|
||||
.result-play-btn:hover,.result-play-btn.active{background:var(--accent);border-color:var(--accent)}
|
||||
.result-play-btn svg{fill:var(--muted);transition:fill .15s}
|
||||
.result-play-btn:hover svg,.result-play-btn.active svg{fill:#0a0a0f}
|
||||
.player-controls{display:flex;align-items:center;gap:8px;padding:.65rem 1rem}
|
||||
.ctrl{width:36px;height:36px;border-radius:9px;border:1px solid var(--border);background:transparent;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--muted);flex-shrink:0;transition:all .15s}
|
||||
.ctrl:hover{background:var(--surface2);color:var(--text)}
|
||||
.ctrl-info{flex:1;text-align:center;font-size:12px;color:var(--muted);font-family:'Syne',sans-serif}
|
||||
.btn-next{padding:6px 12px;font-size:12px;font-weight:600;font-family:'Syne',sans-serif;border:1px solid var(--border);border-radius:9px;background:transparent;color:var(--muted);cursor:pointer;transition:all .15s;white-space:nowrap}
|
||||
.btn-next:hover{background:var(--surface2);color:var(--text)}
|
||||
|
||||
/* ── QUEUE ── */
|
||||
.queue-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;margin-bottom:1rem}
|
||||
.queue-head{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;cursor:pointer;user-select:none;transition:background .12s}
|
||||
.queue-head:hover{background:var(--surface2)}
|
||||
.queue-head-left{display:flex;align-items:center;gap:8px}
|
||||
.queue-label{font-family:'Syne',sans-serif;font-size:11px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;color:var(--muted)}
|
||||
.queue-chevron{color:var(--muted);font-size:11px;transition:transform .25s;flex-shrink:0}
|
||||
.queue-chevron.open{transform:rotate(180deg)}
|
||||
.queue-body{max-height:0;overflow:hidden;transition:max-height .35s ease}
|
||||
.queue-body.open{max-height:500px}
|
||||
.queue-list{display:flex;flex-direction:column;max-height:500px;overflow-y:auto}
|
||||
.queue-list::-webkit-scrollbar{width:3px}
|
||||
.queue-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
|
||||
|
||||
/* DRAG & DROP */
|
||||
.q-item{
|
||||
display:flex;align-items:center;gap:8px;
|
||||
padding:8px 1rem;border-bottom:1px solid var(--border);
|
||||
cursor:pointer;transition:background .1s;
|
||||
user-select:none;position:relative;
|
||||
}
|
||||
.q-item:last-child{border-bottom:none}
|
||||
.q-item:hover{background:var(--surface2)}
|
||||
.q-item.active{background:rgba(200,255,0,0.04)}
|
||||
.q-item.dragging{opacity:.4;background:var(--surface2)}
|
||||
.q-item.drag-over{border-top:2px solid var(--accent)}
|
||||
.drag-handle{
|
||||
color:var(--muted);cursor:grab;flex-shrink:0;
|
||||
padding:4px 2px;opacity:.4;transition:opacity .15s;
|
||||
display:flex;align-items:center;touch-action:none;
|
||||
}
|
||||
.q-item:hover .drag-handle{opacity:.8}
|
||||
.drag-handle:active{cursor:grabbing}
|
||||
.q-num{font-size:11px;color:var(--muted);width:18px;text-align:right;flex-shrink:0;font-family:'Syne',sans-serif}
|
||||
.q-cover{width:28px;height:28px;border-radius:5px;object-fit:cover;flex-shrink:0;background:var(--surface2)}
|
||||
.q-track{flex:1;font-size:13px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.q-owner{font-size:10px;padding:2px 6px;border-radius:5px;flex-shrink:0;font-weight:500}
|
||||
.bars{width:12px;height:12px;display:flex;align-items:flex-end;gap:1.5px;flex-shrink:0}
|
||||
.bar{width:2.5px;border-radius:1px;background:var(--accent);animation:bA .7s ease-in-out infinite alternate}
|
||||
.bar:nth-child(2){animation-delay:.15s}.bar:nth-child(3){animation-delay:.3s}
|
||||
@keyframes bA{from{height:2px}to{height:11px}}
|
||||
|
||||
/* ── HISTORY TAB ── */
|
||||
.history-empty{text-align:center;padding:3rem 1rem;color:var(--muted);font-size:13px}
|
||||
.history-empty svg{margin-bottom:.75rem;opacity:.3}
|
||||
.history-item{display:flex;align-items:center;gap:10px;padding:10px 1rem;border-bottom:1px solid var(--border);animation:fadeUp .2s ease both}
|
||||
.history-item:last-child{border-bottom:none}
|
||||
.h-cover{width:40px;height:40px;border-radius:8px;object-fit:cover;flex-shrink:0;background:var(--surface2)}
|
||||
.h-info{flex:1;min-width:0}
|
||||
.h-title{font-size:13px;font-weight:500;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.h-sub{font-size:11px;color:var(--muted);margin-top:2px;display:flex;align-items:center;gap:6px}
|
||||
.h-owner{font-size:10px;padding:1px 6px;border-radius:4px}
|
||||
.h-time{font-size:10px;color:var(--muted)}
|
||||
.btn-h-play{width:30px;height:30px;border-radius:50%;border:1px solid var(--border);background:transparent;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .15s}
|
||||
.btn-h-play:hover{background:var(--accent);border-color:var(--accent)}
|
||||
.btn-h-play:hover svg{fill:#0a0a0f}
|
||||
.btn-h-play svg{fill:var(--muted);transition:fill .15s}
|
||||
.history-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
|
||||
.history-head{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;border-bottom:1px solid var(--border)}
|
||||
.history-label{font-family:'Syne',sans-serif;font-size:11px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;color:var(--muted)}
|
||||
.btn-clear{font-size:11px;padding:3px 9px;border:1px solid rgba(255,100,100,0.2);border-radius:7px;background:transparent;color:rgba(255,100,100,0.5);cursor:pointer;font-family:'DM Sans',sans-serif;transition:all .15s}
|
||||
.btn-clear:hover{background:rgba(255,100,100,0.08);color:#ff6b6b;border-color:rgba(255,100,100,0.35)}
|
||||
|
||||
/* ── PARTY TAB (участники + форма) ── */
|
||||
.btn-mix{
|
||||
width:100%;padding:13px;margin-bottom:1rem;
|
||||
font-family:'Syne',sans-serif;font-size:14px;font-weight:800;letter-spacing:.5px;text-transform:uppercase;
|
||||
background:var(--accent);border:none;border-radius:var(--radius);
|
||||
color:#0a0a0f;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:9px;
|
||||
transition:opacity .15s;
|
||||
}
|
||||
.btn-mix:hover{opacity:.9}
|
||||
.btn-mix:active{transform:scale(.99)}
|
||||
.mode-toggle{display:flex;gap:5px;margin-bottom:1rem;background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:4px}
|
||||
.mode-btn{flex:1;padding:7px;font-size:12px;font-family:'Syne',sans-serif;font-weight:600;letter-spacing:.2px;border:none;border-radius:7px;cursor:pointer;background:transparent;color:var(--muted);transition:all .2s}
|
||||
.mode-btn.active{background:var(--surface2);color:var(--text)}
|
||||
.people-list{display:flex;flex-direction:column;gap:.6rem;margin-bottom:1rem}
|
||||
.p-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;animation:fadeUp .3s ease both}
|
||||
.p-head{display:flex;align-items:center;gap:9px;padding:.75rem 1rem;cursor:pointer;user-select:none;transition:background .12s}
|
||||
.p-head:hover{background:var(--surface2)}
|
||||
.avatar{width:30px;height:30px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-family:'Syne',sans-serif;font-size:11px;font-weight:800;flex-shrink:0}
|
||||
.p-name{font-family:'Syne',sans-serif;font-size:13px;font-weight:700;flex:1;letter-spacing:-.1px}
|
||||
.p-count{font-size:11px;color:var(--muted);flex-shrink:0}
|
||||
.p-chevron{color:var(--muted);flex-shrink:0;transition:transform .2s;font-size:11px}
|
||||
.p-chevron.open{transform:rotate(180deg)}
|
||||
.btn-del{width:24px;height:24px;border-radius:7px;border:1px solid rgba(255,100,100,0.15);background:transparent;color:rgba(255,100,100,0.4);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s;flex-shrink:0}
|
||||
.btn-del:hover{background:rgba(255,100,100,0.1);color:#ff6b6b;border-color:rgba(255,100,100,0.35)}
|
||||
.p-chips-wrap{border-top:1px solid var(--border);max-height:0;overflow:hidden;transition:max-height .3s ease}
|
||||
.p-chips-wrap.open{max-height:500px}
|
||||
.chips{display:flex;flex-wrap:wrap;gap:4px;padding:.7rem 1rem}
|
||||
.chip{display:inline-flex;align-items:center;gap:4px;font-size:11px;padding:3px 8px;border-radius:6px;border:1px solid var(--border);background:var(--surface2);color:var(--muted);max-width:220px}
|
||||
.chip span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:165px}
|
||||
.chip.playing{background:rgba(200,255,0,0.07);border-color:rgba(200,255,0,0.25);color:var(--accent);font-weight:500}
|
||||
.dot{width:4px;height:4px;border-radius:50%;background:#4ade80;flex-shrink:0}
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:1.1rem;margin-bottom:1rem}
|
||||
input[type=text],textarea{font-family:'DM Sans',sans-serif;font-size:13px;background:var(--surface2);border:1px solid var(--border);border-radius:9px;padding:9px 13px;color:var(--text);outline:none;width:100%;transition:border-color .2s}
|
||||
input[type=text]:focus,textarea:focus{border-color:rgba(200,255,0,0.35)}
|
||||
.hint{font-size:12px;color:var(--muted);margin-bottom:6px;line-height:1.5}
|
||||
textarea{min-height:80px;resize:vertical;line-height:1.6;margin-top:8px}
|
||||
.err-msg{font-size:12px;color:#ff6b6b;background:rgba(255,107,107,0.08);padding:7px 11px;border-radius:8px;margin-top:6px;display:none;border:1px solid rgba(255,107,107,0.15)}
|
||||
.btn-add{width:100%;padding:11px;margin-top:9px;font-family:'Syne',sans-serif;font-size:13px;font-weight:700;letter-spacing:.4px;background:var(--surface2);border:1px solid var(--border);border-radius:9px;color:var(--text);cursor:pointer;display:flex;align-items:center;justify-content:center;gap:7px;transition:all .2s}
|
||||
.btn-add:hover{border-color:rgba(200,255,0,0.3);color:var(--accent)}
|
||||
.btn{padding:6px 12px;font-size:12px;font-family:'DM Sans',sans-serif;border:1px solid var(--border);border-radius:8px;background:transparent;color:var(--muted);cursor:pointer;transition:all .15s}
|
||||
.btn:hover{background:var(--surface2);color:var(--text)}
|
||||
|
||||
/* ── MOBILE ── */
|
||||
@media(max-width:500px){
|
||||
body{padding:1rem .75rem 5rem}
|
||||
h1{font-size:18px}
|
||||
.header-sub{display:none}
|
||||
.player-hero{min-height:96px}
|
||||
.cover-wrap{width:96px;height:96px}
|
||||
.now-title{font-size:13px}
|
||||
.owner-badge{font-size:10px;padding:2px 7px}
|
||||
.ctrl{width:32px;height:32px}
|
||||
.player-controls{gap:6px;padding:.6rem .75rem}
|
||||
.ctrl-info{font-size:11px}
|
||||
.btn-next{padding:5px 10px;font-size:11px}
|
||||
.audio-wrap{padding:.65rem .75rem}
|
||||
.result-item{padding:8px .75rem}
|
||||
.q-item{padding:8px .75rem}
|
||||
.history-item{padding:9px .75rem}
|
||||
.queue-head{padding:.7rem .75rem}
|
||||
.chips{padding:.6rem .75rem}
|
||||
.p-head{padding:.7rem .75rem}
|
||||
.card{padding:.9rem .75rem}
|
||||
.tab-btn{font-size:12px;padding:7px 4px}
|
||||
.btn-mix{font-size:13px;padding:12px}
|
||||
.mode-btn{font-size:11px;padding:6px 3px}
|
||||
.results-header{padding:6px .75rem}
|
||||
.player-info{padding:.75rem .85rem}
|
||||
.queue-label{font-size:10px}
|
||||
}
|
||||
@media(max-width:360px){
|
||||
.cover-wrap{width:80px;height:80px}
|
||||
.now-title{font-size:12px}
|
||||
.tab-btn span:not(.badge){display:none}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="logo">
|
||||
<svg viewBox="0 0 24 24" fill="none"><path d="M9 18V5l12-2v13" stroke="#0a0a0f" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="6" cy="18" r="3" fill="#0a0a0f"/><circle cx="18" cy="16" r="3" fill="#0a0a0f"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Party Mix</h1>
|
||||
<p class="header-sub">Музыка с hitmotop.com</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PLAYER (всегда виден поверх вкладок) -->
|
||||
<div class="player-card" id="playerCard">
|
||||
<div class="player-hero">
|
||||
<div class="cover-wrap" id="coverWrap">
|
||||
<div class="cover-glow" id="coverGlow"></div>
|
||||
<img class="cover-img" id="coverImg" src="" alt=""/>
|
||||
<canvas class="cover-canvas" id="coverCanvas"></canvas>
|
||||
</div>
|
||||
<div class="player-info">
|
||||
<div class="now-eyebrow">
|
||||
<div class="eq-bars"><div class="eq-bar"></div><div class="eq-bar"></div><div class="eq-bar"></div><div class="eq-bar"></div></div>
|
||||
Сейчас играет
|
||||
</div>
|
||||
<div class="now-title" id="nowTitle">—</div>
|
||||
<span class="owner-badge" id="nowOwner">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="searchStatus" class="search-status">
|
||||
<div class="spinner"></div><span id="searchMsg">Ищем...</span>
|
||||
</div>
|
||||
<div class="audio-wrap" id="audioWrap">
|
||||
<div class="audio-meta"><span id="audioName"></span><span style="margin:0 5px;opacity:.3">·</span><span id="audioArtist"></span></div>
|
||||
<audio id="audioEl" controls preload="auto" crossorigin="anonymous"></audio>
|
||||
</div>
|
||||
<div class="results-wrap" id="resultsWrap">
|
||||
<div class="results-header" onclick="toggleResults()">
|
||||
<span>Найдено — выберите версию</span>
|
||||
<span class="results-chevron" id="resultsChevron">▼</span>
|
||||
</div>
|
||||
<div class="results-body" id="resultsBody"><div id="resultsList"></div></div>
|
||||
</div>
|
||||
<div class="player-controls">
|
||||
<button class="ctrl" onclick="prevTrack()"><svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6 8.5 6V6z"/></svg></button>
|
||||
<button class="ctrl" onclick="reloadTrack()"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-4"/></svg></button>
|
||||
<button class="ctrl" onclick="nextTrack()"><svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zm2.5-6 8.5 6V6z"/></svg></button>
|
||||
<div class="ctrl-info" id="ctrlInfo">— / —</div>
|
||||
<button class="btn-next" onclick="nextTrack()">Далее →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QUEUE (под плеером) -->
|
||||
<div class="queue-card" id="queueCard" style="display:none">
|
||||
<div class="queue-head" onclick="toggleQueue()">
|
||||
<div class="queue-head-left">
|
||||
<span class="queue-label" id="queueLabel">Очередь</span>
|
||||
<button class="btn" onclick="event.stopPropagation();generateMix()" style="padding:3px 9px;font-size:11px">↺</button>
|
||||
</div>
|
||||
<span class="queue-chevron" id="queueChevron">▼</span>
|
||||
</div>
|
||||
<div class="queue-body" id="queueBody">
|
||||
<div class="queue-list" id="queueList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TABS -->
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" onclick="switchTab('party')" id="tab-party">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
<span>Участники</span>
|
||||
</button>
|
||||
<button class="tab-btn" onclick="switchTab('history')" id="tab-history">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
<span>История</span>
|
||||
<span class="badge" id="historyBadge" style="display:none">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- TAB: PARTY -->
|
||||
<div class="tab-content active" id="tab-content-party">
|
||||
<div class="mode-toggle" id="modeToggle" style="display:none">
|
||||
<button class="mode-btn active" id="modeFair" onclick="setMode('fair')">⚖️ По очереди</button>
|
||||
<button class="mode-btn" id="modeRandom" onclick="setMode('random')">🎲 Случайно</button>
|
||||
</div>
|
||||
<button class="btn-mix" onclick="generateMix()">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/></svg>
|
||||
Перемешать и включить
|
||||
</button>
|
||||
<div id="peopleList" class="people-list"></div>
|
||||
<div class="card">
|
||||
<input type="text" id="nameIn" placeholder="Имя участника"/>
|
||||
<div class="hint" style="margin-top:8px">Список треков — каждый с новой строки. Формат: <b style="color:var(--text);font-weight:500">Исполнитель — Название</b></div>
|
||||
<textarea id="tracksIn" placeholder="Тараканы - Пойдём на улицу PALC - Залип Дора - Втюрилась ..."></textarea>
|
||||
<div class="err-msg" id="errMsg"></div>
|
||||
<button class="btn-add" onclick="addPerson()">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12l7-7 7 7"/></svg>
|
||||
Добавить участника
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: HISTORY -->
|
||||
<div class="tab-content" id="tab-content-history">
|
||||
<div class="history-card">
|
||||
<div class="history-head">
|
||||
<span class="history-label">Уже сыграло</span>
|
||||
<button class="btn-clear" onclick="clearHistory()">Очистить</button>
|
||||
</div>
|
||||
<div id="historyList">
|
||||
<div class="history-empty">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
<div>История пуста</div>
|
||||
<div style="font-size:11px;margin-top:4px;opacity:.6">Здесь появятся треки которые уже сыграли</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
const COLORS=[
|
||||
{bg:'rgba(200,255,0,0.1)',text:'#c8ff00'},
|
||||
{bg:'rgba(255,60,172,0.1)',text:'#ff3cac'},
|
||||
{bg:'rgba(0,212,255,0.1)',text:'#00d4ff'},
|
||||
{bg:'rgba(255,165,0,0.1)',text:'#ffa500'},
|
||||
{bg:'rgba(140,100,255,0.1)',text:'#8c64ff'},
|
||||
{bg:'rgba(0,255,140,0.1)',text:'#00ff8c'},
|
||||
{bg:'rgba(255,80,80,0.1)',text:'#ff5050'},
|
||||
];
|
||||
let people=[], queue=[], curIdx=-1, currentResults=[];
|
||||
let shuffleMode='fair';
|
||||
const collapsed={};
|
||||
let queueOpen=false;
|
||||
let history=[];
|
||||
|
||||
// ── TABS ──
|
||||
function switchTab(tab) {
|
||||
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c=>c.classList.remove('active'));
|
||||
document.getElementById('tab-'+tab).classList.add('active');
|
||||
document.getElementById('tab-content-'+tab).classList.add('active');
|
||||
}
|
||||
|
||||
// ── WEB AUDIO VIZ ──
|
||||
let audioCtx=null,analyser=null,source=null,rafId=null;
|
||||
function initAudioViz(el){
|
||||
try{
|
||||
if(!audioCtx){audioCtx=new(window.AudioContext||window.webkitAudioContext)();analyser=audioCtx.createAnalyser();analyser.fftSize=64;analyser.smoothingTimeConstant=0.8;}
|
||||
if(source){try{source.disconnect();}catch(e){}}
|
||||
source=audioCtx.createMediaElementSource(el);
|
||||
source.connect(analyser);analyser.connect(audioCtx.destination);
|
||||
}catch(e){analyser=null;}
|
||||
}
|
||||
function startViz(){
|
||||
if(!analyser)return;
|
||||
const canvas=document.getElementById('coverCanvas'),wrap=document.getElementById('coverWrap'),img=document.getElementById('coverImg');
|
||||
canvas.classList.add('active');wrap.classList.add('playing');img.classList.add('playing');
|
||||
const ctx=canvas.getContext('2d');
|
||||
canvas.width=canvas.offsetWidth;canvas.height=canvas.offsetHeight;
|
||||
const W=canvas.width,H=canvas.height,data=new Uint8Array(analyser.frequencyBinCount);
|
||||
function draw(){
|
||||
rafId=requestAnimationFrame(draw);
|
||||
analyser.getByteFrequencyData(data);
|
||||
ctx.clearRect(0,0,W,H);
|
||||
const barCount=data.length,barW=W/barCount,maxH=H*0.55;
|
||||
for(let i=0;i<barCount;i++){
|
||||
const v=data[i]/255,bh=v*maxH,x=i*barW,hue=80+i*3;
|
||||
const g=ctx.createLinearGradient(0,H,0,H-bh);
|
||||
g.addColorStop(0,`hsla(${hue},100%,60%,.9)`);g.addColorStop(1,`hsla(${hue},100%,80%,.1)`);
|
||||
ctx.fillStyle=g;ctx.fillRect(x,H-bh,barW*.7,bh);
|
||||
}
|
||||
const ov=ctx.createLinearGradient(0,0,0,H);
|
||||
ov.addColorStop(0,'rgba(10,10,15,.3)');ov.addColorStop(.5,'rgba(10,10,15,0)');ov.addColorStop(1,'rgba(10,10,15,.6)');
|
||||
ctx.fillStyle=ov;ctx.fillRect(0,0,W,H);
|
||||
}
|
||||
draw();
|
||||
}
|
||||
function stopViz(){
|
||||
if(rafId){cancelAnimationFrame(rafId);rafId=null;}
|
||||
const canvas=document.getElementById('coverCanvas'),wrap=document.getElementById('coverWrap'),img=document.getElementById('coverImg');
|
||||
canvas.classList.remove('active');wrap.classList.remove('playing');img.classList.remove('playing');
|
||||
canvas.getContext('2d').clearRect(0,0,canvas.width,canvas.height);
|
||||
}
|
||||
function hexToRgba(c,a){if(c.startsWith('rgb'))return c.replace('rgb','rgba').replace(')',`,${a})`);if(c.startsWith('#')){const r=parseInt(c.slice(1,3),16),g=parseInt(c.slice(3,5),16),b=parseInt(c.slice(5,7),16);return`rgba(${r},${g},${b},${a})`;}return`rgba(200,255,0,${a})`;}
|
||||
|
||||
// ── SHUFFLE ──
|
||||
function fairShuffle(ppl){
|
||||
const pools=ppl.map((p,pi)=>{
|
||||
const arr=p.tracks.map((t,ti)=>({title:t.title,owner:p.name,color:p.color,_pi:pi,_ti:ti,img:''}));
|
||||
for(let i=arr.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[arr[i],arr[j]]=[arr[j],arr[i]];}
|
||||
return arr;
|
||||
});
|
||||
const order=[...Array(pools.length).keys()];
|
||||
for(let i=order.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[order[i],order[j]]=[order[j],order[i]];}
|
||||
const result=[];
|
||||
const maxLen=Math.max(...pools.map(p=>p.length));
|
||||
for(let r=0;r<maxLen;r++)for(const pi of order)if(r<pools[pi].length)result.push(pools[pi][r]);
|
||||
return result;
|
||||
}
|
||||
function randomShuffle(ppl){
|
||||
const all=[];
|
||||
ppl.forEach((p,pi)=>p.tracks.forEach((t,ti)=>all.push({title:t.title,owner:p.name,color:p.color,_pi:pi,_ti:ti,img:''})));
|
||||
for(let i=all.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[all[i],all[j]]=[all[j],all[i]];}
|
||||
return all;
|
||||
}
|
||||
function setMode(m){shuffleMode=m;document.getElementById('modeFair').classList.toggle('active',m==='fair');document.getElementById('modeRandom').classList.toggle('active',m==='random');}
|
||||
|
||||
// ── QUEUE TOGGLE ──
|
||||
function toggleQueue(){queueOpen=!queueOpen;document.getElementById('queueBody').classList.toggle('open',queueOpen);document.getElementById('queueChevron').classList.toggle('open',queueOpen);}
|
||||
function toggleResults(){const b=document.getElementById('resultsBody'),c=document.getElementById('resultsChevron'),o=b.classList.toggle('open');c.classList.toggle('open',o);}
|
||||
|
||||
// ── DRAG & DROP ──
|
||||
let dragSrcIdx=null;
|
||||
function initDrag(){
|
||||
const items=document.querySelectorAll('.q-item[draggable]');
|
||||
items.forEach((item,i)=>{
|
||||
item.addEventListener('dragstart',e=>{dragSrcIdx=parseInt(item.dataset.idx);item.classList.add('dragging');e.dataTransfer.effectAllowed='move';});
|
||||
item.addEventListener('dragend',()=>{item.classList.remove('dragging');document.querySelectorAll('.q-item').forEach(it=>it.classList.remove('drag-over'));});
|
||||
item.addEventListener('dragover',e=>{e.preventDefault();e.dataTransfer.dropEffect='move';item.classList.add('drag-over');});
|
||||
item.addEventListener('dragleave',()=>item.classList.remove('drag-over'));
|
||||
item.addEventListener('drop',e=>{
|
||||
e.preventDefault();item.classList.remove('drag-over');
|
||||
const tgtIdx=parseInt(item.dataset.idx);
|
||||
if(dragSrcIdx===null||dragSrcIdx===tgtIdx)return;
|
||||
// Перемещаем в очереди
|
||||
const moved=queue.splice(dragSrcIdx,1)[0];
|
||||
queue.splice(tgtIdx,0,moved);
|
||||
// Корректируем curIdx
|
||||
if(curIdx===dragSrcIdx)curIdx=tgtIdx;
|
||||
else if(dragSrcIdx<curIdx&&tgtIdx>=curIdx)curIdx--;
|
||||
else if(dragSrcIdx>curIdx&&tgtIdx<=curIdx)curIdx++;
|
||||
dragSrcIdx=null;
|
||||
renderQueue();
|
||||
});
|
||||
});
|
||||
// Touch drag
|
||||
initTouchDrag();
|
||||
}
|
||||
|
||||
// Touch drag & drop
|
||||
function initTouchDrag(){
|
||||
let touchSrcIdx=null,clone=null,startY=0;
|
||||
const list=document.getElementById('queueList');
|
||||
list.querySelectorAll('.drag-handle').forEach(handle=>{
|
||||
handle.addEventListener('touchstart',e=>{
|
||||
const item=handle.closest('.q-item');
|
||||
touchSrcIdx=parseInt(item.dataset.idx);
|
||||
startY=e.touches[0].clientY;
|
||||
item.classList.add('dragging');
|
||||
e.preventDefault();
|
||||
},{passive:false});
|
||||
handle.addEventListener('touchmove',e=>{
|
||||
if(touchSrcIdx===null)return;
|
||||
e.preventDefault();
|
||||
const y=e.touches[0].clientY;
|
||||
const els=list.querySelectorAll('.q-item:not(.dragging)');
|
||||
let targetIdx=null;
|
||||
els.forEach(el=>{
|
||||
const r=el.getBoundingClientRect();
|
||||
if(y>r.top&&y<r.bottom)targetIdx=parseInt(el.dataset.idx);
|
||||
el.classList.remove('drag-over');
|
||||
});
|
||||
if(targetIdx!==null){
|
||||
const t=list.querySelector(`.q-item[data-idx="${targetIdx}"]`);
|
||||
if(t)t.classList.add('drag-over');
|
||||
}
|
||||
},{passive:false});
|
||||
handle.addEventListener('touchend',e=>{
|
||||
if(touchSrcIdx===null)return;
|
||||
const items=list.querySelectorAll('.q-item');
|
||||
let tgtIdx=null;
|
||||
items.forEach(el=>{if(el.classList.contains('drag-over'))tgtIdx=parseInt(el.dataset.idx);el.classList.remove('drag-over','dragging');});
|
||||
if(tgtIdx!==null&&tgtIdx!==touchSrcIdx){
|
||||
const moved=queue.splice(touchSrcIdx,1)[0];
|
||||
queue.splice(tgtIdx,0,moved);
|
||||
if(curIdx===touchSrcIdx)curIdx=tgtIdx;
|
||||
else if(touchSrcIdx<curIdx&&tgtIdx>=curIdx)curIdx--;
|
||||
else if(touchSrcIdx>curIdx&&tgtIdx<=curIdx)curIdx++;
|
||||
renderQueue();
|
||||
}
|
||||
touchSrcIdx=null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── SEARCH ──
|
||||
async function searchHitmotop(query){
|
||||
try{
|
||||
const r=await fetch(`/search?q=${encodeURIComponent(query)}`,{signal:AbortSignal.timeout(10000)});
|
||||
if(!r.ok)return[];
|
||||
const html=await r.text();
|
||||
if(!html.includes('track__download-btn'))return[];
|
||||
const results=[];
|
||||
const dlRegex=/href="(\/get\/music\/[^"]+\.mp3)"/g;
|
||||
const metaRegex=/data-musmeta='([^']+)'/g;
|
||||
const dlHrefs=[];let dlm;
|
||||
while((dlm=dlRegex.exec(html))!==null)dlHrefs.push(dlm[1]);
|
||||
const metas=[];let mm;
|
||||
while((mm=metaRegex.exec(html))!==null){try{metas.push(JSON.parse(mm[1]));}catch(e){}}
|
||||
for(let i=0;i<dlHrefs.length;i++){
|
||||
const href=dlHrefs[i];if(!href)continue;
|
||||
const meta=metas[i]||{};
|
||||
const originalMp3='https://rus.hitmotop.com'+href;
|
||||
const mp3=`/mp3?url=${encodeURIComponent(originalMp3)}`;
|
||||
const hrefPos=html.indexOf(href);
|
||||
const chunk=hrefPos>=0?html.substring(Math.max(0,hrefPos-800),hrefPos+200):'';
|
||||
const dur=chunk.match(/track__time[^>]*>([^<]+)</)|| ['',''];
|
||||
let title=meta.title||'',artist=meta.artist||'';
|
||||
if(!title){const tm=chunk.match(/track__title[^>]*>([^<]+)</);title=tm?tm[1].trim():'';}
|
||||
if(!artist){const am=chunk.match(/track__desc[^>]*>([^<]+)</);artist=am?am[1].trim():'';}
|
||||
results.push({mp3,originalMp3,title,artist,duration:dur[1].trim(),img:meta.img||''});
|
||||
}
|
||||
return results;
|
||||
}catch(e){return[];}
|
||||
}
|
||||
|
||||
// ── PEOPLE ──
|
||||
function initials(n){return n.split(' ').map(w=>w[0]).join('').toUpperCase().slice(0,2);}
|
||||
function addPerson(){
|
||||
const name=document.getElementById('nameIn').value.trim();
|
||||
const raw=document.getElementById('tracksIn').value.trim();
|
||||
const err=document.getElementById('errMsg');
|
||||
err.style.display='none';
|
||||
if(!name){document.getElementById('nameIn').focus();return;}
|
||||
if(!raw){err.textContent='Вставьте список треков';err.style.display='block';return;}
|
||||
const tracks=raw.split('\n').map(l=>l.trim()).filter(l=>l.length>1).slice(0,50);
|
||||
if(!tracks.length){err.textContent='Не нашли треков.';err.style.display='block';return;}
|
||||
const pi=people.length;
|
||||
people.push({name,color:COLORS[pi%COLORS.length],tracks:tracks.map(t=>({title:t}))});
|
||||
collapsed[pi]=false;
|
||||
document.getElementById('nameIn').value='';
|
||||
document.getElementById('tracksIn').value='';
|
||||
renderPeople();
|
||||
if(people.length>=2)document.getElementById('modeToggle').style.display='flex';
|
||||
}
|
||||
function removePerson(i){
|
||||
people.splice(i,1);people.forEach((p,j)=>p.color=COLORS[j%COLORS.length]);
|
||||
renderPeople();
|
||||
if(people.length<2)document.getElementById('modeToggle').style.display='none';
|
||||
}
|
||||
function toggleCollapse(pi){
|
||||
collapsed[pi]=!collapsed[pi];
|
||||
document.getElementById(`chips-${pi}`)?.classList.toggle('open',!collapsed[pi]);
|
||||
document.getElementById(`chev-${pi}`)?.classList.toggle('open',!collapsed[pi]);
|
||||
}
|
||||
function renderPeople(){
|
||||
const c=document.getElementById('peopleList');
|
||||
c.innerHTML=people.map((p,pi)=>{
|
||||
const isOpen=!collapsed[pi];
|
||||
const chips=p.tracks.map((t,ti)=>{
|
||||
const isPlay=curIdx>=0&&queue[curIdx]&&queue[curIdx]._pi===pi&&queue[curIdx]._ti===ti;
|
||||
return`<span class="chip${isPlay?' playing':''}"><div class="dot"></div><span>${t.title}</span></span>`;
|
||||
}).join('');
|
||||
return`<div class="p-card">
|
||||
<div class="p-head" onclick="toggleCollapse(${pi})">
|
||||
<div class="avatar" style="background:${p.color.bg};color:${p.color.text}">${initials(p.name)}</div>
|
||||
<span class="p-name">${p.name}</span>
|
||||
<span class="p-count">${p.tracks.length} тр.</span>
|
||||
<span class="p-chevron${isOpen?' open':''}" id="chev-${pi}">▼</span>
|
||||
<button class="btn-del" onclick="event.stopPropagation();removePerson(${pi})">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-chips-wrap${isOpen?' open':''}" id="chips-${pi}">
|
||||
<div class="chips">${chips}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── GENERATE ──
|
||||
function generateMix(){
|
||||
if(!people.length){alert('Добавьте участников!');return;}
|
||||
queue=shuffleMode==='fair'?fairShuffle(people):randomShuffle(people);
|
||||
curIdx=0;renderQueue();loadTrack(0);
|
||||
}
|
||||
|
||||
// ── LOAD TRACK ──
|
||||
async function loadTrack(i){
|
||||
if(i<0||i>=queue.length)return;
|
||||
curIdx=i;
|
||||
const t=queue[i];
|
||||
const audio=document.getElementById('audioEl');
|
||||
stopViz();audio.pause();audio.src='';
|
||||
document.getElementById('audioWrap').style.display='none';
|
||||
document.getElementById('resultsWrap').style.display='none';
|
||||
document.getElementById('playerCard').style.display='block';
|
||||
document.getElementById('nowTitle').textContent=t.title;
|
||||
const ob=document.getElementById('nowOwner');
|
||||
ob.textContent=t.owner;ob.style.background=t.color.bg;ob.style.color=t.color.text;
|
||||
document.getElementById('ctrlInfo').textContent=`${i+1} / ${queue.length}`;
|
||||
document.getElementById('searchStatus').style.display='flex';
|
||||
document.getElementById('searchMsg').textContent='Ищем...';
|
||||
document.getElementById('coverImg').src='data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="110" height="110"><rect fill="%231a1a26" width="110" height="110"/></svg>';
|
||||
document.getElementById('playerCard').scrollIntoView({behavior:'smooth',block:'start'});
|
||||
renderQueue();renderPeople();
|
||||
|
||||
const results=await searchHitmotop(t.title);
|
||||
document.getElementById('searchStatus').style.display='none';
|
||||
|
||||
if(!results.length){
|
||||
document.getElementById('searchStatus').style.display='flex';
|
||||
document.getElementById('searchMsg').innerHTML=`Не найдено. <a href="https://rus.hitmotop.com/search?q=${encodeURIComponent(t.title)}" target="_blank" style="color:var(--accent);margin-left:4px">hitmotop ↗</a>`;
|
||||
setTimeout(()=>{if(curIdx===i)nextTrack();},4000);
|
||||
return;
|
||||
}
|
||||
|
||||
currentResults=results;
|
||||
if(results[0]?.img&&!results[0].img.includes('no-cover')){
|
||||
const proxyImg=`/img?url=${encodeURIComponent(results[0].img)}`;
|
||||
document.getElementById('coverImg').src=proxyImg;
|
||||
queue[i].img=proxyImg;renderQueue();
|
||||
}
|
||||
if(results.length>1)renderResultsList(results);
|
||||
playResult(0);
|
||||
}
|
||||
|
||||
function renderResultsList(results){
|
||||
document.getElementById('resultsWrap').style.display='block';
|
||||
document.getElementById('resultsList').innerHTML=results.map((r,i)=>`
|
||||
<div class="result-item" id="res-${i}" onclick="playResult(${i})">
|
||||
<span class="result-num">${i+1}</span>
|
||||
${r.img&&!r.img.includes('no-cover')?`<img class="res-cover" src="/img?url=${encodeURIComponent(r.img)}" onerror="this.style.display='none'"/>`:''}
|
||||
<div class="result-info"><div class="result-name">${r.title}</div><div class="result-artist">${r.artist}</div></div>
|
||||
<div class="result-dur">${r.duration}</div>
|
||||
<button class="result-play-btn" id="rpb-${i}" onclick="event.stopPropagation();playResult(${i})">
|
||||
<svg width="9" height="9" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
function playResult(idx){
|
||||
if(!currentResults[idx])return;
|
||||
const r=currentResults[idx];
|
||||
document.querySelectorAll('.result-item').forEach((el,i)=>el.classList.toggle('active',i===idx));
|
||||
document.querySelectorAll('.result-play-btn').forEach((el,i)=>el.classList.toggle('active',i===idx));
|
||||
document.getElementById('audioName').textContent=r.title;
|
||||
document.getElementById('audioArtist').textContent=r.artist;
|
||||
if(r.img&&!r.img.includes('no-cover'))document.getElementById('coverImg').src=`/img?url=${encodeURIComponent(r.img)}`;
|
||||
const audio=document.getElementById('audioEl');
|
||||
stopViz();audio.pause();audio.onerror=()=>{};
|
||||
audio.src=r.mp3;audio.load();
|
||||
document.getElementById('audioWrap').style.display='block';
|
||||
audio.play().then(()=>{if(audioCtx?.state==='suspended')audioCtx.resume();startViz();}).catch(()=>{});
|
||||
audio.onplay=()=>{if(audioCtx?.state==='suspended')audioCtx.resume();startViz();};
|
||||
audio.onpause=()=>stopViz();
|
||||
audio.onended=()=>{
|
||||
stopViz();
|
||||
// Добавляем в историю
|
||||
addToHistory({title:r.title,artist:r.artist,img:r.img,owner:queue[curIdx]?.owner||'',color:queue[curIdx]?.color||COLORS[0]});
|
||||
if(curIdx<queue.length-1)loadTrack(curIdx+1);
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('audioEl').addEventListener('play',function init(){initAudioViz(this);this.removeEventListener('play',init);},{once:true});
|
||||
|
||||
function prevTrack(){stopViz();if(curIdx>0)loadTrack(curIdx-1);}
|
||||
function nextTrack(){stopViz();if(curIdx<queue.length-1)loadTrack(curIdx+1);}
|
||||
function reloadTrack(){stopViz();loadTrack(curIdx);}
|
||||
|
||||
// ── HISTORY ──
|
||||
function addToHistory(item){
|
||||
item.playedAt=new Date().toLocaleTimeString('ru',{hour:'2-digit',minute:'2-digit'});
|
||||
history.unshift(item); // новые сверху
|
||||
updateHistoryBadge();
|
||||
renderHistory();
|
||||
}
|
||||
function updateHistoryBadge(){
|
||||
const b=document.getElementById('historyBadge');
|
||||
if(history.length>0){b.textContent=history.length;b.style.display='inline-block';}
|
||||
else b.style.display='none';
|
||||
}
|
||||
function clearHistory(){history=[];updateHistoryBadge();renderHistory();}
|
||||
function renderHistory(){
|
||||
const el=document.getElementById('historyList');
|
||||
if(!history.length){
|
||||
el.innerHTML=`<div class="history-empty">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
<div>История пуста</div>
|
||||
<div style="font-size:11px;margin-top:4px;opacity:.6">Здесь появятся треки которые уже сыграли</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
el.innerHTML=history.map((item,i)=>`
|
||||
<div class="history-item" style="animation-delay:${i*0.03}s">
|
||||
${item.img&&!item.img.includes('no-cover')?`<img class="h-cover" src="/img?url=${encodeURIComponent(item.img)}" onerror="this.style.display='none'"/>`:'<div class="h-cover"></div>'}
|
||||
<div class="h-info">
|
||||
<div class="h-title">${item.title}</div>
|
||||
<div class="h-sub">
|
||||
<span class="h-owner" style="background:${item.color.bg};color:${item.color.text}">${item.owner}</span>
|
||||
<span class="h-time">${item.playedAt}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
// ── QUEUE RENDER ──
|
||||
function renderQueue(){
|
||||
const qc=document.getElementById('queueCard'),ql=document.getElementById('queueList'),lbl=document.getElementById('queueLabel');
|
||||
if(!queue.length){qc.style.display='none';return;}
|
||||
qc.style.display='block';
|
||||
lbl.textContent=`Очередь · ${queue.length} треков`;
|
||||
ql.innerHTML=queue.map((t,i)=>{
|
||||
const active=i===curIdx;
|
||||
const ind=active
|
||||
?`<div class="bars"><div class="bar"></div><div class="bar"></div><div class="bar"></div></div>`
|
||||
:`<span class="q-num">${i+1}</span>`;
|
||||
const thumb=t.img?`<img class="q-cover" src="${t.img}" onerror="this.style.display='none'"/>`:'<div class="q-cover"></div>';
|
||||
return`<div class="q-item${active?' active':''}" data-idx="${i}" draggable="true" onclick="handleQueueClick(event,${i})">
|
||||
<div class="drag-handle" title="Перетащить">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><circle cx="9" cy="5" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="9" cy="19" r="1.5"/><circle cx="15" cy="5" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="15" cy="19" r="1.5"/></svg>
|
||||
</div>
|
||||
${ind}${thumb}
|
||||
<span class="q-track">${t.title}</span>
|
||||
<span class="q-owner" style="background:${t.color.bg};color:${t.color.text}">${t.owner}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
setTimeout(()=>{
|
||||
document.querySelector('.q-item.active')?.scrollIntoView({block:'nearest'});
|
||||
initDrag();
|
||||
},50);
|
||||
}
|
||||
|
||||
function handleQueueClick(e,i){
|
||||
// Не реагируем на клик по drag handle
|
||||
if(e.target.closest('.drag-handle'))return;
|
||||
loadTrack(i);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user