Fix ticker clipping, remove gear icon, add ENDED fallback, persist shuffle, add API key guide modal
- Remove overflow:hidden from .bb-top; give .np-wrap explicit 26px height to stop ticker text being vertically clipped - Remove redundant gear icon from top bar (settings accessible via hamburger menu) - ENDED state: try nextVideo(), fallback to playVideoAt(0) after 1.2s so last-video playlists loop - Persist shuffle state to localStorage from both the toolbar button and S keyboard shortcut - Replace setup hint with direct YouTube API link + "What's an API key?" modal with 4-step guide for first-time users Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
+45
-9
@@ -118,11 +118,11 @@ html,body{width:100%;height:100%;overflow:hidden;background:var(--bg);font-famil
|
||||
|
||||
/* Bottom bar */
|
||||
#bb{position:absolute;bottom:0;left:0;right:0;height:var(--bot-h);z-index:5;display:flex;flex-direction:column;justify-content:center;align-items:stretch;padding:5px 14px;gap:2px}
|
||||
.bb-top{display:flex;align-items:center;gap:8px;overflow:hidden}
|
||||
.bb-top{display:flex;align-items:center;gap:8px;}
|
||||
.bb-bot{display:flex;justify-content:center;align-items:center}
|
||||
.np-lbl{font-family:var(--font-d);font-size:12px;letter-spacing:2.5px;color:var(--accent);flex-shrink:0;display:flex;align-items:center;gap:5px;white-space:nowrap}
|
||||
.np-lbl-dot{width:5px;height:5px;background:var(--accent);border-radius:50%;animation:dot-p 1s ease-in-out infinite}
|
||||
.np-wrap{flex:1;overflow:hidden;height:100%;display:flex;align-items:center;position:relative}
|
||||
.np-wrap{flex:1;overflow:hidden;height:26px;position:relative}
|
||||
#np-ticker{white-space:nowrap;font-size:15px;font-weight:600;letter-spacing:.4px;will-change:transform;position:absolute;left:0;top:50%;transform-origin:left center;margin-top:-10px}
|
||||
.np-art{color:var(--accent2)}
|
||||
.ctrls{display:flex;align-items:center;gap:3px}
|
||||
@@ -284,9 +284,9 @@ body.idle .gt,body.idle .gb,body.idle .gr,body.idle .gl{opacity:0;transition:opa
|
||||
<div class="sp-label">YouTube Data API v3 Key</div>
|
||||
<input id="api-in" class="sp-input" type="text" placeholder="AIzaSy..." autocomplete="off" spellcheck="false">
|
||||
<div class="sp-hint">
|
||||
Get a free key at <a href="https://console.cloud.google.com/" target="_blank">Google Cloud Console</a> →
|
||||
Enable <strong>YouTube Data API v3</strong> → Credentials → Create API Key.<br>
|
||||
No Google login needed to watch videos.
|
||||
A free YouTube API key lets VidFlow search playlists.<br>
|
||||
<a href="https://console.cloud.google.com/marketplace/product/google/youtube.googleapis.com" target="_blank" rel="noopener">Get your key here</a> —
|
||||
<a id="api-guide-btn" style="cursor:pointer;color:var(--accent2);text-decoration:underline">What's an API key?</a>
|
||||
</div>
|
||||
<button class="btn-p" id="setup-btn">ENTER VIDFLOW</button>
|
||||
<div class="sp-err" id="sp-err"></div>
|
||||
@@ -294,6 +294,26 @@ body.idle .gt,body.idle .gb,body.idle .gr,body.idle .gl{opacity:0;transition:opa
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="api-guide" style="display:none;position:fixed;inset:0;z-index:1001;align-items:center;justify-content:center;background:rgba(0,0,0,.82)">
|
||||
<div class="sp" style="max-height:88vh;overflow-y:auto;position:relative">
|
||||
<button id="api-guide-close" style="position:absolute;top:14px;right:16px;background:transparent;border:none;color:var(--dim);font-size:20px;cursor:pointer;line-height:1">✕</button>
|
||||
<div class="sp-logo" style="font-size:36px;margin-bottom:6px">API KEY GUIDE</div>
|
||||
<div class="sp-sub" style="margin-bottom:20px">Getting your free YouTube key</div>
|
||||
<div class="sp-label">What is an API key?</div>
|
||||
<p class="sp-hint">An API key is a short code that lets VidFlow talk to YouTube on your behalf so it can search for playlists. It is free, takes about 60 seconds to create, and you only ever need one.</p>
|
||||
<div class="sp-label">Step 1 — Open the YouTube API page</div>
|
||||
<p class="sp-hint">Click the link below and sign in with any Google account (Gmail works fine).</p>
|
||||
<p style="margin-bottom:16px"><a href="https://console.cloud.google.com/marketplace/product/google/youtube.googleapis.com" target="_blank" rel="noopener" style="color:var(--accent2);font-size:12px;font-weight:700;letter-spacing:.5px">⧉ YouTube Data API v3 on Google Cloud</a></p>
|
||||
<div class="sp-label">Step 2 — Enable the API</div>
|
||||
<p class="sp-hint">Click the blue <strong>Enable</strong> button. If asked to create a project, click <strong>Create Project</strong> and accept the defaults.</p>
|
||||
<div class="sp-label">Step 3 — Create a key</div>
|
||||
<p class="sp-hint">In the left sidebar click <strong>Credentials</strong>. Then click <strong>Create Credentials</strong> at the top and choose <strong>API Key</strong>. Google will generate a key instantly.</p>
|
||||
<div class="sp-label">Step 4 — Copy & paste</div>
|
||||
<p class="sp-hint" style="margin-bottom:20px">Click <strong>Copy</strong> on the key that appears, then paste it into the VidFlow setup screen and press Enter VidFlow.</p>
|
||||
<button class="btn-p" id="api-guide-close2">GOT IT</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── APP ── -->
|
||||
<div id="app">
|
||||
<div id="pw"><div id="yt-player"></div><div id="pw-cover"></div></div>
|
||||
@@ -342,7 +362,6 @@ body.idle .gt,body.idle .gb,body.idle .gr,body.idle .gl{opacity:0;transition:opa
|
||||
<div style="display:flex;gap:4px;flex-shrink:0">
|
||||
<button class="icon-btn" id="po-btn" title="Pop-out player (P)">⧉</button>
|
||||
<button class="icon-btn" id="fs-btn" title="Fullscreen (F)">⛶</button>
|
||||
<button class="icon-btn" id="cfg-btn" title="Settings / Change API Key">⚙</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -460,7 +479,12 @@ function onPState(e){
|
||||
// browser blocked autoplay — prompt user to tap
|
||||
pb.textContent='▶'; A.playing=false; showTap();
|
||||
} else if(e.data===S.ENDED){
|
||||
A.player?.nextVideo?.();
|
||||
try{ A.player.nextVideo(); }catch(e){}
|
||||
setTimeout(()=>{
|
||||
try{
|
||||
if(A.player?.getPlayerState?.()===YT.PlayerState.ENDED) A.player.playVideoAt(0);
|
||||
}catch(e){}
|
||||
}, 1200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -882,6 +906,16 @@ const closeSB=()=>qs('#sb').classList.remove('on');
|
||||
|
||||
let toastT;
|
||||
function toast(msg){ const t=qs('#toast'); t.textContent=msg; t.classList.add('on'); clearTimeout(toastT); toastT=setTimeout(()=>t.classList.remove('on'),2800); }
|
||||
function initApiGuide(){
|
||||
const guide=qs('#api-guide');
|
||||
const show=()=>{ guide.style.display='flex'; };
|
||||
const hide=()=>{ guide.style.display='none'; };
|
||||
const btn=qs('#api-guide-btn');
|
||||
if(btn) btn.onclick=show;
|
||||
qs('#api-guide-close').onclick=hide;
|
||||
qs('#api-guide-close2').onclick=hide;
|
||||
guide.onclick=e=>{ if(e.target===guide) hide(); };
|
||||
}
|
||||
|
||||
// ── Persist ──
|
||||
function save(){
|
||||
@@ -941,6 +975,7 @@ function wireControls(){
|
||||
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);
|
||||
toast(A.shuffled?'\u21cc Shuffle ON':'Shuffle OFF');
|
||||
};
|
||||
qs('#b-sb').onclick=openSB;
|
||||
@@ -957,7 +992,6 @@ function wireControls(){
|
||||
if(!document.fullscreenElement) document.documentElement.requestFullscreen?.().catch(()=>{});
|
||||
else document.exitFullscreen?.();
|
||||
};
|
||||
qs('#cfg-btn').onclick=()=>{ openSB(); renderTab('settings'); };
|
||||
qs('#pb-wrap').onclick=e=>{
|
||||
try{
|
||||
if(!A.player?.getDuration) return;
|
||||
@@ -976,7 +1010,7 @@ function wireControls(){
|
||||
document.addEventListener('click',e=>{
|
||||
if(!document.contains(e.target)) return; // guard: element may have been removed by innerHTML replacement
|
||||
const sb=qs('#sb');
|
||||
if(sb.classList.contains('on')&&!sb.contains(e.target)&&!qs('#b-sb').contains(e.target)&&!e.target.closest('#add-ch-btn')&&!e.target.closest('#cfg-btn')) closeSB();
|
||||
if(sb.classList.contains('on')&&!sb.contains(e.target)&&!qs('#b-sb').contains(e.target)&&!e.target.closest('#add-ch-btn')) closeSB();
|
||||
});
|
||||
document.addEventListener('keydown',e=>{
|
||||
if(['INPUT','TEXTAREA'].includes(e.target.tagName)) return;
|
||||
@@ -1005,6 +1039,7 @@ function wireControls(){
|
||||
A.shuffled=!A.shuffled;
|
||||
qs('#b-shuf').classList.toggle('act',A.shuffled);
|
||||
try{A.player?.setShuffle?.(A.shuffled);}catch(err){}
|
||||
localStorage.setItem('vf_shuffle', A.shuffled);
|
||||
toast(A.shuffled?'⇌ Shuffle ON':'Shuffle OFF');
|
||||
} else if(e.code==='KeyC'){ openSB();
|
||||
} else if(e.code==='KeyP'){ window.opener ? window.close() : popOut();
|
||||
@@ -1111,6 +1146,7 @@ if(!window.opener){
|
||||
loadState();
|
||||
wireControls();
|
||||
initSetup();
|
||||
initApiGuide();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user