Run 3: settings sidebar panel
- Gear button opens sidebar to new SETTINGS tab (replaces confirm dialog) - Settings: API key management (save/clear), shuffle toggle, volume slider, hotkeys reference - Shuffle state persisted to vf_shuffle localStorage key - Volume restored on player ready from vf_volume localStorage key Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
+91
-4
@@ -179,6 +179,22 @@ html,body{width:100%;height:100%;overflow:hidden;background:var(--bg);font-famil
|
|||||||
|
|
||||||
/* URL form */
|
/* URL form */
|
||||||
.url-hint{font-size:11px;color:var(--dim);margin-bottom:10px;line-height:1.6;letter-spacing:.3px}
|
.url-hint{font-size:11px;color:var(--dim);margin-bottom:10px;line-height:1.6;letter-spacing:.3px}
|
||||||
|
.set-section{margin-bottom:20px}
|
||||||
|
.set-hd{font-size:10px;letter-spacing:2px;text-transform:uppercase;color:var(--dim);font-weight:700;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid var(--border)}
|
||||||
|
.set-row{display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.04)}
|
||||||
|
.set-lbl{font-size:13px;font-weight:600;letter-spacing:.3px}
|
||||||
|
.set-sub{font-size:10px;color:var(--dim);margin-top:2px;letter-spacing:.3px}
|
||||||
|
.tog{width:36px;height:20px;background:rgba(255,255,255,.1);border-radius:10px;position:relative;cursor:pointer;transition:.2s;flex-shrink:0;border:none}
|
||||||
|
.tog.on{background:var(--accent2)}
|
||||||
|
.tog::after{content:'';position:absolute;width:14px;height:14px;background:#fff;border-radius:50%;top:3px;left:3px;transition:.2s}
|
||||||
|
.tog.on::after{left:19px}
|
||||||
|
.set-vol{display:flex;align-items:center;gap:8px;padding:8px 0}
|
||||||
|
.set-vol input[type=range]{flex:1;-webkit-appearance:none;height:3px;background:rgba(255,255,255,.15);border-radius:2px;outline:none}
|
||||||
|
.set-vol input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;background:var(--accent2);border-radius:50%;cursor:pointer}
|
||||||
|
.set-vol-lbl{font-size:11px;color:var(--dim);min-width:32px;text-align:right}
|
||||||
|
.set-key-row{display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid rgba(255,255,255,.03)}
|
||||||
|
.set-key{font-family:var(--font-b);font-size:11px;color:var(--dim);letter-spacing:.3px}
|
||||||
|
.set-kbd{font-family:monospace;font-size:11px;color:var(--accent2);background:rgba(0,212,255,.08);padding:1px 6px;border-radius:3px}
|
||||||
|
|
||||||
/* Empty state */
|
/* Empty state */
|
||||||
.empty{text-align:center;padding:36px 16px;color:var(--dim)}
|
.empty{text-align:center;padding:36px 16px;color:var(--dim)}
|
||||||
@@ -365,7 +381,8 @@ body.idle .gt,body.idle .gb,body.idle .gr,body.idle .gl{opacity:0;transition:opa
|
|||||||
<div class="sb-tabs">
|
<div class="sb-tabs">
|
||||||
<div class="stab act" data-tab="channels">MY LIST</div>
|
<div class="stab act" data-tab="channels">MY LIST</div>
|
||||||
<div class="stab" data-tab="search">SEARCH</div>
|
<div class="stab" data-tab="search">SEARCH</div>
|
||||||
<div class="stab" data-tab="add">ADD URL</div>
|
<div class="stab" data-tab="add">ADD</div>
|
||||||
|
<div class="stab" data-tab="settings">SETTINGS</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sb-body" id="sb-body"></div>
|
<div class="sb-body" id="sb-body"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -413,6 +430,8 @@ function sizePW(){
|
|||||||
|
|
||||||
function onPReady(){
|
function onPReady(){
|
||||||
A.ready=true; hideLoad(); sizePW();
|
A.ready=true; hideLoad(); sizePW();
|
||||||
|
const _vol=parseInt(localStorage.getItem('vf_volume')||'100');
|
||||||
|
try{ A.player.setVolume(_vol); }catch(e){}
|
||||||
// Popup: check for video transferred from main window
|
// Popup: check for video transferred from main window
|
||||||
if(window.opener){
|
if(window.opener){
|
||||||
try{
|
try{
|
||||||
@@ -630,6 +649,7 @@ function renderTab(tab){
|
|||||||
if(tab==='channels') renderChTab();
|
if(tab==='channels') renderChTab();
|
||||||
else if(tab==='search') renderSearchTab();
|
else if(tab==='search') renderSearchTab();
|
||||||
else if(tab==='add') renderAddTab();
|
else if(tab==='add') renderAddTab();
|
||||||
|
else if(tab==='settings') renderSettingsTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderChTab(){
|
function renderChTab(){
|
||||||
@@ -708,6 +728,74 @@ function renderAddTab(){
|
|||||||
setTimeout(()=>inp.focus(),50);
|
setTimeout(()=>inp.focus(),50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSettingsTab(){
|
||||||
|
const b=qs('#sb-body');
|
||||||
|
const shuf=A.shuffled;
|
||||||
|
const vol=parseInt(localStorage.getItem('vf_volume')||'100');
|
||||||
|
b.innerHTML=`
|
||||||
|
<div class="set-section">
|
||||||
|
<div class="set-hd">API KEY</div>
|
||||||
|
<div style="margin-bottom:8px">
|
||||||
|
<div class="sp-label" style="margin-bottom:6px">YouTube Data API v3 Key</div>
|
||||||
|
${A.apiKey?`<div style="font-size:11px;color:var(--dim);margin-bottom:8px">Current key: …${esc(A.apiKey.slice(-8))}</div>`:''}
|
||||||
|
<div class="s-row">
|
||||||
|
<input id="set-key-in" class="s-in" type="password" placeholder="${A.apiKey?'Enter new key to replace':'Paste API key…'}" autocomplete="off">
|
||||||
|
<button id="set-key-save" class="s-go">SAVE</button>
|
||||||
|
</div>
|
||||||
|
${A.apiKey?`<div style="margin-top:6px;text-align:right"><a style="font-size:10px;color:var(--dim);cursor:pointer;text-decoration:underline" id="set-key-reset">Clear key & restart</a></div>`:''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="set-section">
|
||||||
|
<div class="set-hd">PLAYBACK</div>
|
||||||
|
<div class="set-row">
|
||||||
|
<div><div class="set-lbl">Shuffle</div><div class="set-sub">Play channels in random order</div></div>
|
||||||
|
<button class="tog${shuf?' on':''}" id="tog-shuf"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="set-section">
|
||||||
|
<div class="set-hd">VOLUME</div>
|
||||||
|
<div class="set-vol">
|
||||||
|
<input type="range" min="0" max="100" value="${vol}" id="set-vol-in">
|
||||||
|
<span class="set-vol-lbl" id="set-vol-val">${vol}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="set-section">
|
||||||
|
<div class="set-hd">KEYBOARD SHORTCUTS</div>
|
||||||
|
<div class="set-key-row"><span class="set-key">Play / Pause</span><span class="set-kbd">Space</span></div>
|
||||||
|
<div class="set-key-row"><span class="set-key">Prev / Next video</span><span class="set-kbd">← →</span></div>
|
||||||
|
<div class="set-key-row"><span class="set-key">Volume</span><span class="set-kbd">↑ ↓</span></div>
|
||||||
|
<div class="set-key-row"><span class="set-key">Channel up / down</span><span class="set-kbd">+ -</span></div>
|
||||||
|
<div class="set-key-row"><span class="set-key">Shuffle</span><span class="set-kbd">S</span></div>
|
||||||
|
<div class="set-key-row"><span class="set-key">Channels panel</span><span class="set-kbd">C</span></div>
|
||||||
|
<div class="set-key-row"><span class="set-key">Pop-out player</span><span class="set-kbd">P</span></div>
|
||||||
|
<div class="set-key-row"><span class="set-key">Fullscreen</span><span class="set-kbd">F</span></div>
|
||||||
|
<div class="set-key-row"><span class="set-key">Close panel</span><span class="set-kbd">Esc</span></div>
|
||||||
|
</div>`;
|
||||||
|
const keyIn=qs('#set-key-in');
|
||||||
|
const doSaveKey=()=>{
|
||||||
|
const k=keyIn.value.trim(); if(!k){ toast('Enter a key'); return; }
|
||||||
|
A.apiKey=k; save(); toast('Key saved'); renderSettingsTab();
|
||||||
|
};
|
||||||
|
qs('#set-key-save').onclick=doSaveKey;
|
||||||
|
keyIn.onkeydown=e=>{ if(e.key==='Enter') doSaveKey(); };
|
||||||
|
const resetEl=qs('#set-key-reset');
|
||||||
|
if(resetEl) resetEl.onclick=()=>{ if(confirm('Clear API key and restart?')){ localStorage.removeItem('vf_key'); location.reload(); } };
|
||||||
|
qs('#tog-shuf').onclick=()=>{
|
||||||
|
A.shuffled=!A.shuffled;
|
||||||
|
qs('#b-shuf').classList.toggle('act',A.shuffled);
|
||||||
|
try{ A.player?.setShuffle?.(A.shuffled); }catch(e){}
|
||||||
|
localStorage.setItem('vf_shuffle',A.shuffled);
|
||||||
|
qs('#tog-shuf').classList.toggle('on',A.shuffled);
|
||||||
|
};
|
||||||
|
const volIn=qs('#set-vol-in'), volVal=qs('#set-vol-val');
|
||||||
|
volIn.oninput=()=>{
|
||||||
|
const v=parseInt(volIn.value);
|
||||||
|
volVal.textContent=v+'%';
|
||||||
|
try{ A.player?.setVolume?.(v); }catch(e){}
|
||||||
|
localStorage.setItem('vf_volume',v);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ── NCO onboarding ──
|
// ── NCO onboarding ──
|
||||||
function initNCO(){
|
function initNCO(){
|
||||||
const demo=A.demo&&!A.apiKey;
|
const demo=A.demo&&!A.apiKey;
|
||||||
@@ -805,6 +893,7 @@ function loadState(){
|
|||||||
A.apiKey=localStorage.getItem('vf_key')||'';
|
A.apiKey=localStorage.getItem('vf_key')||'';
|
||||||
try{ A.channels=JSON.parse(localStorage.getItem('vf_ch')||'[]'); }catch(e){ A.channels=[]; }
|
try{ A.channels=JSON.parse(localStorage.getItem('vf_ch')||'[]'); }catch(e){ A.channels=[]; }
|
||||||
A.cur=parseInt(localStorage.getItem('vf_cur')||'-1');
|
A.cur=parseInt(localStorage.getItem('vf_cur')||'-1');
|
||||||
|
A.shuffled = localStorage.getItem('vf_shuffle') !== 'false';
|
||||||
if(A.cur>=A.channels.length) A.cur=A.channels.length>0?0:-1;
|
if(A.cur>=A.channels.length) A.cur=A.channels.length>0?0:-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -868,9 +957,7 @@ function wireControls(){
|
|||||||
if(!document.fullscreenElement) document.documentElement.requestFullscreen?.().catch(()=>{});
|
if(!document.fullscreenElement) document.documentElement.requestFullscreen?.().catch(()=>{});
|
||||||
else document.exitFullscreen?.();
|
else document.exitFullscreen?.();
|
||||||
};
|
};
|
||||||
qs('#cfg-btn').onclick=()=>{
|
qs('#cfg-btn').onclick=()=>{ openSB(); renderTab('settings'); };
|
||||||
if(confirm('Change API key? App will reload.')){ localStorage.removeItem('vf_key'); location.reload(); }
|
|
||||||
};
|
|
||||||
qs('#pb-wrap').onclick=e=>{
|
qs('#pb-wrap').onclick=e=>{
|
||||||
try{
|
try{
|
||||||
if(!A.player?.getDuration) return;
|
if(!A.player?.getDuration) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user