Searches are kept in local storage and the five that best fit what is in
the box are offered under it: the ones it starts, then the ones it starts
a word of, then the ones it appears in anywhere, most recently searched
first. Walk them with the arrows, take one with enter or a tap, dismiss
with escape. The part that matched is picked out, and rows are built from
text nodes -- these strings are the reader's own and they are going back
on screen.
The box searches as you type, so there is no submit to record and storing
every pause would fill the list with the way to a query rather than the
query: a new entry swallows the ones it grew out of ("hot", "hote",
"hotel" leaves "hotel"), and going back to something shorter adds
nothing. Enter records straight away, since that is a reader saying they
meant it.
That turned up a bug the suggestions would have run into constantly: a
search started while the box's own last search was still out was dropped
-- loadVideos saw a load running and returned -- leaving the grid on the
previous query. Changing the result set now stops the load that is
running before starting the next one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPZFnNdHbPGDTqQUNiE4ZN
252 lines
13 KiB
HTML
252 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Jacuzzi</title>
|
|
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
|
<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=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,500;0,9..144,600;1,9..144,400&family=Sora:wght@400;500;600&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="static/css/style.css">
|
|
</head>
|
|
<body>
|
|
<div id="scroll-progress" class="scroll-progress" aria-hidden="true"></div>
|
|
<header class="top-bar">
|
|
<div class="logo">Jacuzzi</div>
|
|
<div class="search-container">
|
|
<input type="text" id="search-input" placeholder="Search videos..."
|
|
autocomplete="off" role="combobox" aria-expanded="false"
|
|
aria-controls="search-suggest" aria-autocomplete="list">
|
|
<button class="search-clear-btn" id="search-clear-btn" type="button" aria-label="Clear search" title="Clear search">✕</button>
|
|
<div id="search-suggest" class="search-suggest" role="listbox" aria-label="Recent searches" hidden></div>
|
|
</div>
|
|
<div class="actions">
|
|
<button class="icon-btn reload-toggle" id="reload-channel-btn" title="Reload Channel">
|
|
<img class="icon-svg" src="https://cdn.jsdelivr.net/npm/heroicons@2.0.13/24/outline/arrow-path.svg" alt="Reload">
|
|
</button>
|
|
<button class="icon-btn menu-toggle" onclick="toggleDrawer('menu')" title="Menu">
|
|
<img class="icon-svg" src="https://cdn.jsdelivr.net/npm/heroicons@2.0.13/24/outline/bars-3.svg" alt="Menu">
|
|
</button>
|
|
<button class="icon-btn settings-toggle" onclick="toggleDrawer('settings')" title="Settings">
|
|
<img class="icon-svg" src="https://cdn.jsdelivr.net/npm/heroicons@2.0.13/24/outline/wrench.svg" alt="Settings">
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<section id="favorites-bar" class="favorites-bar" aria-label="Favorites">
|
|
<div class="favorites-header">
|
|
<h3>Favorites</h3>
|
|
<div class="favorites-actions">
|
|
<select id="favorites-sort" class="favorites-sort" aria-label="Sort favorites"></select>
|
|
<button id="favorites-browse-btn" class="btn-secondary" type="button" aria-pressed="false">Browse all</button>
|
|
</div>
|
|
</div>
|
|
<div id="favorites-list" class="favorites-list"></div>
|
|
<div id="favorites-empty" class="favorites-empty">No favorites yet. Tap the heart on a video to save it here.</div>
|
|
</section>
|
|
|
|
<div class="sidebar-overlay" id="overlay" onclick="closeDrawers()"></div>
|
|
|
|
<aside id="drawer-menu" class="sidebar">
|
|
<div class="sidebar-header">
|
|
<h3>Menu</h3>
|
|
<button class="close-btn" onclick="closeDrawers()">✕</button>
|
|
</div>
|
|
<div class="sidebar-content">
|
|
<div class="sidebar-section">
|
|
<h4 class="sidebar-subtitle">Network</h4>
|
|
<div class="setting-item">
|
|
<label for="source-select">Source</label>
|
|
<select id="source-select"></select>
|
|
</div>
|
|
<div class="setting-item">
|
|
<label for="channel-picker-btn">Channel</label>
|
|
<button id="channel-picker-btn" class="channel-trigger" type="button" aria-haspopup="dialog" aria-expanded="false">
|
|
<span class="channel-mark" id="channel-trigger-mark">
|
|
<img class="channel-favicon" id="channel-trigger-icon" alt="" decoding="async">
|
|
</span>
|
|
<span class="channel-trigger-text">
|
|
<span class="channel-trigger-name" id="channel-trigger-name">No channel</span>
|
|
<span class="channel-trigger-note" id="channel-trigger-note"></span>
|
|
</span>
|
|
<span class="channel-trigger-caret" aria-hidden="true"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sidebar-section">
|
|
<h4 class="sidebar-subtitle">Filters</h4>
|
|
<div id="filters-container" class="filters-container"></div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<aside id="drawer-settings" class="sidebar">
|
|
<div class="sidebar-header">
|
|
<h3>Settings</h3>
|
|
<button class="close-btn" onclick="closeDrawers()">✕</button>
|
|
</div>
|
|
<div class="sidebar-content">
|
|
<div class="setting-item">
|
|
<label>Theme</label>
|
|
<select id="theme-select">
|
|
<option value="dark">Dark</option>
|
|
<option value="light">Light</option>
|
|
</select>
|
|
</div>
|
|
<div class="setting-item">
|
|
<label>Preferred Quality</label>
|
|
<select id="quality-select">
|
|
<option value="auto">Best Available</option>
|
|
<option value="2160">2160p</option>
|
|
<option value="1440">1440p</option>
|
|
<option value="1080">1080p</option>
|
|
<option value="720">720p</option>
|
|
<option value="480">480p</option>
|
|
<option value="360">360p</option>
|
|
</select>
|
|
</div>
|
|
<div class="setting-item">
|
|
<label for="density-select">Grid Density</label>
|
|
<select id="density-select">
|
|
<option value="comfortable">Comfortable</option>
|
|
<option value="compact">Compact</option>
|
|
</select>
|
|
</div>
|
|
<div class="setting-item">
|
|
<label for="card-size-range">Card Size</label>
|
|
<input type="range" id="card-size-range" min="0.7" max="1.5" step="0.1" value="1">
|
|
</div>
|
|
<div class="setting-item">
|
|
<label for="text-size-range">Text Size</label>
|
|
<input type="range" id="text-size-range" min="0.8" max="1.4" step="0.1" value="1">
|
|
</div>
|
|
<div class="setting-item">
|
|
<label for="feed-end-select">Reels: On Video End</label>
|
|
<select id="feed-end-select">
|
|
<option value="loop">Loop</option>
|
|
<option value="scroll">Scroll to Next</option>
|
|
</select>
|
|
</div>
|
|
<div class="setting-item setting-toggle">
|
|
<div class="setting-label-row">
|
|
<label for="favorites-toggle">Favorites Bar</label>
|
|
</div>
|
|
<label class="toggle">
|
|
<input type="checkbox" id="favorites-toggle">
|
|
<span class="toggle-track"></span>
|
|
</label>
|
|
</div>
|
|
<div class="sidebar-section">
|
|
<h4 class="sidebar-subtitle">Sources</h4>
|
|
<div class="setting-item">
|
|
<label for="source-input">Add Source URL</label>
|
|
<div class="input-row">
|
|
<input id="source-input" type="text" placeholder="https://example.com">
|
|
<button id="add-source-btn" class="btn-secondary" type="button">Add</button>
|
|
</div>
|
|
</div>
|
|
<div id="sources-list" class="sources-list"></div>
|
|
</div>
|
|
<div class="sidebar-section">
|
|
<h4 class="sidebar-subtitle">Hot Tub Backup</h4>
|
|
<div class="setting-item">
|
|
<label for="import-favorites-btn">Import Favorites</label>
|
|
<input id="import-favorites-file" type="file" accept=".sqlite3,.sqlite,.db" hidden>
|
|
<button id="import-favorites-btn" class="btn-secondary" type="button">Choose backup file…</button>
|
|
<p id="import-favorites-status" class="setting-note" role="status" aria-live="polite">
|
|
Reads the favorites out of an exported Hot Tub database. The file stays on this device.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<main id="video-grid" class="grid-container"></main>
|
|
<div id="sentinel"></div>
|
|
<button id="load-more-btn" class="load-more-btn" title="Load More">
|
|
<img class="icon-svg" src="https://cdn.jsdelivr.net/npm/heroicons@2.0.13/24/outline/chevron-down.svg" alt="Load More">
|
|
</button>
|
|
|
|
<div id="custom-player" class="custom-player" aria-hidden="true"></div>
|
|
|
|
<button id="mode-toggle-btn" class="mode-toggle-btn" type="button" title="Switch to Reels view" aria-pressed="false">
|
|
<img class="icon-svg" id="mode-toggle-icon" src="https://cdn.jsdelivr.net/npm/heroicons@2.0.13/24/outline/device-phone-mobile.svg" alt="Switch to Reels view">
|
|
</button>
|
|
|
|
<div id="feed-view" class="feed-view" aria-hidden="true">
|
|
<button id="feed-mute-btn" class="feed-mute-btn" type="button" title="Toggle mute">
|
|
<img class="icon-svg" id="feed-mute-icon" src="https://cdn.jsdelivr.net/npm/heroicons@2.0.13/24/outline/speaker-x-mark.svg" alt="Unmute">
|
|
</button>
|
|
<div id="feed-scroll" class="feed-scroll">
|
|
<div id="feed-top-spacer" class="feed-top-spacer"></div>
|
|
<div id="feed-sentinel" class="feed-sentinel"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="info-modal" class="info-modal" aria-hidden="true">
|
|
<div class="info-card" role="dialog" aria-modal="true" aria-labelledby="info-title">
|
|
<div class="info-actions">
|
|
<button id="info-copy" class="btn-secondary info-copy" type="button">Copy JSON</button>
|
|
<button id="info-close" class="info-close" type="button" aria-label="Close">✕</button>
|
|
</div>
|
|
<h3 id="info-title">Video Info</h3>
|
|
<div id="info-list" class="info-list"></div>
|
|
<div id="info-empty" class="info-empty">No additional info available.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="error-toast" class="error-toast" role="alert" aria-live="assertive">
|
|
<span id="error-toast-text"></span>
|
|
<button id="error-toast-close" type="button" aria-label="Close">✕</button>
|
|
</div>
|
|
|
|
<div id="update-banner" class="update-banner" role="status" aria-live="polite">
|
|
<span>A new version is available.</span>
|
|
<button id="update-banner-btn" type="button">Refresh</button>
|
|
</div>
|
|
|
|
<button id="back-to-top" class="back-to-top" type="button" title="Back to top" aria-label="Back to top">↑</button>
|
|
|
|
<div id="channel-picker" class="channel-picker" aria-hidden="true">
|
|
<div class="channel-picker-box" role="dialog" aria-modal="true" aria-labelledby="channel-picker-title">
|
|
<div class="channel-picker-head">
|
|
<h3 id="channel-picker-title">Channel</h3>
|
|
<button id="channel-picker-close" class="close-btn" type="button" aria-label="Close">✕</button>
|
|
</div>
|
|
<input id="channel-search" class="channel-search" type="text" placeholder="Search channels…"
|
|
autocomplete="off" spellcheck="false" aria-controls="channel-picker-list">
|
|
<div id="channel-picker-list" class="channel-picker-list" role="listbox" aria-label="Channels"></div>
|
|
<div id="channel-picker-empty" class="channel-picker-empty">No channel matches that.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="command-palette" class="command-palette" aria-hidden="true">
|
|
<div class="cmdk-box" role="dialog" aria-modal="true" aria-label="Command palette">
|
|
<input id="cmdk-input" class="cmdk-input" type="text" placeholder="Type a command or search… (⌘K)" autocomplete="off" spellcheck="false">
|
|
<div id="cmdk-list" class="cmdk-list" role="listbox"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="privacy-veil" class="privacy-veil" aria-hidden="true"></div>
|
|
|
|
<script src="static/js/state.js"></script>
|
|
<script src="static/js/marquee.js"></script>
|
|
<script src="static/js/storage.js"></script>
|
|
<script src="static/js/customPlayer.js"></script>
|
|
<script src="static/js/player.js"></script>
|
|
<script src="static/js/favorites.js"></script>
|
|
<script src="static/js/favoritesView.js"></script>
|
|
<script src="static/js/sqlite.js"></script>
|
|
<script src="static/js/hottubBackup.js"></script>
|
|
<script src="static/js/videos.js"></script>
|
|
<script src="static/js/feed.js"></script>
|
|
<script src="static/js/ui.js"></script>
|
|
<script src="static/js/version.js"></script>
|
|
<script src="static/js/enhance.js"></script>
|
|
<script src="static/js/privacy.js"></script>
|
|
<script src="static/js/main.js"></script>
|
|
</body>
|
|
</html>
|