557 lines
30 KiB
HTML
Executable File
557 lines
30 KiB
HTML
Executable File
<!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 https://fonts.gstatic.com data:; script-src 'self'; connect-src 'self' https: ipc: http://ipc.localhost blob:; worker-src 'self';"
|
||
/>
|
||
<title>YT Player</title>
|
||
<link rel="manifest" href="/manifest.webmanifest" />
|
||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||
<link
|
||
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600;12..96,700;12..96,800&family=Hanken+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap"
|
||
rel="stylesheet"
|
||
/>
|
||
<link rel="stylesheet" href="styles.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">🔍 Search</button>
|
||
<button class="nav-item" data-view="queue">▶ Queue <span id="navQueueCount" class="nav-badge hidden">0</span></button>
|
||
<button class="nav-item" data-view="history">🕘 History</button>
|
||
<button class="nav-item" data-view="saved">💾 Saved</button>
|
||
<button class="nav-item" data-view="downloads">⬇ Downloads <span id="navDlCount" class="nav-badge hidden">0</span></button>
|
||
<button class="nav-item" data-view="library">📁 Library</button>
|
||
<button class="nav-item" data-view="notes">📝 Notes</button>
|
||
<button class="nav-item" data-view="stats">📊 Stats</button>
|
||
<button class="nav-item" data-view="settings">⚙ Settings</button>
|
||
<button class="nav-item" id="navPartyBtn" type="button" title="Watch together with friends">🎉 Watch party</button>
|
||
<button class="nav-item" id="navRemoteBtn" type="button" title="Control this screen from a phone, or use this phone as a remote">📱 Remote</button>
|
||
</nav>
|
||
|
||
<div class="pl-header">
|
||
<span>Playlists</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"
|
||
type="text"
|
||
placeholder="Search YouTube… (no login, no ads)"
|
||
autocomplete="off"
|
||
/>
|
||
<button type="submit" class="search-btn">Search</button>
|
||
</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>
|
||
<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, no tracking. Search, play, and keep playlists right on your device.</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>
|
||
</div>
|
||
<div id="bufferSpinner" class="spinner hidden"></div>
|
||
<!-- Touch gesture feedback (Gestures in app.js) -->
|
||
<div id="gestureHud" class="gesture-hud hidden"></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" 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>
|
||
</div>
|
||
<button id="waveLoud" class="wave-loud hidden" type="button">⚡ Jump to the loudest part</button>
|
||
<div class="btn-row">
|
||
<button id="prevBtn" class="ctrl" title="Previous">⏮</button>
|
||
<button id="playBtn" class="ctrl play" title="Play/Pause">▶</button>
|
||
<button id="nextBtn" class="ctrl" title="Next">⏭</button>
|
||
|
||
<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>
|
||
<button id="rememberPosBtn" class="ctrl" title="Remember playback position for this song (per playlist)">📍</button>
|
||
<div class="vol">
|
||
<button id="muteBtn" class="ctrl" title="Mute">🔊</button>
|
||
<input id="volume" type="range" min="0" max="1" step="0.01" value="1" />
|
||
</div>
|
||
|
||
<div class="spacer"></div>
|
||
|
||
<label class="sel">
|
||
Speed
|
||
<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">
|
||
Quality
|
||
<select id="qualitySelect"></select>
|
||
</label>
|
||
|
||
<button id="sleepTimerBtn" class="ctrl" title="Sleep timer">⏱</button>
|
||
<button id="shuffleBtn" class="ctrl" title="Shuffle upcoming tracks (S)">🔀</button>
|
||
<button id="loopBtn" class="ctrl" title="Loop current video (L)">🔂</button>
|
||
<button id="repeatBtn" class="ctrl" title="Repeat list when finished (R)">🔁</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>
|
||
<button id="fsBtn" class="ctrl" title="Fullscreen">⛶</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="sleepStatus" class="sleep-status hidden">
|
||
<span id="sleepCountdown" class="sleep-countdown"></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>
|
||
<div class="np-actions">
|
||
<button id="queueBtn" class="np-btn" title="Add to the temporary queue">+ Queue</button>
|
||
<button id="saveBtn" class="np-btn" title="Save this video for offline playback">⬇ Save</button>
|
||
<button id="editBtn" class="np-btn" title="Cut parts out and save a custom edited copy">✂ Edit & download</button>
|
||
<button id="addPlaylistBtn" class="np-btn" title="Add to a playlist">+ Playlist</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>
|
||
</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 & 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>
|
||
</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>
|
||
</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 & 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>
|
||
</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">
|
||
<h2 id="listTitle">Search</h2>
|
||
<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>
|
||
<div id="batchBar" class="batch-bar hidden">
|
||
<span id="batchCount" class="batch-count">0 selected</span>
|
||
<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="status" class="status hidden"></div>
|
||
<div id="cards" class="cards"></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 -->
|
||
<section id="serviceMode" class="service-mode hidden" aria-label="Service Mode">
|
||
<div class="service-topbar">
|
||
<div id="serviceWakeState" class="service-wake-state">Screen may sleep</div>
|
||
<div class="service-topbar-actions">
|
||
<button id="serviceAutoscrollBtn" class="service-exit-btn service-video-btn hidden" type="button"
|
||
aria-pressed="true" aria-label="Keep the sung line centred">⇅ Auto-scroll on</button>
|
||
<button id="serviceLyricsBtn" class="service-exit-btn service-video-btn" type="button"
|
||
aria-pressed="false" aria-label="Show only the lyrics">🎵 Lyrics off</button>
|
||
<button id="serviceVideoBtn" class="service-exit-btn service-video-btn" type="button"
|
||
aria-pressed="false" aria-label="Show or hide the video">📺 Video off</button>
|
||
<button id="serviceExitBtn" class="service-exit-btn" type="button" aria-label="Exit Service Mode">Exit</button>
|
||
</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>
|
||
<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 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>
|
||
<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>
|
||
<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>
|
||
<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>
|
||
<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>
|
||
<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>
|
||
<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>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="stats-core.js"></script>
|
||
<script src="async-guard.js"></script>
|
||
<script src="sw-update.js"></script>
|
||
<script src="app.js"></script>
|
||
</body>
|
||
</html>
|