Favorites now carry `favoriteDate`. Ones saved before this had no way of knowing when they were saved, so they are all stamped with the moment the client first reads them -- they sort together as one batch, at the point favorites learned to keep dates. An import brings the date the other client recorded instead, so a restored library keeps its history. The bar used to build a card per favorite, which an import of several hundred made an expensive way to open the app. It now renders a screenful and appends more as the strip is scrolled. "Browse all" turns the whole grid into favorites: the same cards, the same virtualized masonry, the same infinite scroll and reels mode as a channel listing -- App.videos.loadVideos simply pages out of localStorage instead of the server while that view is open. Sort applies to the bar and the grid together: recently added (default), oldest, title, longest, shortest, and a shuffle for rediscovering a long list. Tests (scratchpad): dates backfilled onto undated favorites, the bar paging as it scrolls rather than building every card, the grid paging to the full list with zero server calls, each sort order reordering it, and the way back to the channel listing. Also measured: 515 imported favorites load with the page responsive and 24 bar cards built. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd
220 lines
11 KiB
HTML
220 lines
11 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...">
|
|
<button class="search-clear-btn" id="search-clear-btn" type="button" aria-label="Clear search" title="Clear search">✕</button>
|
|
</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-select">Channel</label>
|
|
<select id="channel-select"></select>
|
|
</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">
|
|
<button id="info-close" class="info-close" type="button" aria-label="Close">✕</button>
|
|
<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="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>
|
|
|
|
<script src="static/js/state.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/main.js"></script>
|
|
</body>
|
|
</html>
|