Files
jacuzzi/frontend/index.html
Simon d5c859a1c7 Count only the part of a page the reader was shown
`perPage` is a request, not a promise: a source whose own pages are 30
long answers a page of 12 with 30. The grid showed the reader the page it
asked for, the mark kept 24 of those ids, and the daily check then counted
every probe id the mark did not contain -- including ids 25 to 30 of the
same page, which were never new and no amount of looking took off the
badge. A saved search on sxyprn sat on "6" through every reload.

So the count now goes only as far down the page as the mark reaches, and
the mark holds enough ids (60) for a page larger than the one asked for.
Order does not come into it, which keeps it right for sources that shift a
listing between visits.

And the whole list can be marked read in one press, for a reader who sees
the badge and decides none of it is for them. That moves each mark on to
what the source lists now rather than zeroing the count, which would come
straight back on the next check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPZFnNdHbPGDTqQUNiE4ZN
2026-10-19 20:15:17 +00:00

291 lines
15 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>
<button class="search-save-btn" id="search-save-btn" type="button" aria-pressed="false"
aria-label="Save this search" title="Save this search">
<svg class="icon-line" viewBox="0 0 24 24" aria-hidden="true">
<path d="M6.5 4.75h11a.75.75 0 0 1 .75.75v13.3l-6.25-4.1-6.25 4.1V5.5a.75.75 0 0 1 .75-.75z"/>
</svg>
</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 saved-toggle" id="saved-searches-btn" type="button"
title="Saved searches" aria-label="Saved searches" aria-haspopup="dialog" aria-expanded="false">
<svg class="icon-line" viewBox="0 0 24 24" aria-hidden="true">
<path d="M8 3.75h8a1 1 0 0 1 1 1V17l-5-3.2L7 17V4.75a1 1 0 0 1 1-1z"/>
<path d="M18.5 6.5h.5a1 1 0 0 1 1 1v12.75l-3-2" stroke-opacity="0.55"/>
</svg>
<span class="saved-badge" id="saved-searches-badge" aria-hidden="true" hidden></span>
</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="saved-panel" class="saved-panel" aria-hidden="true">
<div class="saved-box" role="dialog" aria-modal="true" aria-labelledby="saved-panel-title">
<div class="saved-head">
<div class="saved-head-text">
<h3 id="saved-panel-title">Saved searches</h3>
<p class="saved-head-note" id="saved-head-note"></p>
</div>
<div class="saved-head-actions">
<button id="saved-read-btn" class="btn-link" type="button">Mark all read</button>
<button id="saved-check-btn" class="btn-link" type="button">Check now</button>
<button id="saved-close-btn" class="close-btn" type="button" aria-label="Close">✕</button>
</div>
</div>
<div id="saved-list" class="saved-list"></div>
<div id="saved-empty" class="saved-empty">
Nothing saved yet. Pick a channel, set your filters, type a search —
then press the bookmark in the search bar to keep the whole thing.
</div>
<div class="saved-foot">
<button id="saved-add-btn" class="btn-secondary saved-add" type="button">Save the current search</button>
</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/savedSearches.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>