Files
ytplayer/frontend/index.html
2026-10-03 17:56:01 +08:00

679 lines
46 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#1a1311" />
<!-- Build this page was served from — the server stamps it at request time;
app.js compares it with /api/version before offering an update. -->
<meta name="ytp-build" content="__BUILD_TAG__" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="YT Player" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; img-src 'self' https: data: asset: http://asset.localhost blob:; media-src 'self' https: blob: asset: http://asset.localhost; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; script-src 'self' 'wasm-unsafe-eval' https://esm.sh; connect-src 'self' https: ipc: http://ipc.localhost blob:; worker-src 'self';"
/>
<title>YT Player</title>
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="preload" href="fonts/BricolageGrotesque-latin.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="fonts/HankenGrotesk-latin.woff2" as="font" type="font/woff2" crossorigin />
<link rel="stylesheet" href="fonts/fonts.css" />
<link rel="preconnect" href="https://i.ytimg.com" crossorigin />
<link rel="stylesheet" href="styles.css" />
<link rel="stylesheet" href="service-ui.css" />
<link rel="stylesheet" href="refresh.css" />
<link rel="stylesheet" href="theme-glass.css" />
<link rel="stylesheet" href="theme-classic.css" />
<link rel="stylesheet" href="recommendations.css" />
<link rel="stylesheet" href="settings-sections.css" />
<link rel="stylesheet" href="lower-third.css" />
<link rel="stylesheet" href="car-mode.css" />
<link rel="stylesheet" href="offline-pages.css" />
<link rel="stylesheet" href="saved-page.css" />
</head>
<body>
<div class="app">
<!-- Sidebar -->
<aside id="sidebar" class="sidebar">
<div class="brand">
<span class="brand-mark">▶</span> YT Player
</div>
<nav class="nav">
<button class="nav-item active" data-view="search"><span class="legacy-nav-icon" aria-hidden="true">🔍</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 5 5"/></svg> Search</button>
<button class="nav-item" data-view="queue"><span class="legacy-nav-icon" aria-hidden="true">▶</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><path d="m8 5 11 7-11 7z"/></svg> Queue <span id="navQueueCount" class="nav-badge hidden">0</span></button>
<button class="nav-item" data-view="history"><span class="legacy-nav-icon" aria-hidden="true">🕘</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg> History</button>
<button class="nav-item" data-view="saved"><span class="legacy-nav-icon" aria-hidden="true">💾</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><path d="M5 3h12l4 4v14H3V3h2zm2 0v6h10V3M7 21v-8h10v8"/></svg> Saved</button>
<button class="nav-item" data-view="downloads"><span class="legacy-nav-icon" aria-hidden="true">⬇</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M4 16v5h16v-5"/></svg> Downloads <span id="navDlCount" class="nav-badge hidden">0</span></button>
<button class="nav-item" data-view="library"><span class="legacy-nav-icon" aria-hidden="true">📁</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><path d="M3 6h7l2 3h9v11H3z"/></svg> Library</button>
<button class="nav-item" data-view="notes"><span class="legacy-nav-icon" aria-hidden="true">📝</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H4v18h16V11M9 15l2-5 7-7 3 3-7 7z"/></svg> Notes</button>
<button class="nav-item" data-view="stats"><span class="legacy-nav-icon" aria-hidden="true">📊</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><path d="M4 21V11h4v10M10 21V3h4v18M16 21V7h4v14"/></svg> Stats</button>
<button class="nav-item" data-view="settings"><span class="legacy-nav-icon" aria-hidden="true">⚙</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="m9 3-1 3-3 1-2 3 2 2-1 3 2 3 3-1 2 3h3l1-3 3-1 2-3-2-2 1-3-2-3-3 1-2-3z"/></svg> Settings</button>
<button class="nav-item" id="navPartyBtn" type="button" title="Watch together with friends"><span class="legacy-nav-icon" aria-hidden="true">🎉</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><path d="m4 20 4-13 9 9zM14 3v4m4 1 3-2m-2 5 3 2"/></svg> <span class="nav-label">Watch party</span></button>
<button class="nav-item" id="navRemoteBtn" type="button" title="Control this screen from a phone, or use this phone as a remote"><span class="legacy-nav-icon" aria-hidden="true">📱</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><rect x="6" y="2" width="12" height="20" rx="2"/><path d="M10 18h4"/></svg> Remote</button>
</nav>
<div class="pl-header">
<span>Playlists</span>
<span id="syncSpinner" class="sync-spinner hidden" title="Syncing with your profile…" aria-live="polite" role="status"></span>
<button id="newPlaylistBtn" class="icon-btn" title="New playlist">+</button>
</div>
<div id="playlistList" class="playlist-list"></div>
<div class="pl-header smart-pl-header">
<span>Auto Playlists</span>
</div>
<div id="smartPlaylistList" class="playlist-list smart-pl-list"></div>
<div class="sidebar-footer">
<!-- Shown only when an online profile is linked (see updateProfileStatus) -->
<div id="sidebarProfile" class="sb-profile hidden">
<div class="sb-profile-id">
<span class="sb-profile-mark">👤</span>
<span id="sidebarProfileName" class="sb-profile-name"></span>
</div>
<div class="sb-profile-actions">
<button id="sidebarProfileCopy" class="sb-profile-btn" type="button" title="Copy profile link">⧉ Copy link</button>
<button id="sidebarProfileShare" class="sb-profile-btn" type="button" title="Share profile link">↗ Share</button>
</div>
</div>
<label class="switch">
<input type="checkbox" id="audioOnlyToggle" />
<span>Audio-only mode</span>
</label>
</div>
</aside>
<!-- Sidebar backdrop (mobile drawer) -->
<div id="sidebarBackdrop" class="sidebar-backdrop"></div>
<!-- Main -->
<main class="main">
<header class="topbar">
<button
id="sidebarToggle"
class="sidebar-toggle"
type="button"
aria-label="Toggle sidebar"
aria-controls="sidebar"
aria-expanded="false"
>☰</button>
<form id="searchForm" class="search-form">
<input
id="searchInput"
aria-label="Search YouTube"
type="text"
placeholder="Search YouTube… (no login, no ads)"
autocomplete="off"
/>
<!-- Shown after "Later" on the update dialog: one tap updates -->
<button id="updateIconBtn" type="button" class="update-icon-btn hidden" title="Update available — tap to update" aria-label="Update available — tap to update">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 12a8 8 0 1 1-2.34-5.66M20 4v4.5h-4.5"/><path d="M12 8v5M9.5 11l2.5 2.5 2.5-2.5"/></svg>
</button>
<button type="submit" class="search-btn">Search</button>
<div id="searchRecent" class="search-recent hidden" role="listbox" aria-label="Recent searches"></div>
</form>
</header>
<div class="body">
<!-- Player pane -->
<section id="playerPane" class="player-pane empty">
<div class="player-stage">
<video id="video" playsinline></video>
<audio id="audio"></audio>
<!-- Lyrics shown in place of the video (watch party / sing-along) -->
<div id="stageLyrics" class="stage-lyrics hidden">
<div id="stageLyricsList" class="sl-list"></div>
<div class="stage-lyrics-bar">
<button id="stageLyricsAuto" class="np-btn" type="button" aria-pressed="true">⇅ Auto-scroll on</button>
<button id="stageLyricsOff" class="np-btn" type="button">📺 Show video</button>
</div>
</div>
<div id="artFallback" class="art-fallback hidden">
<img id="artImg" alt="" />
</div>
<div id="playerPlaceholder" class="player-placeholder">
<div class="hero">
<div class="ph-logo">▶</div>
<h1 class="hero-title">YT Player</h1>
<p class="hero-tagline">Ad-free YouTube — no login needed. Search, play, and discover videos shaped by what you listen to.</p>
<div class="hero-pills">
<span class="hero-pill">No ads</span>
<span class="hero-pill">No sign-in</span>
<span class="hero-pill">Offline playlists</span>
</div>
<div class="hero-suggests">
<span class="hero-suggests-label">Try</span>
<button class="chip" data-q="lofi hip hop radio">lofi beats</button>
<button class="chip" data-q="live news">live news</button>
<button class="chip" data-q="relaxing music">relaxing music</button>
<button class="chip" data-q="podcast highlights">podcasts</button>
</div>
</div>
<!-- Replaces the hero once this device has playlists (renderHomePlaylists) -->
<div id="homePlaylists" class="home-pl hidden">
<div class="home-pl-head">
<span class="home-pl-title">Your playlists</span>
<button id="homePlNewBtn" class="home-pl-new" type="button">+ New</button>
</div>
<div id="homePlGrid" class="home-pl-grid"></div>
</div>
<section id="homeRecommendations" class="recommendations hidden" aria-label="Recommended videos"></section>
</div>
<div id="bufferSpinner" class="spinner hidden"></div>
<!-- Touch gesture feedback (Gestures in app.js) -->
<div id="gestureHud" class="gesture-hud hidden"></div>
<div id="stgOverlay" class="stg-overlay" aria-hidden="true">
<div class="stg-top">
<span class="stg-chip"><i class="stg-dot"></i><b id="stgQuality">Auto</b></span>
<div class="stg-seg" role="group" aria-label="Video or audio only">
<button id="stgVideo" type="button">Video</button>
<button id="stgAudio" type="button">Audio-only</button>
</div>
</div>
<div class="stg-mid">
<button id="stgBack" class="stg-skip" type="button" aria-label="Back 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12a8 8 0 1 0 3-6.2M4 4v4.5h4.5"/><text x="12" y="15.5" text-anchor="middle" font-size="7.5" font-weight="700" fill="currentColor" stroke="none">10</text></svg></button>
<button id="stgPlay" class="stg-play" type="button" aria-label="Play or pause"><svg viewBox="0 0 24 24" aria-hidden="true" class="fill"><path d="M9 5.5v13l10-6.5z"/></svg></button>
<button id="stgFwd" class="stg-skip" type="button" aria-label="Forward 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 12a8 8 0 1 1-3-6.2M20 4v4.5h-4.5"/><text x="12" y="15.5" text-anchor="middle" font-size="7.5" font-weight="700" fill="currentColor" stroke="none">10</text></svg></button>
</div>
<div class="stg-bot"><span class="stg-chip"><b id="stgZoom">Pinch 1.0×</b></span><span class="stg-chip"><i class="stg-dot"></i><b id="stgBuf">Buffered</b></span></div>
</div>
</div>
<div id="controls" class="controls hidden">
<div class="seek-row">
<span id="curTime" class="time">0:00</span>
<div class="seek-wrap">
<!-- Audio-only: loudness waveform drawn behind the transparent range input -->
<canvas id="waveCanvas" class="wave-canvas" aria-hidden="true"></canvas>
<span id="waveTip" class="wave-tip hidden"></span>
<input id="seek" aria-label="Playback position" class="seek" type="range" min="0" max="1000" value="0" />
<div id="seekMarkers" class="seek-markers" aria-hidden="true"></div>
<div id="abIndicator" class="ab-indicator hidden">
<span id="abALabel" class="ab-label">A: --</span>
<span style="flex:1"></span>
<span id="abBLabel" class="ab-label">B: --</span>
</div>
</div>
<span id="durTime" class="time">0:00</span>
<button id="chBadge" type="button" class="ch-badge" hidden title="Chapters — tap to jump" aria-haspopup="dialog"></button>
<button id="fsMini" class="fs-mini" type="button" title="Fullscreen" aria-label="Fullscreen"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg></button>
</div>
<button id="waveLoud" class="wave-loud hidden" type="button">⚡ Jump to the loudest part</button>
<div class="deck-main">
<button id="prevBtn" class="ctrl" title="Previous" aria-label="Previous"><svg viewBox="0 0 24 24" aria-hidden="true" class="fill"><rect x="5" y="5" width="2.8" height="14" rx="1.2"/><path d="M19 5.5v13L9.5 12z"/></svg></button>
<button id="back10Btn" class="ctrl skip10" type="button" title="Back 10 seconds" aria-label="Back 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12a8 8 0 1 0 3-6.2M4 4v4.5h4.5"/><text x="12" y="15.5" text-anchor="middle" font-size="7.5" font-weight="700" fill="currentColor" stroke="none">10</text></svg></button>
<button id="playBtn" class="ctrl play" title="Play/Pause" aria-label="Play or pause"><svg viewBox="0 0 24 24" aria-hidden="true" class="fill"><path d="M9 5.5v13l10-6.5z"/></svg></button>
<button id="fwd10Btn" class="ctrl skip10" type="button" title="Forward 10 seconds" aria-label="Forward 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 12a8 8 0 1 1-3-6.2M20 4v4.5h-4.5"/><text x="12" y="15.5" text-anchor="middle" font-size="7.5" font-weight="700" fill="currentColor" stroke="none">10</text></svg></button>
<button id="nextBtn" class="ctrl" title="Next" aria-label="Next"><svg viewBox="0 0 24 24" aria-hidden="true" class="fill"><rect x="16.2" y="5" width="2.8" height="14" rx="1.2"/><path d="M5 5.5v13l9.5-6.5z"/></svg></button>
<div id="dkModeHost" hidden><button id="shuffleBtn" class="ctrl" title="Shuffle upcoming tracks (S)" aria-label="Shuffle"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 3h5v5M4 20 21 3M21 16v5h-5M15 15l6 6M4 4l5 5"/></svg></button><button id="repeatBtn" class="ctrl" title="Repeat list when finished (R)" aria-label="Repeat list"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M17 2l4 4-4 4M3 12v-2a4 4 0 0 1 4-4h14M7 22l-4-4 4-4M21 12v2a4 4 0 0 1-4 4H3"/></svg></button><button id="loopBtn" class="ctrl" title="Loop this video (L)"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M17 2l4 4-4 4M3 12v-2a4 4 0 0 1 4-4h14M7 22l-4-4 4-4M21 12v2a4 4 0 0 1-4 4H3M11 10.2l1.6-.9v5.4"/></svg></button></div>
</div>
<div class="deck-vol vol">
<div class="vol-pod">
<input id="volume" type="range" min="0" max="1" step="0.01" value="1" aria-label="Volume" />
<button id="muteBtn" class="ctrl" title="Mute" aria-label="Mute"></button>
<span id="volPct" class="vol-pct">100%</span>
</div>
<button id="modeBtn" class="ctrl" type="button" title="Playback mode" aria-label="Playback mode"></button>
</div>
<div class="dk-quick">
<button id="fsBtn" class="ctrl" title="Fullscreen" aria-label="Fullscreen"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg><span class="fs-label">Fullscreen</span></button>
<div class="ab-group">
<button id="abABtn" class="ctrl ab-ctrl" title="Set A-B loop start [A]">A</button>
<button id="abBBtn" class="ctrl ab-ctrl" title="Set A-B loop end [B]">B</button>
<button id="abClearBtn" class="ctrl ab-ctrl hidden" title="Clear A-B loop">↺</button>
</div>
<button id="ctlMoreBtn" class="dk-more" type="button" aria-haspopup="dialog" title="More controls" aria-label="More controls"><span><svg class="mc-tune" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h9M17 7h3M4 17h3M11 17h9M13 4v6M7 14v6"/></svg><em class="mc-dots">⋯</em><i class="mc-label"> More<i class="mc-rest"> controls</i></i></span><small>Speed · Quality · A-B · Sleep · EQ</small></button>
</div>
<div id="deckChips" class="deck-chips" aria-label="Quick settings"></div>
<div id="dkMoreHost" hidden>
<button id="rememberPosBtn" class="ctrl" title="Remember playback position for this song (per playlist)">📍</button>
<button id="sleepTimerBtn" class="ctrl" title="Sleep timer">⏱</button>
<button id="transitionBtn" class="ctrl" title="Between songs in a playlist: Normal — click to change">↦</button>
<button id="eqBtn" class="ctrl" title="Equalizer (E)">🎚</button>
<button id="pipBtn" class="ctrl hidden" title="Picture-in-picture (I)">⧉</button>
<label class="sel dk-sel">
<span>Speed</span>
<select id="speedSelect">
<option value="0.5">0.5×</option>
<option value="0.75">0.75×</option>
<option value="1" selected>1×</option>
<option value="1.25">1.25×</option>
<option value="1.5">1.5×</option>
<option value="2">2×</option>
</select>
</label>
<label class="sel dk-sel">
<span>Quality</span>
<select id="qualitySelect"></select>
</label>
</div>
</div>
<div id="sleepStatus" class="sleep-status hidden">
<span class="sl-ic" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M20 14.5A8.5 8.5 0 0 1 9.5 4a8.5 8.5 0 1 0 10.5 10.5z"/></svg></span>
<span class="sl-txt"><span id="sleepCountdown" class="sleep-countdown"></span><small class="sl-sub">Audio fades out smoothly</small></span>
<button id="sleepCancelBtn" class="sleep-cancel">✕ Cancel</button>
</div>
<div id="nowPlayingMeta" class="now-meta hidden">
<div class="np-text">
<div class="np-title" id="npTitle"></div>
<div class="np-channel" id="npChannel"></div>
<div class="np-avail hidden" id="npAvail" aria-live="polite"></div>
</div>
<div class="np-actions">
<button id="addBtn" class="np-btn" title="Add to the queue or to a playlist"><svg class="ti" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h10M4 11h10M4 16h6M17 13v8M13 17h8"/></svg><span class="tl">Add</span></button>
<button id="saveBtn" class="np-btn" title="Save this video for offline playback"><svg class="ti" viewBox="0 0 24 24" aria-hidden="true"><path class="i-save" d="M12 4v11M7 11l5 5 5-5M5 20h14"/><path class="i-done" d="M20 12a8 8 0 1 1-16 0 8 8 0 0 1 16 0zM8.5 12.2l2.4 2.4 4.6-4.8"/></svg><span class="tl">Save</span></button>
<button id="exportBtn" class="np-btn" title="Save this video to your gallery, Files app or Downloads folder — works while streaming too"><svg class="ti" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="3"/><circle cx="9" cy="10.5" r="1.6"/><path d="M4 17l5-4.5 4 3.5 3-2.5 4 3.5"/></svg><span class="tl">Gallery</span></button>
<button id="serviceBtn" class="np-btn" title="Service mode — set-list view with the screen kept awake"><svg class="ti" viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21"/></svg><span class="tl">Service</span></button>
<div id="npMoreHost" class="np-more-host" hidden>
<button id="queueBtn" class="np-btn" title="Add to the temporary queue">+ Queue</button>
<button id="addPlaylistBtn" class="np-btn" title="Add to a playlist">+ Playlist</button>
<button id="editBtn" class="np-btn" title="Cut parts out and save a custom edited copy">✂ Edit &amp; download</button>
<button id="shareMomentBtn" class="np-btn" title="Share a link or GIF of this exact moment">🔗 Share moment</button>
<button id="externalBtn" class="np-btn" title="Play this in VLC, MX Player or another app">📺 Open in…</button>
<button id="brokenBtn" class="np-btn" title="Video won't play or looks broken? Re-download the server's copy" hidden>⚠ Broken</button>
</div>
<button id="moreBtn" class="np-more" type="button" aria-haspopup="dialog"><span>⋯ More actions</span><small>Edit &amp; download · Share moment · Open in… · Report broken</small></button>
</div>
</div>
<!-- Lyrics · chapters · bookmarks (shared notes, notes.js on the server) -->
<div id="notesPanel" class="notes-panel hidden">
<div class="notes-tabs" role="tablist">
<button class="notes-tab active" data-tab="lyrics" role="tab" type="button">Lyrics</button>
<button class="notes-tab" data-tab="markers" role="tab" type="button">Chapters &amp; bookmarks <span id="markersCount" class="notes-count"></span></button>
<button class="notes-tab" data-tab="search" role="tab" type="button">🔎 Search</button>
<span id="notesStatus" class="notes-status"></span>
<button id="notesFoldBtn" class="notes-fold" type="button" aria-expanded="true" title="Fold this card">▾</button>
</div>
<div id="lyricsPane" class="notes-pane">
<div class="lyrics-toolbar">
<button id="lyricsEditBtn" class="np-btn" type="button" title="Edit the shared lyrics">✎ Edit</button>
<button id="lyricsSyncBtn" class="np-btn" type="button" title="Tap along to time each line">⏱ Tap-sync</button>
<button id="lyricsHistoryBtn" class="np-btn" type="button" title="Earlier versions">🕘 History</button>
<span class="notes-spacer"></span>
<span class="lyr-offset" title="Your personal sync offset for this song — positive shows lyrics later">
<button id="lyrOffMinus" class="lyr-off-btn" type="button">−0.5s</button>
<span id="lyrOffVal" class="lyr-off-val">0.0s</span>
<button id="lyrOffPlus" class="lyr-off-btn" type="button">+0.5s</button>
</span>
<label class="lyr-auto"><input type="checkbox" id="lyrAutoscroll" checked /> Autoscroll</label>
<button id="lyricsFlagBtn" class="lyr-flag hidden" type="button" aria-pressed="false" title="Mark a line with wrong words or timing — an admin reviews it"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 21V4M5 4h11l-2 4 2 4H5"/></svg><span>Report a wrong line</span></button>
</div>
<div id="lyricsSyncBar" class="lyrics-syncbar hidden">
<span id="lyricsSyncHint" class="lyrics-sync-hint"></span>
<button id="lyricsStampBtn" class="np-btn primary-btn" type="button">Stamp line ⏎</button>
<button id="lyricsUndoBtn" class="np-btn" type="button">↶ Undo</button>
<button id="lyricsSyncCancel" class="np-btn" type="button">Cancel</button>
<button id="lyricsSyncDone" class="np-btn" type="button">✓ Save timings</button>
</div>
<div id="lyricsTags" class="lyrics-tags"></div>
<div id="lyricsList" class="lyrics-list"></div>
</div>
<div id="searchPane" class="notes-pane hidden">
<div class="ts-bar">
<input id="tsQuery" class="ts-query" type="search" placeholder="Search what's said or sung…" autocomplete="off" />
<select id="tsSource" class="ts-source" aria-label="Search in">
<option value="lyrics">Lyrics</option>
<option value="captions">YouTube transcript</option>
</select>
</div>
<div id="tsInfo" class="ts-info"></div>
<div id="tsResults" class="ts-results"></div>
</div>
<div id="markersPane" class="notes-pane hidden">
<div id="markerChips" class="marker-chips"></div>
<div class="markers-toolbar">
<button id="addChapterBtn" class="np-btn" type="button">+ Chapter</button>
<button id="addBookmarkBtn" class="np-btn" type="button" title="Pin a private bookmark here [P]">📌 Bookmark</button>
<button id="addNoteBtn" class="np-btn" type="button" title="Write a note tied to this moment [N]">📝 Note</button>
<span class="notes-spacer"></span>
<button id="chaptersHistoryBtn" class="np-btn" type="button" title="Earlier versions of the shared chapters">🕘</button>
<button id="exportMarkersBtn" class="np-btn" type="button">⤓ Export</button>
</div>
<div class="markers-section">
<div class="markers-heading">Chapters <span class="markers-scope">shared with everyone</span></div>
<div id="chapterList" class="marker-list"></div>
</div>
<div class="markers-section">
<div class="markers-heading">My notes &amp; bookmarks <span class="markers-scope">only you</span></div>
<div id="bookmarkList" class="marker-list"></div>
</div>
</div>
</div>
<!-- Up next queue -->
<div id="upnext" class="upnext hidden">
<div class="upnext-header">
<span>Up next</span>
<span id="upnextCount" class="upnext-count">0</span>
<button id="upnextClear" class="upnext-clear" type="button">Clear</button>
</div>
<div id="upnextList" class="upnext-list"></div>
</div>
<!-- Related videos -->
<div id="relatedPanel" class="related-panel hidden">
<div class="related-header">
<span class="related-label">Related</span>
<button id="relatedToggleBtn" class="related-toggle-btn">−</button>
</div>
<div id="relatedList" class="related-list"></div>
</div>
</section>
<!-- List pane -->
<section id="listPane" class="list-pane">
<div class="list-header">
<div class="list-title-row">
<h2 id="listTitle">Search</h2>
<div id="listTitleActions" class="list-title-actions"></div>
</div>
<p id="listSub" class="list-sub"></p>
<div id="listActions" class="list-actions"></div>
</div>
<div id="listFilterBar" class="list-filter-bar hidden">
<input id="listFilterInput" type="text" placeholder="Filter…" autocomplete="off" />
<div id="listFilterActions" class="list-filter-actions"></div>
</div>
<div id="batchBar" class="batch-bar hidden">
<span id="batchCount" class="batch-count">0 selected</span>
<button id="batchAllBtn" class="batch-btn">Select all</button>
<button id="batchQueueBtn" class="batch-btn">+ Queue</button>
<button id="batchAddPlaylistBtn" class="batch-btn">+ Playlist</button>
<button id="batchDeleteBtn" class="batch-btn batch-danger">Delete</button>
<button id="batchCancelBtn" class="batch-btn">Cancel</button>
</div>
<div id="searchMeta" class="search-meta hidden"></div>
<div id="status" class="status hidden"></div>
<div id="cards" class="cards"></div>
<div id="p2pStrip" class="p2p-strip hidden"></div>
</section>
</div>
</main>
</div>
<!-- Generic modal -->
<div id="modal" class="modal-backdrop hidden">
<div class="modal">
<h3 id="modalTitle"></h3>
<div id="modalBody"></div>
<div class="modal-actions" id="modalActions"></div>
</div>
</div>
<!-- Service mode (set-list view) overlay -->
<!-- Mobile: floating rail that jumps between the cards on this page and
follows the scroll. Tapping the active icon folds that card. -->
<nav id="sectionRail" class="sec-rail hidden" aria-label="Jump to a section"></nav>
<section id="serviceMode" class="service-mode hidden" aria-label="Service Mode">
<div class="svc-top">
<div class="svc-head">
<div id="serviceWakeState" class="svc-wake" role="status">Screen may sleep</div>
<button id="serviceExitBtn" class="svc-exit" type="button" aria-label="Exit Service Mode">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>Exit
</button>
</div>
<div class="svc-dock" role="toolbar" aria-label="Service view">
<button id="serviceViewBtn" class="svc-tile svc-view" type="button" aria-label="Service view: Default — tap to change">
<svg class="svc-ico" viewBox="0 0 24 24" aria-hidden="true"></svg>
<span class="svc-label">Default</span><i class="svc-led"></i>
</button>
<button id="serviceAutoscrollBtn" class="svc-tile hidden" type="button" aria-pressed="true" aria-label="Keep the sung line centred">
<svg class="svc-ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v16M7 9l5-5 5 5M7 15l5 5 5-5"/></svg>
<span class="svc-label">Auto-scroll</span><i class="svc-led"></i>
</button>
<button id="serviceFlagBtn" class="svc-tile svc-flag hidden" type="button" aria-pressed="false" aria-label="Report a wrong lyric line">
<svg class="svc-ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 21V4M5 4h11l-2 4 2 4H5"/></svg>
<span class="svc-label">Report</span><i class="svc-led"></i>
</button>
<div class="svc-font" role="group" aria-label="Lyrics text size">
<button id="svcFontDown" type="button" aria-label="Smaller text">A−</button>
<span id="svcFontVal" aria-live="polite">0</span>
<button id="svcFontUp" type="button" aria-label="Larger text">A+</button>
</div>
</div>
<div id="serviceFlagHint" class="svc-hint hidden" role="status">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 21V4M5 4h11l-2 4 2 4H5"/></svg>
<span>Tap the line that’s wrong</span>
</div>
</div>
<div class="service-center">
<div class="service-meta">
<h1 id="serviceTitle" class="service-title"></h1>
<div id="serviceChannel" class="service-channel"></div>
<div id="serviceNote" class="service-note hidden"></div>
</div>
<!-- Album art / visualizer views -->
<div id="serviceVisual" class="service-visual hidden" aria-hidden="true">
<img id="serviceArt" class="svc-art" alt="" decoding="async" />
<canvas id="serviceViz" class="svc-viz"></canvas>
</div>
<div id="serviceUpNext" class="service-upnext">Last song</div>
<!-- Lyrics-only view: the sung line is centred, largest and highlighted -->
<div id="serviceLyrics" class="service-lyrics hidden" aria-live="off">
<div id="serviceLyricsList" class="sl-list"></div>
</div>
</div>
<div class="service-bottom">
<div class="svc-progress" aria-hidden="true"><i id="serviceProgressFill"></i></div>
<div id="serviceTime" class="service-time">
<span id="serviceCurTime">0:00</span> / <span id="serviceDurTime">0:00</span>
</div>
<div class="service-controls">
<button id="servicePrevBtn" class="service-btn" type="button" aria-label="Previous track">⏮</button>
<button id="servicePlayBtn" class="service-btn service-btn-play" type="button" aria-label="Play or Pause">▶</button>
<button id="serviceNextBtn" class="service-btn" type="button" aria-label="Next track">⏭</button>
</div>
</div>
</section>
<!-- Sleep timer "screen off": black curtain; tap to wake -->
<div id="sleepCurtain" class="sleep-curtain hidden" role="button" aria-label="Wake">
<span id="sleepCurtainMsg">Sleep timer ended · tap to wake</span>
</div>
<div id="toastContainer" class="toast-container"></div>
<!-- Mini now-playing bar -->
<div id="miniBar" class="mini-bar hidden">
<div class="mini-bar-inner">
<button id="miniPlayBtn" class="mini-btn">▶</button>
<div class="mini-info">
<div id="miniTitle" class="mini-title"></div>
<div id="miniChannel" class="mini-channel"></div>
</div>
<span id="miniTime" class="mini-time">0:00</span>
<button id="miniServiceBtn" class="mini-btn mini-service" type="button"
title="Service mode" aria-label="Service mode">🎤</button>
<button id="miniCloseBtn" class="mini-btn mini-close">✕</button>
</div>
<div id="miniProgress" class="mini-progress"><div id="miniProgressFill" class="mini-progress-fill"></div></div>
</div>
<!-- YouTube-style bottom navigation bar (portrait PWA mode only) -->
<!-- Watch party panel (Party in app.js) -->
<aside id="partyPanel" class="party-panel hidden" aria-label="Watch party">
<div class="pt-head"><span id="ptTitle">🎉 Watch party</span><span id="ptStatus" class="pt-status"></span><button id="ptMin" class="rv-icon" type="button" aria-label="Minimise">▁</button></div>
<div class="pt-body"></div>
</aside>
<!-- Desktop: shown while phones are driving this screen (remote.js) -->
<button id="remoteChip" class="remote-chip hidden" type="button" title="Phone remote settings">
<span class="remote-chip-dot"></span><span id="remoteChipText">Remote connected</span>
</button>
<!-- Presenter / projector screen: lyrics of whatever the paired screen plays -->
<section id="presenterView" class="presenter-view hidden" aria-label="Presenter view">
<div id="pvList" class="sl-list pv-list"></div>
<div id="pvTitle" class="pv-title hidden"></div>
<div id="pvBar" class="pv-bar">
<span id="pvStatus" class="pv-status">Connecting…</span>
<span class="pv-hint">Double-click or F = fullscreen · B = blank screen</span>
<button id="pvFull" class="pv-btn" type="button">⛶ Fullscreen</button>
</div>
<div id="pvPair" class="pv-pair hidden">
<div class="pv-pair-title">Presenter screen</div>
<p>Enter the 6-digit code shown on the playing computer (📱 Remote → Control this screen → Presenter).</p>
<input id="pvCode" inputmode="numeric" maxlength="7" placeholder="000 000" />
<button id="pvPairBtn" class="rv-primary" type="button">Connect</button>
<div id="pvMsg" class="rv-pair-msg"></div>
</div>
<div id="pvBlank" class="pv-blank hidden"></div>
</section>
<!-- Phone: remote control for a desktop instance -->
<section id="remoteView" class="remote-view hidden" aria-label="Phone remote">
<div class="rv-top">
<button id="rvClose" class="rv-icon" type="button" aria-label="Close remote">✕</button>
<div class="rv-host">
<div id="rvHostName" class="rv-host-name">Remote</div>
<div id="rvStatus" class="rv-status">Not connected</div>
</div>
<button id="rvMenu" class="rv-icon" type="button" aria-label="Remote options">⋯</button>
</div>
<div id="rvPair" class="rv-pair">
<div class="rv-pair-title">Connect to a screen</div>
<p class="rv-pair-help">On the computer that plays to the TV or speakers, open <b>📱 Remote</b> → <b>Control from a phone</b>, then enter the 6-digit code shown there (or scan its QR code with your camera).</p>
<input id="rvCode" class="rv-code" inputmode="numeric" autocomplete="one-time-code" maxlength="7" placeholder="000 000" />
<input id="rvName" class="rv-name" maxlength="40" placeholder="This phone's name" />
<button id="rvPairBtn" class="rv-primary" type="button">Pair</button>
<div id="rvPairMsg" class="rv-pair-msg"></div>
</div>
<div id="rvControl" class="rv-control hidden">
<div class="rv-now">
<img id="rvThumb" class="rv-thumb" alt="" />
<div class="rv-now-text">
<div id="rvTitle" class="rv-title">Nothing playing</div>
<div id="rvChannel" class="rv-channel"></div>
</div>
</div>
<div class="rv-seek">
<input id="rvSeek" type="range" min="0" max="1000" value="0" />
<div class="rv-times"><span id="rvCur">0:00</span><span id="rvDur">0:00</span></div>
</div>
<div class="rv-transport">
<button id="rvPrev" class="rv-tbtn" type="button" aria-label="Previous">⏮</button>
<button id="rvToggle" class="rv-tbtn rv-play" type="button" aria-label="Play or pause">▶</button>
<button id="rvNext" class="rv-tbtn" type="button" aria-label="Next">⏭</button>
</div>
<div class="rv-volume">
<span>🔊</span><input id="rvVolume" type="range" min="0" max="100" value="100" /><span id="rvVolVal">100%</span>
</div>
<div class="rv-queue-head"><span>Up next</span><span id="rvQueueCount"></span></div>
<div id="rvQueue" class="rv-queue"></div>
<div class="rv-queue-hint">Tap a song to play it · drag ⠿ to reorder · ✕ removes</div>
<div class="rv-actions">
<button id="rvAdd" class="rv-action" type="button">+ Add to queue</button>
<button id="rvSleep" class="rv-action" type="button">⏱ Sleep</button>
<button id="rvService" class="rv-action" type="button">🎤 Service mode</button>
</div>
</div>
</section>
<nav id="bottomNav" class="bottom-nav" aria-label="Main navigation">
<button class="bottom-nav-btn active" data-view="search" aria-label="Search">
<span class="bottom-nav-icon"><span class="legacy-nav-icon" aria-hidden="true">🔍</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 5 5"/></svg></span>
<span class="bottom-nav-label">Search</span>
</button>
<button class="bottom-nav-btn" data-view="queue" aria-label="Queue">
<span class="bottom-nav-icon"><span class="legacy-nav-icon" aria-hidden="true">▶</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><path d="m8 5 11 7-11 7z"/></svg></span>
<span class="bottom-nav-label">Queue</span>
<span id="bnQueueCount" class="bottom-nav-badge hidden">0</span>
</button>
<button class="bottom-nav-btn" data-view="history" aria-label="History">
<span class="bottom-nav-icon"><span class="legacy-nav-icon" aria-hidden="true">🕘</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg></span>
<span class="bottom-nav-label">History</span>
</button>
<button class="bottom-nav-btn" data-view="saved" aria-label="Saved">
<span class="bottom-nav-icon"><span class="legacy-nav-icon" aria-hidden="true">💾</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><path d="M5 3h12l4 4v14H3V3h2zm2 0v6h10V3M7 21v-8h10v8"/></svg></span>
<span class="bottom-nav-label">Saved</span>
</button>
<button class="bottom-nav-btn" data-view="downloads" aria-label="Downloads">
<span class="bottom-nav-icon"><span class="legacy-nav-icon" aria-hidden="true">⬇</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M4 16v5h16v-5"/></svg></span>
<span class="bottom-nav-label">Downloads</span>
<span id="bnDlCount" class="bottom-nav-badge hidden">0</span>
</button>
<button class="bottom-nav-btn" data-view="settings" aria-label="Settings">
<span class="bottom-nav-icon"><span class="legacy-nav-icon" aria-hidden="true">⚙</span><svg class="classic-nav-icon" hidden viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="m9 3-1 3-3 1-2 3 2 2-1 3 2 3 3-1 2 3h3l1-3 3-1 2-3-2-2 1-3-2-3-3 1-2-3z"/></svg></span>
<span class="bottom-nav-label">More</span>
</button>
</nav>
<!-- Keyboard shortcuts help overlay -->
<div id="shortcutHelp" class="shortcut-overlay hidden">
<div class="shortcut-panel">
<div class="shortcut-header">
<h2>Keyboard Shortcuts</h2>
<button id="shortcutClose" class="shortcut-close">✕</button>
</div>
<div class="shortcut-grid">
<div class="shortcut-row"><kbd>Space</kbd><span>Play / Pause</span></div>
<div class="shortcut-row"><kbd>←</kbd><kbd>→</kbd><span>Seek backward / forward 5s</span></div>
<div class="shortcut-row"><kbd>↑</kbd><kbd>↓</kbd><span>Volume up / down</span></div>
<div class="shortcut-row"><kbd>F</kbd><span>Toggle fullscreen</span></div>
<div class="shortcut-row"><kbd>M</kbd><span>Toggle mute</span></div>
<div class="shortcut-row"><kbd>S</kbd><span>Shuffle upcoming tracks</span></div>
<div class="shortcut-row"><kbd>L</kbd><span>Loop current video</span></div>
<div class="shortcut-row"><kbd>R</kbd><span>Repeat list when finished</span></div>
<div class="shortcut-row"><kbd>Q</kbd><span>Add current video to queue</span></div>
<div class="shortcut-row"><kbd>A</kbd><span>Set A-B loop start point</span></div>
<div class="shortcut-row"><kbd>B</kbd><span>Set A-B loop end point</span></div>
<div class="shortcut-row"><kbd>P</kbd><span>Pin a bookmark at the current time</span></div>
<div class="shortcut-row"><kbd>[</kbd><kbd>]</kbd><span>Previous / next chapter</span></div>
<div class="shortcut-row"><kbd>N</kbd><span>Write a note at this moment</span></div>
<div class="shortcut-row"><kbd>I</kbd><span>Picture-in-picture</span></div>
<div class="shortcut-row"><kbd>E</kbd><span>Equalizer</span></div>
<div class="shortcut-row"><kbd>?</kbd><span>Show this help</span></div>
<div class="shortcut-row"><kbd>Esc</kbd><span>Close overlay / cancel selection</span></div>
</div>
</div>
</div>
<script src="fingerprint.js"></script>
<script src="opfs.js"></script>
<script src="video-edit.js"></script>
<script src="lyrics-core.js"></script>
<script src="lyrics-display.js"></script>
<script src="flag-ui.js"></script>
<script src="export.js"></script>
<script src="stats-core.js"></script>
<script src="sha256-wasm.js"></script>
<script src="sha256.js"></script>
<script src="loudness-wasm.js"></script>
<script src="loudness.js"></script>
<script src="device-db.js"></script>
<script src="p2p-client.js"></script>
<script src="p2p-core.js"></script>
<script src="p2p-transfer.js"></script>
<script src="async-guard.js"></script>
<script src="sw-update.js"></script>
<script src="settings-sections.js"></script>
<script src="downloads.js"></script>
<script src="downloads-page.js"></script>
<script src="saved-page.js"></script>
<script src="server-backfill.js"></script>
<script src="party-dj.js"></script>
<script src="lyrics-window.js"></script>
<script src="midi.js"></script>
<script src="lower-third.js"></script>
<script src="vocal-reducer.js"></script>
<script src="eq-core.js"></script>
<script src="piano-core.js"></script>
<script src="piano.js"></script>
<script src="setlist-import.js"></script>
<script src="car-mode.js"></script>
<script src="app.js"></script>
</body>
</html>