-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjukebox.html
More file actions
245 lines (231 loc) · 16.7 KB
/
Copy pathjukebox.html
File metadata and controls
245 lines (231 loc) · 16.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>JUKEBOX</title>
<style>
:root{ --gold:#d9a441; --gold-soft:#e8c47a; --ink:#050507;
--panel:rgba(14,15,20,.74); --line:rgba(217,164,65,.18); --txt:#e8e3d6; --dim:#8c8576; }
*{box-sizing:border-box; -webkit-tap-highlight-color:transparent}
html,body{margin:0;height:100%;background:var(--ink);overflow:hidden;
font-family:"Cormorant Garamond",Georgia,"Times New Roman",serif;color:var(--txt);user-select:none}
#stage{position:fixed;inset:0}
canvas#scene{position:absolute;inset:0;width:100%;height:100%;display:block}
#gate{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;
gap:10px;z-index:40;cursor:pointer;background:radial-gradient(ellipse at center,rgba(0,0,0,.1),rgba(0,0,0,.5));
transition:opacity .8s}
#gate.hidden{opacity:0;pointer-events:none}
#gate .ring{font-size:13px;letter-spacing:.42em;text-transform:uppercase;color:var(--gold-soft);animation:breathe 3.4s ease-in-out infinite}
#gate .sub{font-size:12px;letter-spacing:.28em;color:var(--dim);text-transform:uppercase}
@keyframes breathe{0%,100%{opacity:.35}50%{opacity:.95}}
#lobby{position:absolute;top:0;left:0;height:100%;width:300px;z-index:20;display:flex;flex-direction:column;
background:linear-gradient(90deg,var(--ink),rgba(5,5,7,0));padding:18px 14px 14px;
transition:transform .5s cubic-bezier(.2,.8,.2,1);backdrop-filter:blur(2px)}
#lobby.collapsed{transform:translateX(-260px)}
#lobby .brand{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}
#lobby .brand h1{font-size:18px;letter-spacing:.5em;font-weight:600;margin:0;color:var(--gold-soft);text-transform:uppercase;padding-left:6px}
#lobby .brand .count{font-size:11px;color:var(--dim);letter-spacing:.2em}
#toggleLobby{position:absolute;top:16px;right:-2px;width:26px;height:46px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--gold);opacity:.55;font-size:14px}
#toggleLobby:hover{opacity:1}
#rows{flex:1;overflow-y:auto;overflow-x:hidden;margin:8px 0;padding-right:4px}
#rows::-webkit-scrollbar{width:6px}
#rows::-webkit-scrollbar-thumb{background:rgba(217,164,65,.25);border-radius:3px}
.row{display:flex;align-items:center;gap:11px;padding:8px;border-radius:10px;cursor:pointer;position:relative;transition:background .25s;margin-bottom:2px}
.row:hover{background:rgba(217,164,65,.06)} .row.active{background:rgba(217,164,65,.10)}
.row .thumb{width:38px;height:38px;border-radius:50%;flex:none;background:#111;box-shadow:0 0 0 1px rgba(217,164,65,.25),0 0 12px rgba(0,0,0,.6);overflow:hidden}
.row .thumb img{width:100%;height:100%;display:block}
.row.active .thumb img{animation:spin 6s linear infinite} @keyframes spin{to{transform:rotate(360deg)}}
.row .meta{flex:1;min-width:0}
.row .title{font-size:15px;line-height:1.15;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.row .tag{font-size:11px;color:var(--dim);letter-spacing:.14em;text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.row .dot{width:6px;height:6px;border-radius:50%;flex:none;background:var(--gold);opacity:.85}
.row .dot.sim{background:#5a6b7a;opacity:.7} .row .dot.missing{background:#7a3a3a}
.row.active .glowbar{position:absolute;left:0;top:6px;bottom:6px;width:2px;border-radius:2px;background:var(--gold);box-shadow:0 0 10px var(--gold)}
.row .x{opacity:0;color:var(--dim);font-size:14px;padding:2px 4px;transition:opacity .2s}
.row:hover .x{opacity:.7} .row .x:hover{color:#c98}
#add{border-top:1px solid var(--line);padding-top:10px}
#add textarea{width:100%;height:50px;resize:none;background:rgba(0,0,0,.35);color:var(--txt);border:1px solid var(--line);border-radius:9px;padding:8px 10px;font-family:inherit;font-size:13px;outline:none}
#add textarea::placeholder{color:#5d574c}
.btns{display:flex;gap:7px;margin-top:8px;flex-wrap:wrap}
.btn{flex:1;min-width:60px;text-align:center;font-size:11px;letter-spacing:.16em;text-transform:uppercase;padding:8px 6px;border:1px solid var(--line);border-radius:8px;color:var(--gold-soft);cursor:pointer;background:rgba(217,164,65,.04);transition:.2s}
.btn:hover{background:rgba(217,164,65,.13);color:#fff} .btn.ghost{color:var(--dim)}
#add .hint{font-size:11px;color:#5d574c;margin-top:8px;line-height:1.4}
#transport{position:absolute;left:50%;bottom:22px;transform:translateX(-50%);z-index:25;display:flex;align-items:center;gap:14px;padding:12px 20px;border-radius:40px;background:var(--panel);border:1px solid var(--line);backdrop-filter:blur(10px);opacity:0;transition:opacity .45s;pointer-events:none}
#transport.show{opacity:1;pointer-events:auto}
#transport .ic{width:22px;height:22px;cursor:pointer;color:var(--gold-soft);opacity:.8;display:flex;align-items:center;justify-content:center;transition:.2s}
#transport .ic:hover{opacity:1;color:#fff;transform:scale(1.08)} #transport .ic.big{width:30px;height:30px} #transport .ic.on{color:var(--gold);opacity:1}
#scrubWrap{display:flex;align-items:center;gap:9px;min-width:220px}
#time,#dur{font-size:12px;color:var(--dim);letter-spacing:.08em;font-variant-numeric:tabular-nums;width:38px} #dur{text-align:right}
.bar{flex:1;height:3px;border-radius:3px;background:rgba(217,164,65,.16);position:relative;cursor:pointer}
.bar .fill{position:absolute;left:0;top:0;bottom:0;border-radius:3px;background:var(--gold)}
.bar .knob{position:absolute;top:50%;transform:translate(-50%,-50%);width:10px;height:10px;border-radius:50%;background:var(--gold-soft);box-shadow:0 0 8px var(--gold);opacity:0;transition:opacity .2s}
.bar:hover .knob{opacity:1} #vol{width:70px}
#nowp{position:absolute;left:50%;bottom:86px;transform:translateX(-50%);z-index:15;text-align:center;opacity:0;transition:opacity 1s;pointer-events:none;max-width:70vw}
#nowp.show{opacity:.92}
#nowp .t{font-size:22px;letter-spacing:.08em;color:#f3ecda;text-shadow:0 2px 30px rgba(217,164,65,.4)}
#nowp .a{font-size:14px;font-style:italic;letter-spacing:.14em;color:var(--gold-soft);opacity:.85;margin-top:3px}
#nowp .m{font-size:12px;letter-spacing:.32em;text-transform:uppercase;color:var(--gold);opacity:.7;margin-top:4px}
/* controls panel */
#panel{position:absolute;top:0;right:0;height:100%;width:230px;z-index:22;padding:18px 16px;overflow-y:auto;
background:linear-gradient(270deg,var(--ink),rgba(5,5,7,0));transition:transform .5s cubic-bezier(.2,.8,.2,1);backdrop-filter:blur(2px)}
#panel.collapsed{transform:translateX(210px)}
#panel::-webkit-scrollbar{width:6px} #panel::-webkit-scrollbar-thumb{background:rgba(217,164,65,.25);border-radius:3px}
#togglePanel{position:absolute;top:16px;left:-2px;width:26px;height:46px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--gold);opacity:.55;font-size:16px}
#togglePanel:hover{opacity:1}
.grp{margin-bottom:16px}
.grp h3{font-size:11px;letter-spacing:.3em;text-transform:uppercase;color:var(--dim);margin:0 0 8px;font-weight:600}
.pills{display:flex;flex-wrap:wrap;gap:6px}
.pill{font-size:12px;letter-spacing:.05em;padding:6px 11px;border:1px solid var(--line);border-radius:20px;color:var(--gold-soft);cursor:pointer;background:rgba(217,164,65,.03);transition:.18s;white-space:nowrap}
.pill:hover{background:rgba(217,164,65,.12);color:#fff}
.pill.on{background:var(--gold);color:#1a1206;border-color:var(--gold);box-shadow:0 0 14px rgba(217,164,65,.5)}
.pill.mode{min-width:30px;text-align:center;padding:6px 8px}
#panel input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:3px;border-radius:3px;background:rgba(217,164,65,.2);outline:none;margin:11px 0 4px}
#panel input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--gold);box-shadow:0 0 10px var(--gold);cursor:pointer}
#panel input[type=range]::-moz-range-thumb{width:14px;height:14px;border:none;border-radius:50%;background:var(--gold);box-shadow:0 0 10px var(--gold);cursor:pointer}
.sensval{font-size:12px;color:var(--gold-soft);letter-spacing:.1em;text-align:right}
.cust{margin:0 0 10px}
.cust .custHead{display:flex;justify-content:space-between;align-items:baseline;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim)}
.cust .custVal{color:var(--gold-soft);font-size:11px;letter-spacing:.06em}
.cust input[type=range]{margin:5px 0 0!important}
.vsearch{width:100%;box-sizing:border-box;padding:6px 10px;border:1px solid var(--line);border-radius:14px;background:rgba(217,164,65,.04);color:var(--gold-soft);font-size:12px;letter-spacing:.05em;outline:none;margin-bottom:8px}
.vsearch::placeholder{color:var(--dim)}
.vsearch:focus{border-color:var(--gold);background:rgba(217,164,65,.08)}
.scrollbox{max-height:188px;overflow-y:auto;padding:2px 4px 2px 0;align-content:flex-start}
.scrollbox::-webkit-scrollbar{width:5px} .scrollbox::-webkit-scrollbar-thumb{background:rgba(217,164,65,.25);border-radius:3px}
.scrollbox .pill{font-size:11px;padding:5px 9px}
.pill .sw{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:middle;box-shadow:0 0 5px currentColor}
.pill.on .sw{box-shadow:none}
#meter{height:4px;border-radius:3px;background:rgba(217,164,65,.12);overflow:hidden;margin-top:8px}
#meter .lvl{height:100%;width:0;background:linear-gradient(90deg,var(--gold),#ffe7a8);transition:width .05s linear}
#drop{position:absolute;inset:0;z-index:50;display:none;align-items:center;justify-content:center;background:rgba(5,5,7,.78);border:2px dashed var(--gold);color:var(--gold-soft);font-size:18px;letter-spacing:.3em;text-transform:uppercase}
#drop.show{display:flex}
@media(max-width:760px){
#lobby{width:84vw;transform:translateX(-100%);background:rgba(5,5,7,.94)} #lobby.collapsed{transform:translateX(-100%)} #lobby.open{transform:translateX(0)}
#toggleLobby{right:-30px;background:var(--panel);border-radius:0 8px 8px 0;border:1px solid var(--line)}
#panel{width:80vw;transform:translateX(100%);background:rgba(5,5,7,.94)} #panel.collapsed{transform:translateX(100%)} #panel.open{transform:translateX(0)}
#togglePanel{left:-30px;background:var(--panel);border-radius:8px 0 0 8px;border:1px solid var(--line)}
#scrubWrap{min-width:130px} #vol{display:none} #nowp .t{font-size:18px}
}
input[type=file]{display:none}
</style>
</head>
<body>
<div id="stage">
<canvas id="scene"></canvas>
<div id="lobby" class="collapsed">
<div id="toggleLobby">☰</div>
<div class="brand"><h1>Jukebox</h1><span class="count" id="count">0</span></div>
<div id="rows"></div>
<div id="add">
<textarea id="links" placeholder="Paste Suno links or mp3 URLs — one per line. Bulk-paste = a playlist."></textarea>
<div class="btns">
<div class="btn" id="addBtn">Add to queue</div>
<div class="btn ghost" id="fileBtn">Files</div>
</div>
<div class="btns">
<div class="btn ghost" id="exportBtn">Export</div>
<div class="btn ghost" id="importBtn">Import</div>
<div class="btn ghost" id="shuffleQ">Shuffle</div>
</div>
<div class="hint">Drop audio to add · drop an image/video to set the backdrop · double-click a row to rename · press 1–0 for party modes.</div>
</div>
<input type="file" id="fileInput" accept="audio/*,image/*,video/*" multiple>
<input type="file" id="importInput" accept="application/json,.json">
</div>
<div id="panel" class="collapsed">
<div id="togglePanel">⚙</div>
<div class="grp"><h3>Party Mode</h3><div class="pills" id="modePills"></div></div>
<div class="grp"><h3>Centerpiece</h3><div class="pills" id="shapePills"></div></div>
<div class="grp"><h3>Spin</h3><div class="pills" id="spinPills"></div></div>
<div class="grp"><h3>Backdrop — <span id="bgCount">240</span></h3>
<input type="text" id="bgSearch" class="vsearch" placeholder="Search backdrops…">
<div class="pills scrollbox" id="bgPresetPills"></div>
<div class="pills" style="margin-top:8px"><div class="pill" id="setBgBtn">Set image/video…</div><div class="pill" id="clearBgBtn">Clear</div></div>
<input type="file" id="bgFileInput" accept="image/*,video/*" style="display:none"></div>
<div class="grp"><h3>Visualizers — <span id="vizCount">0</span>/3</h3>
<input type="text" id="vizSearch" class="vsearch" placeholder="Search visualizers…">
<div class="pills scrollbox" id="vizPills"></div>
</div>
<div class="grp"><h3>Customize</h3>
<div id="customSliders"></div>
<div class="pills"><div class="pill" id="resetCustom">Reset all</div></div>
</div>
<div class="grp"><h3>Reactivity</h3>
<div class="pills" id="agcPills"></div>
<input type="range" id="sens" min="0.3" max="3" step="0.05" value="1">
<div class="sensval" id="sensVal">1.00×</div>
<div id="meter"><div class="lvl" id="meterLvl"></div></div>
</div>
</div>
<div id="nowp"><div class="t" id="npTitle"></div><div class="a" id="npArtist"></div><div class="m" id="npMood"></div></div>
<div id="transport">
<div class="ic" id="prev" title="Previous">⏮</div>
<div class="ic big" id="play" title="Play / Pause">►</div>
<div class="ic" id="next" title="Next">⏯</div>
<div id="scrubWrap"><span id="time">0:00</span><div class="bar" id="seek"><div class="fill" id="seekFill"></div><div class="knob" id="seekKnob"></div></div><span id="dur">0:00</span></div>
<div class="ic" id="shuffle" title="Shuffle">⥁</div>
<div class="ic" id="loop" title="Loop set">↻</div>
<div class="bar" id="vol"><div class="fill" id="volFill"></div><div class="knob" id="volKnob"></div></div>
</div>
<div id="gate"><div class="ring">Tap for sound</div><div class="sub">the set is already playing</div></div>
<div id="drop">Release to add</div>
</div>
<script type="importmap">
{ "imports": {
"three": "./vendor/three.module.js",
"three/addons/": "./vendor/jsm/"
}}
</script>
<!-- Inline (non-module) error surface: runs before the deferred module, so it
catches a blocked import, a WebGL/renderer throw, or any top-level error
and prints it ON SCREEN instead of leaving a silent dead gate. -->
<script>
(function(){
function showErr(title, detail){
var o=document.getElementById('bootErr');
if(!o){
o=document.createElement('div');o.id='bootErr';
o.style.cssText='position:fixed;inset:0;z-index:99999;display:flex;flex-direction:column;'
+'align-items:center;justify-content:center;gap:14px;padding:28px;'
+'background:rgba(8,7,12,.94);color:#f4e9c9;font:14px/1.5 ui-monospace,Menlo,Consolas,monospace;'
+'text-align:center;-webkit-user-select:text;user-select:text';
document.body.appendChild(o);
}
o.innerHTML='<div style="font:600 18px/1.3 system-ui,sans-serif;color:#e8c873">Jukebox couldn’t start</div>'
+'<div style="max-width:680px;color:#bdb4a0">'+title+'</div>'
+'<pre style="max-width:680px;max-height:40vh;overflow:auto;margin:0;padding:12px 14px;'
+'background:#000;border:1px solid #3a3326;border-radius:8px;color:#ffb4a0;'
+'white-space:pre-wrap;text-align:left">'+detail+'</pre>'
+'<div style="font-size:12px;color:#8a826f">Screenshot this and send it over — it names the exact fix.</div>';
}
function esc(s){return String(s==null?'':s).replace(/[&<>]/g,function(c){return {'&':'&','<':'<','>':'>'}[c];});}
window.addEventListener('error', function(e){
// Resource (script/img) load failures put the failing element on e.target.
if(e.target && e.target!==window && (e.target.src||e.target.href)){
showErr('A file failed to load (most often a blocked or missing import):',
esc(e.target.src||e.target.href));
return;
}
var d=esc(e.message)+(e.filename?('\n at '+esc(e.filename)+':'+e.lineno+':'+e.colno):'');
if(e.error&&e.error.stack)d+='\n\n'+esc(e.error.stack);
showErr('A script error stopped the engine:', d);
}, true);
window.addEventListener('unhandledrejection', function(e){
var r=e.reason; showErr('An async error stopped the engine:',
esc(r&&r.stack?r.stack:(r&&r.message?r.message:r)));
});
// Safety net: if 8s pass and the engine never marked itself ready, say so.
window.__jukeboxBooted=false;
setTimeout(function(){
if(!window.__jukeboxBooted && !document.getElementById('bootErr')){
showErr('The engine script never finished loading within 8 seconds.',
'No error was thrown — this usually means the module import is hanging '
+'(check that vendor/three.module.js is being served) or WebGL is unavailable.');
}
}, 8000);
})();
</script>
<script type="module" src="jukebox.js"></script>
</body>
</html>