Files
jellytau/ux-flows.html
T

1702 lines
100 KiB
HTML
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" class="navy" dir="ltr">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>UX Flows - JellyTau Documentation</title>
<!-- Custom HTML head -->
<meta name="description" content="Requirements, traceability, and architecture for the JellyTau Jellyfin client.">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="icon" href="favicon.svg">
<link rel="shortcut icon" href="favicon.png">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/general.css">
<link rel="stylesheet" href="css/chrome.css">
<link rel="stylesheet" href="css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="highlight.css">
<link rel="stylesheet" href="tomorrow-night.css">
<link rel="stylesheet" href="ayu-highlight.css">
<!-- Custom theme stylesheets -->
</head>
<body class="sidebar-visible no-js">
<div id="body-container">
<!-- Provide site root to javascript -->
<script>
var path_to_root = "";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "navy";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('navy')
html.classList.add(theme);
var body = document.querySelector('body');
body.classList.remove('no-js')
body.classList.add('js');
</script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var body = document.querySelector('body');
var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
sidebar_toggle.checked = sidebar === 'visible';
body.classList.remove('sidebar-visible');
body.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded affix "><a href="index.html">Introduction</a></li><li class="chapter-item expanded affix "><li class="part-title">Requirements & Traceability</li><li class="chapter-item expanded "><a href="requirements.html"><strong aria-hidden="true">1.</strong> Requirements Specification</a></li><li class="chapter-item expanded "><a href="traceability.html"><strong aria-hidden="true">2.</strong> Traceability Matrix</a></li><li class="chapter-item expanded "><a href="traceability-ci.html"><strong aria-hidden="true">3.</strong> Traceability CI</a></li><li class="chapter-item expanded "><a href="traces-quick-ref.html"><strong aria-hidden="true">4.</strong> Traces Quick Reference</a></li><li class="chapter-item expanded affix "><li class="part-title">Architecture</li><li class="chapter-item expanded "><a href="architecture/index.html"><strong aria-hidden="true">5.</strong> Overview</a><a class="toggle"><div></div></a></li><li><ol class="section"><li class="chapter-item "><a href="architecture/01-rust-backend.html"><strong aria-hidden="true">5.1.</strong> Rust Backend</a></li><li class="chapter-item "><a href="architecture/02-svelte-frontend.html"><strong aria-hidden="true">5.2.</strong> Svelte Frontend</a></li><li class="chapter-item "><a href="architecture/03-data-flow.html"><strong aria-hidden="true">5.3.</strong> Data Flow</a></li><li class="chapter-item "><a href="architecture/04-type-sync-and-threading.html"><strong aria-hidden="true">5.4.</strong> Type Sync & Threading</a></li><li class="chapter-item "><a href="architecture/05-platform-backends.html"><strong aria-hidden="true">5.5.</strong> Platform Backends</a></li><li class="chapter-item "><a href="architecture/06-downloads-and-offline.html"><strong aria-hidden="true">5.6.</strong> Downloads & Offline</a></li><li class="chapter-item "><a href="architecture/07-connectivity.html"><strong aria-hidden="true">5.7.</strong> Connectivity</a></li><li class="chapter-item "><a href="architecture/08-database-design.html"><strong aria-hidden="true">5.8.</strong> Database Design</a></li><li class="chapter-item "><a href="architecture/09-security.html"><strong aria-hidden="true">5.9.</strong> Security</a></li></ol></li><li class="chapter-item expanded "><li class="part-title">UX</li><li class="chapter-item expanded "><a href="ux-flows.html" class="active"><strong aria-hidden="true">6.</strong> UX Flows</a></li><li class="chapter-item expanded affix "><li class="part-title">Specs — Pending Work</li><li class="chapter-item expanded "><a href="specs/index.html"><strong aria-hidden="true">7.</strong> Specs Index</a></li><li class="chapter-item expanded "><a href="specs/SPEC-TEMPLATE.html"><strong aria-hidden="true">8.</strong> Spec Template</a></li><li class="chapter-item expanded "><a href="specs/SPEC-REVIEW-CHECKLIST.html"><strong aria-hidden="true">9.</strong> Spec Review Checklist</a></li><li class="chapter-item expanded "><a href="specs/playback-backend-unification.html"><strong aria-hidden="true">10.</strong> Playback Backend Unification</a></li><li class="chapter-item expanded "><a href="specs/linux-native-video-spike.html"><strong aria-hidden="true">11.</strong> Linux Native Video Spike</a></li><li class="chapter-item expanded "><a href="specs/backend-owned-stream-selection.html"><strong aria-hidden="true">12.</strong> Backend-Owned Stream Selection</a></li><li class="chapter-item expanded "><a href="specs/player-facade-enforcement.html"><strong aria-hidden="true">13.</strong> Player Facade Enforcement</a></li><li class="chapter-item expanded "><a href="specs/windows-native-audio-backend.html"><strong aria-hidden="true">14.</strong> Windows Native Audio Backend</a></li><li class="chapter-item expanded "><a href="specs/libmpv2-migration.html"><strong aria-hidden="true">15.</strong> libmpv2 Migration</a></li><li class="chapter-item expanded "><a href="specs/read-through-media-cache.html"><strong aria-hidden="true">16.</strong> Read-Through Media Cache</a></li><li class="chapter-item expanded "><a href="specs/scoped-search.html"><strong aria-hidden="true">17.</strong> Scoped Search</a></li><li class="chapter-item expanded "><a href="specs/scoped-search-boundary.html"><strong aria-hidden="true">18.</strong> Scoped Search Boundary</a></li><li class="chapter-item expanded "><a href="specs/scoped-search-boundary-implementation.html"><strong aria-hidden="true">19.</strong> Scoped Search Boundary — Implementation</a></li><li class="chapter-item expanded "><a href="specs/frontend-domain-model.html"><strong aria-hidden="true">20.</strong> Frontend Domain Model</a></li><li class="chapter-item expanded "><a href="specs/build-provenance.html"><strong aria-hidden="true">21.</strong> Build Provenance</a></li><li class="chapter-item expanded affix "><li class="part-title">Build & Release</li><li class="chapter-item expanded "><a href="build/build-release.html"><strong aria-hidden="true">22.</strong> Build & Release</a></li><li class="chapter-item expanded "><a href="release-checklist.html"><strong aria-hidden="true">23.</strong> Release Checklist</a></li><li class="chapter-item expanded "><a href="build/build-desktop-packages.html"><strong aria-hidden="true">24.</strong> Desktop Packaging</a></li><li class="chapter-item expanded "><a href="build/build-windows.html"><strong aria-hidden="true">25.</strong> Windows Build</a></li><li class="chapter-item expanded "><a href="defect-windows.html"><strong aria-hidden="true">26.</strong> Defect Windows</a></li><li class="chapter-item expanded "><a href="build/docker.html"><strong aria-hidden="true">27.</strong> Docker</a></li><li class="chapter-item expanded "><a href="build/build-builder-image.html"><strong aria-hidden="true">28.</strong> Builder Image</a></li><li class="chapter-item expanded affix "><li class="spacer"></li><li class="chapter-item expanded affix "><a href="api-redirect.html">Rust API Reference (rustdoc)</a></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
</nav>
<!-- Track and set sidebar scroll position -->
<script>
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
sidebarScrollbox.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
}
}, { passive: true });
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
sessionStorage.removeItem('sidebar-scroll');
if (sidebarScrollTop) {
// preserve sidebar scroll position when navigating via links within sidebar
sidebarScrollbox.scrollTop = sidebarScrollTop;
} else {
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
var activeSection = document.querySelector('#sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
</script>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</label>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">JellyTau Documentation</h1>
<div class="right-buttons">
<a href="print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
<a href="https://gitea.tourolle.paris/dtourolle/jellytau" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
<a href="https://gitea.tourolle.paris/dtourolle/jellytau/_edit/master/docs/../docs/ux-flows.md" title="Suggest an edit" aria-label="Suggest an edit">
<i id="git-edit-button" class="fa fa-edit"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1 id="jellytau-ux-flows--screen-transitions"><a class="header" href="#jellytau-ux-flows--screen-transitions">JellyTau UX Flows &amp; Screen Transitions</a></h1>
<p>This document describes the expected user experience flows, screen transitions, and navigation patterns in JellyTau.</p>
<hr />
<h2 id="1-core-navigation-structure"><a class="header" href="#1-core-navigation-structure">1. Core Navigation Structure</a></h2>
<h3 id="11-navigation-system"><a class="header" href="#11-navigation-system">1.1 Navigation System</a></h3>
<p>JellyTau uses a unified navigation system with a bottom navigation bar visible on all platforms (mobile and desktop) and additional header navigation for desktop.</p>
<p><strong>Bottom Navigation Bar (All Platforms - DR-045, UR-039):</strong></p>
<p>The bottom navigation bar is the primary navigation and is <strong>always visible</strong> on all platforms (mobile and desktop) except when:</p>
<ul>
<li>Full-screen video player is active</li>
<li>User is on the login screen</li>
</ul>
<p><strong>Bottom Nav Structure:</strong></p>
<pre><code>┌─────────────────────────────────────────┐
│ [Home] [Library] [Search] │
└─────────────────────────────────────────┘
</code></pre>
<p><strong>Routes:</strong></p>
<ul>
<li><strong>Home</strong><code>/</code> (home page with carousels and featured content)</li>
<li><strong>Library</strong><code>/library</code> (library selector showing all libraries)</li>
<li><strong>Search</strong><code>/search</code> (dedicated search page)</li>
</ul>
<p><strong>Note:</strong> Available on both mobile and desktop for consistent navigation access.</p>
<p><strong>Header Navigation (Desktop):</strong></p>
<p>On desktop (md breakpoint and above), the header contains:</p>
<ul>
<li>Logo (links to <code>/library</code>)</li>
<li>Navigation links: Home, Library, Downloads, Settings</li>
<li>Search bar (inline)</li>
<li>Account menu (see §1.2)</li>
</ul>
<p><strong>Mobile Navigation:</strong></p>
<p>On mobile, the header contains:</p>
<ul>
<li>Logo</li>
<li>Account menu button (see §1.2)</li>
</ul>
<h3 id="12-account-menu"><a class="header" href="#12-account-menu">1.2 Account Menu</a></h3>
<p>Account-level destinations — the ones that are <em>about the user</em> rather than
about media — live behind a single <strong>account menu</strong>, anchored to the user's
name/avatar at the right of the header.</p>
<p><strong>Contents, in order:</strong></p>
<pre><code>┌──────────────────────────┐
│ Signed in as &lt;name&gt; │ ← identity, not a menu item
&lt;server host&gt;
├──────────────────────────┤
│ ⬇ Downloads │
│ ⚙ Settings │
│ ▦ Display │ ← grid/list preference (§5A.2)
├──────────────────────────┤
│ ⇥ Sign out │
└──────────────────────────┘
</code></pre>
<p><strong>Rules:</strong></p>
<ul>
<li><strong>One menu, both platforms.</strong> Desktop and mobile show the same items in the
same order. A user who learns where Settings lives on one form factor finds
it in the same place on the other.</li>
<li><strong>Anchored to identity.</strong> The trigger is the username/avatar, because that is
where users look for account actions. A bare three-dot icon does not signal
"your account".</li>
<li><strong>Sign out is separated</strong> by a divider and placed last — it is destructive and
must not sit adjacent to routine navigation.</li>
<li><strong>The menu is reachable from every authenticated screen</strong>, not only from
library routes. See §1.3.</li>
</ul>
<p><strong>Access Points Summary:</strong></p>
<ul>
<li><strong>Downloads</strong> → header icon (desktop) + account menu (both)</li>
<li><strong>Settings</strong> → header nav link (desktop) + account menu (both)</li>
<li><strong>Sign out</strong> → account menu only</li>
</ul>
<h3 id="13-chrome-availability"><a class="header" href="#13-chrome-availability">1.3 Chrome availability</a></h3>
<p>The header is shared across chrome-bearing routes. Routes fall into three groups:</p>
<div class="table-wrapper"><table><thead><tr><th>Route group</th><th>Header</th><th>Bottom nav</th><th>Account menu reachable?</th></tr></thead><tbody>
<tr><td><code>/library/*</code></td><td>Yes (own layout, shared <code>AppHeader</code>)</td><td>Yes</td><td>Yes</td></tr>
<tr><td><code>/</code>, <code>/search</code>, <code>/downloads</code></td><td>Yes (root-owned <code>AppHeader</code>)</td><td>Yes</td><td>Yes</td></tr>
<tr><td><code>/settings</code></td><td>Own layout</td><td>No</td><td>n/a — already there</td></tr>
<tr><td><code>/player/*</code>, <code>/login</code></td><td>No</td><td>No</td><td>No (by design)</td></tr>
</tbody></table>
</div>
<p>The rule the app honours: every authenticated, non-immersive screen exposes the
account menu. Only the full-screen player and the login screen are chrome-free.</p>
<h3 id="14-known-deviations"><a class="header" href="#14-known-deviations">1.4 Known deviations</a></h3>
<p><em>(None — the account-menu and chrome-availability defects tracked here under
UR-054 were resolved. Settings, Downloads, Display, and Sign out are now reachable
from every authenticated non-immersive screen via the shared <code>AccountMenu</code>, the
username/avatar is the menu trigger, desktop and mobile share one menu, and the
Display preference has a Settings entry — UR-029, §5A.4.)</em></p>
<hr />
<h2 id="2-initial-app-launch-flow"><a class="header" href="#2-initial-app-launch-flow">2. Initial App Launch Flow</a></h2>
<h3 id="21-first-time-launch"><a class="header" href="#21-first-time-launch">2.1 First-Time Launch</a></h3>
<pre><code class="language-mermaid">flowchart TB
Launch[App Launch] --&gt; CheckAuth{Stored&lt;br/&gt;Credentials?}
CheckAuth --&gt;|No| LoginScreen[Login Screen&lt;br/&gt;/login]
CheckAuth --&gt;|Yes| AutoLogin[Auto-login]
LoginScreen --&gt; EnterURL[Enter Server URL]
EnterURL --&gt; EnterCreds[Enter Username/Password]
EnterCreds --&gt; LoginSuccess{Success?}
LoginSuccess --&gt;|No| LoginError[Show Error]
LoginError --&gt; EnterCreds
LoginSuccess --&gt;|Yes| StoreToken[Store Token in Keyring]
AutoLogin --&gt; TokenValid{Token Valid?}
TokenValid --&gt;|No| LoginScreen
TokenValid --&gt;|Yes| HomePage
StoreToken --&gt; HomePage[Home Page&lt;br/&gt;/]
</code></pre>
<p><strong>Screens:</strong></p>
<ol>
<li>
<p><strong>Login Screen</strong> (<code>/login</code>)</p>
<ul>
<li>Server URL input</li>
<li>Username input</li>
<li>Password input</li>
<li>"Remember me" checkbox (default: on)</li>
<li>Login button</li>
<li>No header, no bottom nav</li>
</ul>
</li>
<li>
<p><strong>Home Page</strong> (<code>/</code>)</p>
<ul>
<li>Default landing page after successful login</li>
<li>Shows featured content, carousels, continue watching</li>
<li>No MiniPlayer visible (nothing playing yet)</li>
<li>Bottom nav: Home tab active</li>
<li>Header with navigation links</li>
</ul>
</li>
</ol>
<h3 id="22-subsequent-launches"><a class="header" href="#22-subsequent-launches">2.2 Subsequent Launches</a></h3>
<pre><code class="language-mermaid">flowchart TB
Launch[App Launch] --&gt; LoadAuth[Load Stored Token]
LoadAuth --&gt; Validate{Token Valid?}
Validate --&gt;|Yes| RestoreState[Restore Last Screen]
Validate --&gt;|No| LoginScreen[Login Screen&lt;br/&gt;/login]
RestoreState --&gt; CheckPlayer{Was Player&lt;br/&gt;Active?}
CheckPlayer --&gt;|Yes| ShowMiniPlayer[Show MiniPlayer&lt;br/&gt;at bottom]
CheckPlayer --&gt;|No| HideMiniPlayer[No MiniPlayer]
ShowMiniPlayer --&gt; LastScreen[Last Active Screen&lt;br/&gt;with MiniPlayer]
HideMiniPlayer --&gt; HomePage[Home Page&lt;br/&gt;/]
</code></pre>
<p><strong>State Restoration:</strong></p>
<ul>
<li>Last viewed screen (route) is restored (defaults to <code>/</code> if none)</li>
<li>If audio was playing, MiniPlayer appears at bottom</li>
<li>Playback state is NOT automatically resumed (user must press play)</li>
<li>Queue is restored if it existed</li>
</ul>
<hr />
<h2 id="3-audio-playback-flows"><a class="header" href="#3-audio-playback-flows">3. Audio Playback Flows</a></h2>
<h3 id="31-starting-audio-playback"><a class="header" href="#31-starting-audio-playback">3.1 Starting Audio Playback</a></h3>
<pre><code class="language-mermaid">flowchart TB
Start[User Action] --&gt; Action{Action Type?}
Action --&gt;|Click Track| TrackList[TrackList Component]
Action --&gt;|Click Album| AlbumDetail[Album Detail Page]
Action --&gt;|Click Play on Album| AlbumPlay[Play Album Button]
TrackList --&gt; PlayTrack[Play Single Track]
PlayTrack --&gt; QueueAll[Queue All Filtered Tracks]
AlbumPlay --&gt; PlayAlbum[Play All Album Tracks]
PlayAlbum --&gt; QueueAlbum[Queue Album Tracks]
QueueAll --&gt; InvokePlay[invoke player_play_queue]
QueueAlbum --&gt; InvokePlay
InvokePlay --&gt; PlayerStarts[Player State: Playing]
PlayerStarts --&gt; MiniAppears[MiniPlayer Slides Up&lt;br/&gt;from Bottom]
MiniAppears --&gt; StayOnPage[User Stays on&lt;br/&gt;Current Screen]
</code></pre>
<p><strong>Entry Points for Audio Playback:</strong></p>
<ol>
<li>
<p><strong>TrackList</strong> (<code>/library/music/tracks</code>, <code>/library/music/albums/[id]</code>)</p>
<ul>
<li>Click track number → Play track + queue all visible tracks</li>
<li>Clicking track #3 in an album → Play track 3, queue tracks 1-10</li>
</ul>
</li>
<li>
<p><strong>Album Card</strong> (grid views)</p>
<ul>
<li>Click album → Navigate to album detail</li>
<li>Play button on card → Play album immediately</li>
</ul>
</li>
<li>
<p><strong>Search Results</strong></p>
<ul>
<li>Click track → Play track + queue search results</li>
<li>Click album → Navigate to album detail</li>
</ul>
</li>
</ol>
<p><strong>MiniPlayer Behavior:</strong></p>
<ul>
<li>Slides up from bottom with animation (300ms)</li>
<li>Height: 64px on mobile, 80px on desktop</li>
<li>Shows: artwork, title, artist, play/pause, next, favorite</li>
<li>Stays visible on ALL screens (except video player)</li>
<li>Click anywhere on MiniPlayer → Navigate to full player</li>
</ul>
<p><strong>Track Highlighting:</strong>
When audio is playing, the currently playing track is visually highlighted in track lists and album pages:</p>
<ul>
<li>Subtle blue background tint</li>
<li>Left border accent in Jellyfin blue</li>
<li>Title text colored in Jellyfin blue</li>
<li>Desktop: Animated pulsing dots indicator next to title</li>
<li>Mobile: Play arrow (▶) inline with title</li>
<li>Highlight updates automatically when skipping to next/previous track</li>
</ul>
<h3 id="32-miniplayer--full-player-transition"><a class="header" href="#32-miniplayer--full-player-transition">3.2 MiniPlayer → Full Player Transition</a></h3>
<pre><code class="language-mermaid">flowchart TB
Mini[MiniPlayer Visible] --&gt; UserClick{User Action}
UserClick --&gt;|Click MiniPlayer| NavFullPlayer[Navigate to&lt;br/&gt;/player/[id]]
UserClick --&gt;|Swipe Up| SwipeGesture[Swipe Gesture&lt;br/&gt;Planned]
NavFullPlayer --&gt; FullPlayer[Full Audio Player Screen]
SwipeGesture --&gt; FullPlayer
FullPlayer --&gt; ShowControls[Show Full Controls:&lt;br/&gt;- Large artwork&lt;br/&gt;- Progress bar&lt;br/&gt;- Volume slider&lt;br/&gt;- Queue button&lt;br/&gt;- Shuffle/Repeat&lt;br/&gt;- Favorite button]
ShowControls --&gt; MiniHidden[MiniPlayer Hidden]
</code></pre>
<p><strong>Full Player Screen</strong> (<code>/player/[id]</code>)</p>
<ul>
<li><strong>Header:</strong> Song title, artist (clickable links to artist/album pages)</li>
<li><strong>Artwork:</strong> Large album art (centered, dominant)</li>
<li><strong>Progress:</strong> Seek bar with current time / total duration</li>
<li><strong>Controls:</strong> Previous, Play/Pause, Next (large touch targets)</li>
<li><strong>Secondary Controls:</strong> Shuffle, Repeat mode, Queue, Favorite</li>
<li><strong>Volume:</strong> Volume slider</li>
<li><strong>Bottom Nav:</strong> Still visible (can navigate away while playing)</li>
<li><strong>Back button:</strong> Returns to previous screen, MiniPlayer reappears</li>
</ul>
<h3 id="33-full-player--back-to-browsing"><a class="header" href="#33-full-player--back-to-browsing">3.3 Full Player → Back to Browsing</a></h3>
<pre><code class="language-mermaid">flowchart TB
FullPlayer[Full Player Screen] --&gt; UserAction{User Action}
UserAction --&gt;|Back Button / Close| HistoryBack[window.history.back]
UserAction --&gt;|Bottom Nav Click| NavOther[Navigate to&lt;br/&gt;Other Screen]
HistoryBack --&gt; PrevScreen[Return to Previous Screen&lt;br/&gt;in Browser History]
NavOther --&gt; NewScreen[Navigate to New Screen]
PrevScreen --&gt; MiniReappears[MiniPlayer Slides Up&lt;br/&gt;from Bottom]
NewScreen --&gt; MiniReappears
MiniReappears --&gt; PlaybackContinues[Playback Continues&lt;br/&gt;in Background]
</code></pre>
<p><strong>Navigation Behavior:</strong></p>
<ul>
<li><strong>Back Button:</strong> Uses browser history (<code>window.history.back()</code>) to return to the previous page</li>
<li><strong>Expected behavior:</strong> Returns user to the screen they were on before opening full player</li>
<li><strong>Example:</strong> User browsing album → clicks track → full player opens → clicks back → returns to album</li>
</ul>
<p><strong>Key UX Principles:</strong></p>
<ul>
<li><strong>Playback Never Stops:</strong> Navigating away from player does NOT stop playback</li>
<li><strong>MiniPlayer Persistence:</strong> MiniPlayer visible on ALL screens (except video/login)</li>
<li><strong>Queue Preserved:</strong> Current queue remains intact</li>
<li><strong>State Restoration:</strong> Returning to full player shows same state (position, volume, etc.)</li>
<li><strong>Natural Navigation:</strong> Back button behaves as expected (returns to previous page, not just closes modal)</li>
</ul>
<hr />
<h2 id="4-video-playback-flows"><a class="header" href="#4-video-playback-flows">4. Video Playback Flows</a></h2>
<h3 id="41-starting-video-playback"><a class="header" href="#41-starting-video-playback">4.1 Starting Video Playback</a></h3>
<pre><code class="language-mermaid">flowchart TB
Start[User Action] --&gt; Action{Action Type?}
Action --&gt;|Click Movie| MovieDetail[Movie Detail Page]
Action --&gt;|Click Episode| EpisodeClick[Episode Click]
Action --&gt;|Click Play Button| PlayButton[Play Button]
MovieDetail --&gt; PlayMovie[Play Movie Button]
EpisodeClick --&gt; PlayEpisode[Play Episode]
PlayMovie --&gt; CheckResume{Resume&lt;br/&gt;Position?}
PlayEpisode --&gt; CheckResume
CheckResume --&gt;|Yes, &gt;30s| ShowDialog[Resume Dialog]
CheckResume --&gt;|No| DirectPlay[Start from Beginning]
ShowDialog --&gt; UserChoice{User Choice}
UserChoice --&gt;|Resume| ResumePlay[Start at Saved Position]
UserChoice --&gt;|Start Over| DirectPlay
ResumePlay --&gt; FullscreenVideo[Fullscreen Video Player&lt;br/&gt;/player/[id]]
DirectPlay --&gt; FullscreenVideo
FullscreenVideo --&gt; HideUI[Hide All UI:&lt;br/&gt;- No Bottom Nav&lt;br/&gt;- No MiniPlayer&lt;br/&gt;- Fullscreen only]
</code></pre>
<p><strong>Resume Dialog:</strong></p>
<pre><code>┌─────────────────────────────────────────┐
│ Continue Watching? │
│ │
│ [Movie Title] │
│ Resume from 12:34 / 1:45:00 │
│ │
│ [Start from Beginning] [Resume] │
└─────────────────────────────────────────┘
</code></pre>
<h3 id="42-video-player-screen-ir-003-ir-004-ur-003"><a class="header" href="#42-video-player-screen-ir-003-ir-004-ur-003">4.2 Video Player Screen (IR-003, IR-004, UR-003)</a></h3>
<p><strong>Initial State (First 3 seconds):</strong></p>
<ul>
<li>Controls visible overlay</li>
<li>Top bar: Back button, title</li>
<li>Bottom bar: Play/Pause, seek bar, time, settings (subtitles, audio track)</li>
<li>Center: Large play/pause button</li>
</ul>
<p><strong>After 3 Seconds (Idle):</strong></p>
<ul>
<li>All controls fade out (500ms animation)</li>
<li>Fullscreen video only</li>
<li>System UI hidden (status bar, nav bar)</li>
</ul>
<p><strong>User Interaction:</strong></p>
<ul>
<li><strong>Tap screen:</strong> Controls reappear for 3 seconds</li>
<li><strong>Double tap left side:</strong> Rewind 10 seconds (shows animated feedback with "-10" indicator)</li>
<li><strong>Double tap right side:</strong> Forward 30 seconds (shows animated feedback with "+30" indicator)</li>
<li><strong>Single tap play/pause is deferred</strong> by the 300 ms double-tap window, so a double tap
skips without also toggling pause (UR-061)</li>
<li><strong>Swipe up/down on left side:</strong> Adjust brightness (0.3-1.7x, shows brightness indicator with progress bar)</li>
<li><strong>Swipe up/down on right side:</strong> Adjust volume (0-100%, shows volume indicator with progress bar)</li>
<li><strong>Keyboard arrows:</strong> ← rewind 10s, → forward 30s (desktop/external keyboard)</li>
<li><strong>Keyboard space/K:</strong> Toggle play/pause</li>
<li><strong>Keyboard F:</strong> Toggle fullscreen</li>
<li><strong>Pinch:</strong> Zoom (planned)</li>
</ul>
<h3 id="43-exiting-video-player"><a class="header" href="#43-exiting-video-player">4.3 Exiting Video Player</a></h3>
<pre><code class="language-mermaid">flowchart TB
VideoPlaying[Video Playing] --&gt; UserAction{User Action}
UserAction --&gt;|Back Button| StopVideo[Stop Playback]
UserAction --&gt;|Home Button| Background[App to Background]
UserAction --&gt;|Video Ends| VideoEnd[Playback Ended]
StopVideo --&gt; SaveProgress[Save Progress&lt;br/&gt;to Local DB + Server]
VideoEnd --&gt; SaveComplete[Mark as Watched&lt;br/&gt;Save Progress]
Background --&gt; PauseVideo[Pause Video]
SaveProgress --&gt; ExitFullscreen[Exit Fullscreen]
SaveComplete --&gt; AutoNext{Next Episode&lt;br/&gt;Available?}
AutoNext --&gt;|Yes| ShowCountdown[Show Countdown&lt;br/&gt;Next in 5s...]
AutoNext --&gt;|No| ExitFullscreen
ShowCountdown --&gt; UserCancel{User Cancels?}
UserCancel --&gt;|Yes| ExitFullscreen
UserCancel --&gt;|No, timeout| PlayNext[Play Next Episode]
ExitFullscreen --&gt; RestoreUI[Restore UI:&lt;br/&gt;- Bottom Nav&lt;br/&gt;- Previous Screen]
PlayNext --&gt; VideoPlaying
PauseVideo --&gt; ShowNotification[Show Notification:&lt;br/&gt;Tap to Resume]
</code></pre>
<p><strong>Auto-Next Overlay:</strong></p>
<pre><code>┌─────────────────────────────────────────┐
│ │
│ [Episode Thumbnail] │
│ │
│ Next: S01E02 - Episode Title │
│ Starting in 5 seconds... │
│ │
│ [Cancel] [Play Now] │
└─────────────────────────────────────────┘
</code></pre>
<hr />
<h2 id="5-music-library-navigation-flows"><a class="header" href="#5-music-library-navigation-flows">5. Music Library Navigation Flows</a></h2>
<h3 id="51-music-category-landing-page"><a class="header" href="#51-music-category-landing-page">5.1 Music Category Landing Page</a></h3>
<pre><code class="language-mermaid">flowchart TB
LibraryHome[Library Home&lt;br/&gt;/library] --&gt; ClickMusic[Click Music Library]
ClickMusic --&gt; MusicLanding[Music Landing Page&lt;br/&gt;/library/music]
MusicLanding --&gt; ShowCategories[Show Category Cards:&lt;br/&gt;- Tracks&lt;br/&gt;- Artists&lt;br/&gt;- Albums&lt;br/&gt;- Playlists&lt;br/&gt;- Genres]
ShowCategories --&gt; UserClick{User Clicks Category}
UserClick --&gt;|Tracks| TracksPage[All Tracks Page&lt;br/&gt;/library/music/tracks]
UserClick --&gt;|Artists| ArtistsPage[Artists Grid&lt;br/&gt;/library/music/artists]
UserClick --&gt;|Albums| AlbumsPage[Albums Grid&lt;br/&gt;/library/music/albums]
UserClick --&gt;|Playlists| PlaylistsPage[Playlists Grid&lt;br/&gt;/library/music/playlists]
UserClick --&gt;|Genres| GenresPage[Genres Browser&lt;br/&gt;/library/music/genres]
</code></pre>
<p><strong>Category Cards:</strong></p>
<pre><code>┌─────────────────────────────────────────┐
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ 🎵 │ │ 👤 │ │ 💿 │ │
│ │Track│ │Artist│ │Album│ │
│ └──────┘ └──────┘ └──────┘ │
│ ┌──────┐ ┌──────┐ │
│ │ 📝 │ │ 🎭 │ │
│ │List │ │Genre│ │
│ └──────┘ └──────┘ │
└─────────────────────────────────────────┘
</code></pre>
<h3 id="52-albums-view-flow"><a class="header" href="#52-albums-view-flow">5.2 Albums View Flow</a></h3>
<pre><code class="language-mermaid">flowchart TB
AlbumsGrid[Albums Grid&lt;br/&gt;grid/list per §5A] --&gt; UserAction{User Action}
UserAction --&gt;|Click Album| AlbumDetail[Album Detail Page&lt;br/&gt;/library/[id]]
UserAction --&gt;|Click Play on Card| PlayAlbum[Play Album Immediately]
AlbumDetail --&gt; ShowAlbum[Show Album:&lt;br/&gt;- Album Art&lt;br/&gt;- Title, Artist&lt;br/&gt;- Track List&lt;br/&gt;- Download Button&lt;br/&gt;- Favorite Button]
ShowAlbum --&gt; TrackAction{User Action}
TrackAction --&gt;|Click Track| PlayTrack[Play Track + Queue Album]
TrackAction --&gt;|Click Artist| NavArtist[Navigate to Artist Page]
TrackAction --&gt;|Download Album| DownloadFlow[Download Flow]
TrackAction --&gt;|Back Button| BackToGrid[Return to Albums Grid]
</code></pre>
<p><strong>Album Detail Layout:</strong></p>
<pre><code>┌─────────────────────────────────────────┐
│ [←] [♡] [⬇] │
│ │
│ ┌────────────────────┐ │
│ │ │ │
│ │ Album Artwork │ │
│ │ │ │
│ └────────────────────┘ │
│ │
│ Album Title │
│ Artist Name (clickable) │
│ 2024 • 12 tracks • 45:23 │
│ │
│ [▶ Play] [🔀 Shuffle] │
│ │
│ ───────────────────────────────────── │
│ 1 Track Title 3:45 │
│ 2 Track Title 4:12 │
│ 3 Track Title 3:28 │
│ ... │
└─────────────────────────────────────────┘
</code></pre>
<h3 id="53-artist-navigation"><a class="header" href="#53-artist-navigation">5.3 Artist Navigation</a></h3>
<pre><code class="language-mermaid">flowchart TB
ArtistsGrid[Artists Grid] --&gt; ClickArtist[Click Artist]
ClickArtist --&gt; ArtistPage[Artist Detail Page&lt;br/&gt;/library/artist/[id]]
ArtistPage --&gt; ShowContent[Show Artist Content:&lt;br/&gt;- Artist Photo&lt;br/&gt;- Biography&lt;br/&gt;- Albums Grid&lt;br/&gt;- Top Tracks&lt;br/&gt;- Similar Artists]
ShowContent --&gt; UserAction{User Action}
UserAction --&gt;|Click Album| AlbumDetail[Album Detail Page]
UserAction --&gt;|Play Top Tracks| PlayArtist[Play Artist Radio]
UserAction --&gt;|Click Similar Artist| OtherArtist[Other Artist Page]
</code></pre>
<hr />
<h2 id="5a-library-page-layouts"><a class="header" href="#5a-library-page-layouts">5A. Library Page Layouts</a></h2>
<p>Every browse page is one of two shapes: a <strong>card grid</strong> or a <strong>row list</strong>. This
section is the rule for which shape a page takes, what a card looks like, and
what the user is allowed to change.</p>
<h3 id="5a1-card-shape-follows-the-media-not-the-page"><a class="header" href="#5a1-card-shape-follows-the-media-not-the-page">5A.1 Card shape follows the media, not the page</a></h3>
<p>Card aspect ratio is a property of <em>what the item is</em>, and is never overridden
per-page. This is the single most important layout rule: a user scanning a grid
recognises content type by silhouette before reading a word.</p>
<div class="table-wrapper"><table><thead><tr><th>Item type</th><th>Aspect</th><th>Rationale</th></tr></thead><tbody>
<tr><td>Album, Artist, Track, Playlist</td><td><strong>1:1 square</strong></td><td>Matches album art; the universal music convention (Spotify)</td></tr>
<tr><td>Movie, Series, Season</td><td><strong>2:3 poster</strong></td><td>Matches printed poster art; the universal video convention (Netflix)</td></tr>
<tr><td>Episode</td><td><strong>16:9 thumbnail</strong></td><td>A frame from the episode, not cover art — signals "a thing you watch next"</td></tr>
<tr><td>Library / collection folder</td><td><strong>16:9</strong></td><td>Reads as a container, distinct from the items inside it</td></tr>
</tbody></table>
</div>
<p>Artist cards are square but rendered <strong>circular-masked</strong>, so artists are
distinguishable from albums at a glance within the same music grid.</p>
<h3 id="5a2-grid-vs-list"><a class="header" href="#5a2-grid-vs-list">5A.2 Grid vs. list</a></h3>
<pre><code class="language-mermaid">flowchart TB
Page[Library browse page] --&gt; Kind{Content kind}
Kind --&gt;|Visual-first&lt;br/&gt;albums, artists, movies,&lt;br/&gt;shows, playlists| Grid[Card grid&lt;br/&gt;user may switch to list]
Kind --&gt;|Ordinal&lt;br/&gt;tracks in an album,&lt;br/&gt;episodes in a season| List[Row list&lt;br/&gt;always; no toggle]
Grid --&gt; Toggle[View toggle in page header]
Toggle --&gt; Persist[Choice persists globally&lt;br/&gt;across all grid pages]
</code></pre>
<ul>
<li><strong>Grids are the default</strong> for anything with cover art worth scanning.</li>
<li><strong>Lists are mandatory, not optional</strong>, where position carries meaning —
a track's number within an album, an episode's number within a season.
A grid destroys that ordering cue, so these pages expose <strong>no toggle</strong>.</li>
<li><strong>The toggle is global, not per-page.</strong> A user who prefers dense lists
prefers them everywhere; making them re-set it on each page is friction.
The choice persists across launches.</li>
</ul>
<p><strong>Responsive columns</strong> (grid mode), tuned so cards stay large enough to read
cover art on a phone and don't become postage stamps on a desktop:</p>
<div class="table-wrapper"><table><thead><tr><th>Breakpoint</th><th>Columns</th></tr></thead><tbody>
<tr><td>base (phone)</td><td>2</td></tr>
<tr><td>sm</td><td>3</td></tr>
<tr><td>md</td><td>4</td></tr>
<tr><td>lg</td><td>5</td></tr>
<tr><td>xl</td><td>6</td></tr>
</tbody></table>
</div>
<h3 id="5a3-what-a-card-shows"><a class="header" href="#5a3-what-a-card-shows">5A.3 What a card shows</a></h3>
<pre><code> ┌─────────────┐
│ │ ← cover art (aspect per §5A.1)
│ artwork │ • progress bar overlay if partially played
│ │ • watched/played check if complete
│ [▶] │ • play affordance on hover/focus
└─────────────┘
Primary line ← title, truncated to one line
Secondary line ← artist / year+rating / SxEy — one line, dimmed
</code></pre>
<ul>
<li><strong>Two lines of text maximum.</strong> Titles truncate rather than wrap; a card that
grows to fit its title breaks grid alignment and makes scanning harder.</li>
<li><strong>Progress and watched state live on the artwork</strong>, not in the text — they
must be readable while scanning, without reading.</li>
<li><strong>Hover/focus reveals play</strong>, so a card is both a navigation target and a
playback target without a second control competing for space at rest.</li>
</ul>
<h3 id="5a4-known-deviations"><a class="header" href="#5a4-known-deviations">5A.4 Known deviations</a></h3>
<p>These are places the implementation currently diverges from the rules above.
They are recorded here so the gap is explicit rather than mistaken for intent.</p>
<ul>
<li><strong>The view toggle is discoverable only on a browse page.</strong> The preference is
already global and persisted, but the only control that sets it is the pair
of icon buttons in a library page header. Settings has no display section, so
there is nowhere to look for it. <em>(UR-029)</em></li>
</ul>
<hr />
<h2 id="5b-video-detail-page-composition"><a class="header" href="#5b-video-detail-page-composition">5B. Video Detail Page Composition</a></h2>
<p>Movie, Series, and Episode detail pages all live at <code>/library/[id]</code>. Which
surface renders is decided by item type plus the <code>?episode=</code> query param, and
<strong>section order is part of the spec</strong> — it is what makes "keep watching this
show" the path of least resistance.</p>
<h3 id="5b1-which-surface-renders"><a class="header" href="#5b1-which-surface-renders">5B.1 Which surface renders</a></h3>
<pre><code class="language-mermaid">flowchart TB
Nav[Navigate to /library/&amp;#91;id&amp;#93;] --&gt; Type{Item type}
Type --&gt;|Person| Person[PersonDetailView]
Type --&gt;|Movie| Movie[Movie detail&lt;br/&gt;§5B.3]
Type --&gt;|Series| Ep{?episode= param&lt;br/&gt;present?}
Ep --&gt;|Yes| Focus[Episode Focus View&lt;br/&gt;§5B.2]
Ep --&gt;|No| Series[Series detail&lt;br/&gt;§5B.4]
Focus --&gt;|Back to series| Series
Series --&gt;|Click episode| Focus
</code></pre>
<p>An episode is <strong>never</strong> browsed as a bare <code>Episode</code> item page. Clicking an
episode anywhere — a series' season list, a Home carousel (§5B.5), etc. —
navigates to <code>/library/&lt;seriesId&gt;?episode=&lt;episodeId&gt;</code>, so the episode is always
shown in the context of its series and the series' full episode list is already
loaded. Should an episode ever arrive without a <code>seriesId</code> (deep link, stale
cache), the bare Episode page renders as a fallback and links back to its parent
series and season by title so the user is never stranded.</p>
<h3 id="5b2-episode-focus-view--section-order"><a class="header" href="#5b2-episode-focus-view--section-order">5B.2 Episode Focus View — section order</a></h3>
<p><strong>The next episodes appear directly below the current episode, above cast and
similar shows.</strong> Nothing may be inserted between the episode hero and the
episode strip.</p>
<pre><code>┌─────────────────────────────────────────────────┐
│ [←] │
│ ┌───────────────────────────────────────────┐ │
│ │ episode backdrop │ │
│ │ Series Name │ │ ← 1. HERO
│ │ Episode Title │ │
│ │ S2E4 • 48m • ★8.1 │ │
│ │ Overview… │ │
│ │ ▓▓▓▓▓░░░░░ 32m left │ │
│ │ [▶ Play] [⬇] [♡] │ │
│ └───────────────────────────────────────────┘ │
│ │
│ More Episodes │ ← 2. EPISODE STRIP
│ ┌──────┐┌──────┐┌──────┐┌──────┐ │ (immediately below hero)
│ │ E3 ││▓E4▓ ││ E5 ││ E6 │ → scroll │
│ │ ││NOW ││ ││ │ │
│ └──────┘└──────┘└──────┘└──────┘ │
│ │
│ Cast │ ← 3. CAST
│ ( ○ )( ○ )( ○ )( ○ ) │
│ │
│ More Like This │ ← 4. SIMILAR
│ ┌────┐┌────┐┌────┐┌────┐ │
└─────────────────────────────────────────────────┘
</code></pre>
<p><strong>Rules for the episode strip:</strong></p>
<ul>
<li><strong>Position is fixed.</strong> Hero → episode strip → cast → similar. The strip sits
between the current episode and every other section; cast and related
content are <em>below</em> it, never above.</li>
<li><strong>Window, not full list.</strong> The strip shows a window around the current
episode — roughly 3 before and 6 after — so the immediate next episodes are
visible without scrolling, and earlier ones remain reachable by scrolling
left. It is horizontally scrollable, not a wrapped grid.</li>
<li><strong>Forward bias.</strong> More episodes are shown <em>after</em> the current one than
before it: the dominant intent on this screen is "watch the next one."</li>
<li><strong>The current episode is present and marked.</strong> It renders in-strip with a
"NOW" badge and a highlight ring, and is not clickable. It anchors the
user's position in the season rather than being hidden.</li>
<li><strong>Cross-season continuity.</strong> The window spans the whole series in episode
order, so the strip runs past a season boundary into the next season's first
episodes rather than dead-ending at the end of a season.</li>
<li><strong>Per-episode state.</strong> Each card shows a thumbnail, <code>SxEy</code> + title, a resume
progress bar when partially watched, and a watched checkmark when complete.</li>
<li><strong>Clicking an episode swaps focus in place</strong> (<code>?episode=</code> changes); it does
not start playback. Playback starts only from the hero's Play button.</li>
</ul>
<h3 id="5b3-movie-detail--section-order"><a class="header" href="#5b3-movie-detail--section-order">5B.3 Movie detail — section order</a></h3>
<pre><code>Hero (poster, title, metadata, Play / Download / Favorite)
→ Crew links (Directed by / Written by / Music by)
→ Genre tags
→ Cast
→ More Like This
</code></pre>
<p>A movie has no continuation set, so cast follows the hero directly.</p>
<h3 id="5b4-series-detail--section-order"><a class="header" href="#5b4-series-detail--section-order">5B.4 Series detail — section order</a></h3>
<pre><code>Hero (poster, title, metadata, Resume SxEy / Download / Favorite / Clear history)
→ Crew links
→ Genre tags
→ Seasons (collapsible; only the current season expanded)
→ Cast
→ More Like This
</code></pre>
<p>The same principle as §5B.2: <strong>episodes come before cast and similar shows.</strong>
The reason a user opens a series page is to pick an episode; discovery content
is secondary and sits underneath.</p>
<p><strong>Rules for the seasons block</strong> <em>(UR-062, UR-064)</em>:</p>
<ul>
<li><strong>The page opens where the viewer is.</strong> The backend resolves the current
episode — in progress, else Next Up, else first unwatched, else the premiere —
and the page scrolls it into view with an <code>Up next</code> badge and a highlight ring.
Never season 1 by default, unless season 1 <em>is</em> where the viewer is.</li>
<li><strong>Seasons collapse; only the current one is expanded.</strong> A ten-season show
otherwise renders hundreds of rows and buries the episode the viewer came for.
A collapsed season still names its episode count and watched count, so
progress is readable without expanding it.</li>
<li><strong>The hero button opens, it does not play.</strong> It reads <code>Resume S2E4</code> /
<code>Play S1E1</code> — naming its target — and navigates to that episode's Focus View,
where Play commits. Play on a <em>container</em> is navigation (§5B.5); Play on a
<em>leaf</em> is the commitment.</li>
<li><strong>A season is never its own page.</strong> <code>/library/&lt;seasonId&gt;</code> redirects to
<code>/library/&lt;seriesId&gt;#season-N</code>. Every affordance that names a season — the
episode breadcrumb, a season card in a grid, a Downloads drill-in — lands on
the series with that season in view, so the episodes of all seasons stay one
browsable list.</li>
<li><strong>Watch history is erasable</strong> per series (hero) and per season (season
header). It confirms first, cannot be undone, and needs the server. Clearing a
whole series returns it to S1E1 by the same path a never-watched show takes.</li>
</ul>
<h3 id="5b5-home-card-interaction--tap-opens-long-press-plays"><a class="header" href="#5b5-home-card-interaction--tap-opens-long-press-plays">5B.5 Home-card interaction — tap opens, long-press plays</a></h3>
<p>Cards on the Home screen carousels (Next Movie, Next Episode, Continue
Watching, Recently Added, …) <strong>do not play on tap.</strong> A plain tap opens the
item; playback is the deliberate, second gesture.</p>
<div class="table-wrapper"><table><thead><tr><th>Card kind</th><th>Tap (short)</th><th>Long-press (~500 ms hold)</th></tr></thead><tbody>
<tr><td>Movie</td><td>Movie detail page (<code>/library/&lt;id&gt;</code>)</td><td>Confirm → play now (<code>/player/&lt;id&gt;</code>)</td></tr>
<tr><td>Episode</td><td>Series Episode Focus View (<code>/library/&lt;seriesId&gt;?episode=&lt;id&gt;</code>, per §5B.1)</td><td>Confirm → play now (<code>/player/&lt;id&gt;</code>)</td></tr>
<tr><td>Series / Season / Album / Artist / Playlist / Folder</td><td>Detail page (<code>/library/&lt;id&gt;</code>)</td><td>Same as tap (no single "play now" target)</td></tr>
<tr><td>Channel / live leaf</td><td>Player (<code>/player/&lt;id&gt;</code>) — no detail page exists</td><td>Confirm → play now</td></tr>
</tbody></table>
</div>
<p>Rationale and rules:</p>
<ul>
<li><strong>Tap is navigation, not commitment.</strong> Previously a tap on a movie/episode
jumped straight into the player, which made it easy to lose your place in a
half-watched item or start a stream you only meant to inspect. Tap now lands
on the detail/focus page, where Play is an explicit button.</li>
<li><strong>Long-press is the shortcut for "just play it."</strong> It surfaces a native
confirm (<code>Play "&lt;name&gt;" now?</code>) before starting playback, so an accidental
hold never blows away a resume position silently.</li>
<li><strong>The long-press must not fight the carousel.</strong> Detection cancels if the
pointer moves more than ~10 px (a horizontal scroll of the row), so holding
to scroll never triggers play.</li>
<li><strong>Episodes still obey §5B.1</strong> — a home tap on an episode opens the series
Focus View, never a bare Episode page, so the series context loads.</li>
</ul>
<p>This behavior lives in <code>MediaCard</code> (<code>onLongPress</code> prop + pointer-based
detection) so any surface can opt in; today the Home carousels are the only
opt-in. Grids and other surfaces keep tap-to-open with no long-press.</p>
<hr />
<h2 id="5c-favourites"><a class="header" href="#5c-favourites">5C. Favourites</a></h2>
<p>Favouriting is a two-sided promise: the heart takes the input, and the app must
be able to give it back. This section covers both sides — where you can mark a
favourite, and where marked favourites resurface.</p>
<p>See <a href="architecture/01-rust-backend.html#favorites-system">architecture/01-rust-backend.md</a> for the layer
assignment and wire shapes.</p>
<h3 id="5c1-the-heart-appears-wherever-an-item-does"><a class="header" href="#5c1-the-heart-appears-wherever-an-item-does">5C.1 The heart appears wherever an item does</a></h3>
<p>A favourite is a property of an <em>item</em>, so the affordance follows the item
rather than living on one privileged screen. Any surface that shows a whole
item shows its heart.</p>
<div class="table-wrapper"><table><thead><tr><th>Surface</th><th>Heart position</th><th>Notes</th></tr></thead><tbody>
<tr><td>Movie / Series detail hero</td><td>In the button row, after Play and Download</td><td>§5B.3, §5B.4</td></tr>
<tr><td>Episode Focus View hero</td><td>Same row as Play / Download</td><td>§5B.2</td></tr>
<tr><td>Album, Artist, Playlist detail</td><td>In the header button row</td><td>§5.2</td></tr>
<tr><td>Media card (any grid or carousel)</td><td>Top-right overlay on the artwork</td><td>Hidden on server-only (greyed) cards</td></tr>
<tr><td>Mini player</td><td>Right of the track metadata</td><td>Existing behaviour, unchanged</td></tr>
<tr><td>Full player</td><td>Secondary controls row</td><td>§3.2 — <strong>not yet built</strong>, see §5C.5</td></tr>
</tbody></table>
</div>
<p>Rules:</p>
<ul>
<li><strong>The heart never competes with the card.</strong> On a media card it is its own
button and swallows the tap, so hearting an item never also opens or plays
it, and never triggers the §5B.5 long-press.</li>
<li><strong>State is shown, not guessed.</strong> A filled heart means the <em>server</em> considers
the item a favourite (or you just tapped it). An item favourited in Jellyfin
Web, on another device, or by another client renders filled here without
being touched in JellyTau.</li>
<li><strong>Feedback is immediate.</strong> The heart fills on tap and a toast confirms;
neither waits for the server round-trip.</li>
</ul>
<h3 id="5c2-three-ways-back-to-what-you-favourited"><a class="header" href="#5c2-three-ways-back-to-what-you-favourited">5C.2 Three ways back to what you favourited</a></h3>
<p>Favourites are not one destination — they are a lens, and the right surface
depends on whether the user is <em>browsing</em>, <em>deciding</em>, or <em>hunting</em>.</p>
<pre><code class="language-mermaid">flowchart TB
User[User wants their favourites] --&gt; How{Intent}
How --&gt;|Passive: show me something| Home[Home carousels&lt;br/&gt;Favourite Movies / Shows / Music]
How --&gt;|Deliberate: my whole collection| Page[Favourites page&lt;br/&gt;/library/favorites]
How --&gt;|Narrowing: within this library| Filter[Favourites filter&lt;br/&gt;on a library page]
Home --&gt;|See all| Page
Page --&gt; Detail[Item detail page]
Filter --&gt; Detail
</code></pre>
<p><strong>Home carousels.</strong> Rows for favourite movies, shows and music sit below
<em>Recently Added</em>. A row with nothing in it <strong>does not render</strong> — a fresh install
shows no empty favourite rows. Each row ends with <em>See all</em>, landing on the
matching tab of the Favourites page.</p>
<p><strong>The Favourites page</strong> (<code>/library/favorites</code>) is the complete collection,
scoped by tabs:</p>
<pre><code>┌─────────────────────────────────────────────────┐
│ [←] Favourites │
│ ┌─────┬────────┬───────┬───────┐ │
│ │ All │ Movies │ Shows │ Music │ ← scope tabs │
│ └─────┴────────┴───────┴───────┘ │
│ │
│ ┌────┐┌────┐┌────┐┌────┐┌────┐ │
│ │ ♥ ││ ♥ ││ ♥ ││ ♥ ││ ♥ │ grid/list │
│ └────┘└────┘└────┘└────┘└────┘ per §5A │
└─────────────────────────────────────────────────┘
</code></pre>
<ul>
<li>Cards obey §5A in full — shape follows the media, so a mixed <em>All</em> tab reads
as posters, squares and thumbnails side by side rather than one forced shape.</li>
<li>Reached from a card on the library overview (<code>/library</code>) and from <em>See all</em>
on any home favourites row.</li>
<li>Sorted by name. Jellyfin does not record <em>when</em> an item was favourited, so
"recently favourited" is not offerable — see §5C.5.</li>
<li>Empty state, per tab: <em>"Nothing favourited yet — tap the heart on anything
you like."</em></li>
</ul>
<p><strong>The in-library filter</strong> is for narrowing where the user already is: a
favourites toggle in the header of the Movies, TV and Music browse pages,
filtering the current list in place. It is <strong>session-scoped and not persisted</strong>
a sticky filter that silently hides most of a library reads as data loss on the
next launch.</p>
<h3 id="5c3-removing-a-favourite-removes-it-everywhere-at-once"><a class="header" href="#5c3-removing-a-favourite-removes-it-everywhere-at-once">5C.3 Removing a favourite removes it everywhere, at once</a></h3>
<p>Un-hearting an item on the Favourites page removes its card from the grid
immediately; the same item disappears from the home rows and shows an empty
heart on its detail page without a manual refresh. The reverse holds for
favouriting. There is no confirmation prompt — the action is one tap to undo.</p>
<h3 id="5c4-offline"><a class="header" href="#5c4-offline">5C.4 Offline</a></h3>
<ul>
<li><strong>Marking works offline.</strong> The heart fills, the toast confirms, and the change
is held locally.</li>
<li><strong>It reaches the server on reconnect</strong>, without the user returning to the
screen where they made it.</li>
<li><strong>Browsing offline shows favourites among media on the device</strong>, subject to
the same "Show all server media" gate as every other browse surface (§7.2) —
with the gate off, an empty Favourites tab means <em>nothing favourited is
downloaded</em>, and the page does not quietly fall back to the server catalog.</li>
</ul>
<h3 id="5c5-known-deviations"><a class="header" href="#5c5-known-deviations">5C.5 Known deviations</a></h3>
<ul>
<li><strong>The full player has no heart.</strong> §3.2 and §3.3 list a Favorite button among
the full player's secondary controls; it was never built, and this pass does
not add it. The mini player heart above it is the only in-player affordance.
<em>(UR-067)</em></li>
<li><strong>No "recently favourited" sort.</strong> Jellyfin's API does not expose a favourite
timestamp, so favourites can only be ordered by name. Recording the
timestamp locally at toggle time would order <em>this device's</em> favourites only,
which is worse than a consistent name sort.</li>
<li><strong>Music is one tab, not three.</strong> The Music scope mixes albums, artists and
tracks in a single grid rather than offering sub-tabs. Acceptable while
favourite counts are small; revisit if the tab becomes unscannable.</li>
</ul>
<hr />
<h2 id="6-search-flow"><a class="header" href="#6-search-flow">6. Search Flow</a></h2>
<p>Search is <strong>context-scoped</strong>: what you are looking at when you start a search
determines what the search covers. A search begun inside the Music library
searches music. A search begun from Home or the top-level library page searches
everything. The scope is always shown, and always overridable.</p>
<h3 id="61-scope-is-inherited-from-context"><a class="header" href="#61-scope-is-inherited-from-context">6.1 Scope is inherited from context</a></h3>
<pre><code class="language-mermaid">flowchart TB
Start[User starts a search] --&gt; Where{Where from?}
Where --&gt;|Home &amp;#40;/&amp;#41;| All[Scope: All]
Where --&gt;|Library root &amp;#40;/library&amp;#41;| All
Where --&gt;|Search tab| All
Where --&gt;|Inside Music| Music[Scope: Music]
Where --&gt;|Inside Movies| Movies[Scope: Movies]
Where --&gt;|Inside TV| TV[Scope: TV]
All --&gt; Chips[Filter chips shown&lt;br/&gt;All chip selected]
Music --&gt; Chips2[Filter chips shown&lt;br/&gt;Music chip preselected]
Movies --&gt; Chips2
TV --&gt; Chips2
Chips --&gt; Results[Results, grouped by type]
Chips2 --&gt; Results
Results --&gt; Change{User taps a chip}
Change --&gt; Rescope[Re-run search at new scope&lt;br/&gt;query preserved]
Rescope --&gt; Results
</code></pre>
<p><strong>Rules:</strong></p>
<ul>
<li>
<p><strong>Context sets the <em>initial</em> chip, never a locked filter.</strong> Entering search
from TV preselects the TV chip; the user can tap "All" to widen without
retyping the query. Scope is a starting point, not a cage.</p>
</li>
<li>
<p><strong>Home, <code>/library</code>, and the search tab all start at "All".</strong> These are the
places a user has expressed no narrower intent.</p>
</li>
<li>
<p><strong>Changing scope preserves the query</strong> and re-runs the search. Changing the
query preserves the scope.</p>
</li>
<li>
<p><strong>Scope maps to item types</strong>, resolved at the point of search:</p>
<div class="table-wrapper"><table><thead><tr><th>Chip</th><th><code>includeItemTypes</code></th></tr></thead><tbody>
<tr><td>All</td><td><em>(unset — every type)</em></td></tr>
<tr><td>Music</td><td><code>MusicAlbum</code>, <code>MusicArtist</code>, <code>Audio</code>, <code>Playlist</code></td></tr>
<tr><td>Movies</td><td><code>Movie</code></td></tr>
<tr><td>TV</td><td><code>Series</code>, <code>Episode</code></td></tr>
</tbody></table>
</div></li>
<li>
<p><strong>Chips render under the search bar</strong>, on both the dedicated search page and
the in-library header search. They are horizontally scrollable if they
overflow, never wrapped onto a second row.</p>
</li>
</ul>
<h3 id="62-search-page-layout"><a class="header" href="#62-search-page-layout">6.2 Search page layout</a></h3>
<pre><code>┌─────────────────────────────────────────┐
│ [🔍 Search...] [✕] │
│ │
│ ( All ) (•Music•) ( Movies ) ( TV ) │ ← scope chips
│ │
│ Songs ──────────────────────────── │
│ ♪ Song Title - Artist 3:45 │
│ ♪ Song Title - Artist 4:12 │
│ See all (23) │
│ │
│ Albums ─────────────────────────── │
│ [Cover] Album Title │
│ See all (8) │
│ │
│ Artists ────────────────────────── │
│ ( Photo ) Artist Name │
│ See all (5) │
└─────────────────────────────────────────┘
</code></pre>
<ul>
<li>Results stay <strong>grouped by type</strong> even when a scope is selected — a Music
search still separates Songs / Albums / Artists.</li>
<li>Each group shows a bounded preview with a <strong>See all (n)</strong> affordance rather
than an unbounded list, so no single type can bury the others.</li>
<li>Live search is <strong>debounced</strong> as the user types; a query that becomes empty
clears results rather than searching for the empty string.</li>
</ul>
<h3 id="63-result-group-order-is-user-configurable"><a class="header" href="#63-result-group-order-is-user-configurable">6.3 Result group order is user-configurable</a></h3>
<p>Which <em>kind</em> of thing a user is usually searching for is personal: a
music-first user wants Songs at the top, a TV-first user wants Shows. Rather
than guessing, the group order is a setting.</p>
<pre><code class="language-mermaid">flowchart TB
Settings[Settings → Search] --&gt; List[Draggable list of result groups]
List --&gt; Drag[User drags a group up or down]
Drag --&gt; Persist[Order persisted]
Persist --&gt; Render[Rendering a result set]
Scope[Active scope chip §6.1] --&gt; Render
Render --&gt; Filter[1 - Drop groups outside the active scope]
Filter --&gt; Sort[2 - Sort remaining groups by user order]
Sort --&gt; Prune[3 - Omit groups with no results]
Prune --&gt; Show[Render]
</code></pre>
<p><strong>Scope and order compose — they are two independent axes.</strong> The scope chip
decides <em>which</em> groups are eligible; the settings list decides <em>what sequence</em>
the eligible ones appear in. Order is preserved as a relative ranking, never
renumbered per scope:</p>
<ul>
<li>Scope <strong>Music</strong> with order <code>Movies → Songs → Albums → Artists → TV</code> renders
<code>Songs → Albums → Artists</code>. Movies and TV are filtered out; the surviving
groups keep their relative order.</li>
<li>Scope <strong>All</strong> with the same setting renders all five in exactly that order.</li>
<li><strong>Changing scope never rewrites the saved order.</strong> A user who narrows to
Music and back to All sees their original arrangement intact.</li>
</ul>
<p><strong>Rules:</strong></p>
<ul>
<li><strong>Drag and drop to reorder</strong>, in a settings list showing every result group
(Songs, Albums, Artists, Movies, TV Shows).</li>
<li><strong>The order applies to grouped results everywhere</strong> — the search page and
the in-library header search alike.</li>
<li><strong>Order is presentation-only.</strong> It never changes which results are returned
or how they are ranked <em>within</em> a group, only the sequence groups appear in.</li>
<li><strong>Empty groups are skipped, not gapped.</strong> A group with no results is omitted
entirely; it does not reserve space or leave a stray heading.</li>
<li><strong>A sensible default ships</strong> (Songs → Albums → Artists → Movies → TV Shows)
so the setting is an adjustment, never a prerequisite.</li>
<li><strong>Keyboard/accessible reordering must exist</strong> alongside dragging — a
drag-only control is unusable with a screen reader or without a pointer.</li>
</ul>
<h3 id="64-known-deviations"><a class="header" href="#64-known-deviations">6.4 Known deviations</a></h3>
<p>Recorded so the gap between this spec and the build is explicit.</p>
<ul>
<li><strong>Scope is not implemented.</strong> The in-library header search calls the same
unscoped query as the global search page, so searching inside TV returns
music. The backend already accepts <code>includeItemTypes</code> on both the online and
offline paths, and the per-page list search already uses it — only the global
path ignores it. <em>(UR-049)</em></li>
<li><strong>Filter chips do not exist</strong> on either search surface. <em>(UR-049)</em></li>
<li><strong>Group order is hardcoded</strong> to Music → Movies → TV in the results markup,
with no setting. <em>(UR-050)</em></li>
</ul>
<hr />
<h2 id="7-download-flows"><a class="header" href="#7-download-flows">7. Download Flows</a></h2>
<h3 id="71-initiating-downloads"><a class="header" href="#71-initiating-downloads">7.1 Initiating Downloads</a></h3>
<pre><code class="language-mermaid">flowchart TB
User[User on Album/Track Page] --&gt; ClickDownload[Click Download Button]
ClickDownload --&gt; CheckType{Download Type?}
CheckType --&gt;|Single Track| DownloadTrack[Download Single File]
CheckType --&gt;|Album| DownloadAlbum[Download All Tracks]
CheckType --&gt;|Artist| ShowOptions[Show Options Dialog]
ShowOptions --&gt; UserChoice{User Choice}
UserChoice --&gt;|Discography| DownloadAll[Download All Albums]
UserChoice --&gt;|Select Albums| AlbumPicker[Album Selection UI]
DownloadTrack --&gt; QueueDownload[Queue in Download Manager]
DownloadAlbum --&gt; QueueMultiple[Queue Multiple Files]
QueueDownload --&gt; ShowProgress[Show Progress Ring&lt;br/&gt;on Download Button]
QueueMultiple --&gt; ShowProgress
ShowProgress --&gt; DownloadActive[Download Active:&lt;br/&gt;Button shows % complete]
</code></pre>
<p><strong>Download Button States:</strong></p>
<pre><code>States:
1. [⬇] Available - Gray outline
2. [○ 45%] Downloading - Blue ring progress
3. [✓] Downloaded - Green checkmark
4. [!] Failed - Red with retry option
5. [⏸] Paused - Yellow pause icon
</code></pre>
<h3 id="72-downloads--a-browsable-offline-library-not-a-flat-list"><a class="header" href="#72-downloads--a-browsable-offline-library-not-a-flat-list">7.2 Downloads = a browsable offline library, not a flat list</a></h3>
<p><strong>The central idea:</strong> "my downloads" is not a list of file-transfer rows — it is
<em>the library, filtered to what's on the device</em>. A user who has downloaded three
seasons of a show and two albums thinks in terms of shows and albums, not
seventy-odd individual episode/track transfers. So the primary Downloads surface
<strong>reuses the library browse screens</strong>, scoped to downloaded content, and keeps
the transfer-progress list as a secondary "Transfers" view for the <em>act</em> of
downloading.</p>
<p>This splits one overloaded page into two clear jobs:</p>
<div class="table-wrapper"><table><thead><tr><th>Surface</th><th>Answers</th><th>Reuses</th></tr></thead><tbody>
<tr><td><strong>Downloaded</strong> (browse)</td><td>"What do I have offline, and let me play it"</td><td>Library grids, detail pages, cards (§5A)</td></tr>
<tr><td><strong>Transfers</strong> (activity)</td><td>"What is downloading right now, and control it"</td><td>The existing progress-row list</td></tr>
</tbody></table>
</div>
<pre><code class="language-mermaid">flowchart TB
Nav[Open Downloads] --&gt; Downloads[/downloads]
Downloads --&gt; View{View}
View --&gt;|Downloaded &amp;#40;default&amp;#41;| Browse[Offline library browse]
View --&gt;|Transfers| Activity[Transfer activity list]
Browse --&gt; Libs[Libraries — only those with&lt;br/&gt;downloaded content]
Libs --&gt; Grid[Library grid, offline-scoped&lt;br/&gt;same cards/layout as online §5A]
Grid --&gt; Detail[Detail page&lt;br/&gt;same as online]
Detail --&gt; Play[Play from local file]
Detail --&gt; Remove[Remove download&lt;br/&gt;frees space, keeps browsable? — see rules]
Activity --&gt; Rows[Per-transfer rows:&lt;br/&gt;downloading / queued / paused / failed /&lt;br/&gt;waiting-for-WiFi]
Rows --&gt; Ctl[Pause / Resume / Cancel / Retry]
</code></pre>
<p><strong>Why reuse the library screens (not a bespoke list):</strong></p>
<ul>
<li><strong>One mental model.</strong> Browsing offline should feel identical to browsing
online — same grids, same card shapes, same detail pages, same play action.
The only difference is <em>what's present</em>, not <em>how it looks</em>.</li>
<li><strong>It already works in the backend.</strong> The offline repository's <code>get_items</code>
already returns downloaded items <strong>plus</strong> their containers (an album with any
downloaded track, a series/season with any downloaded episode). That is a
browsable tree today — see §7.4.</li>
<li><strong>It scales.</strong> A flat completed-list becomes unusable at a few dozen items; a
browsable library does not.</li>
</ul>
<h3 id="73-the-downloaded-browse-surface"><a class="header" href="#73-the-downloaded-browse-surface">7.3 The Downloaded browse surface</a></h3>
<pre><code>┌─────────────────────────────────────────────┐
│ Downloads │
│ ( Downloaded ) ( Transfers ) ← view switch
│ │
│ [~ 3.4 GB on device · 12 items] Manage ▸ │ ← storage summary
│ │
│ Music │ ← only libraries that
│ ┌────┐┌────┐┌────┐ │ have downloaded content
│ │alb ││alb ││art │ │
│ └────┘└────┘└────┘ │
│ │
│ TV │
│ ┌────┐┌────┐ │
│ │show││show│ │
│ └────┘└────┘ │
└─────────────────────────────────────────────┘
</code></pre>
<p><strong>Rules:</strong></p>
<ul>
<li><strong>Libraries with nothing downloaded are omitted</strong>, not shown empty. If only
music is downloaded, only Music appears.</li>
<li><strong>Cards, grids, and detail pages are the library's own</strong> (§5A) — offline
browse is the same components with an offline-scoped data source, never a
parallel re-implementation.</li>
<li><strong>A downloaded badge / "on device" affordance</strong> distinguishes fully-downloaded
from partially-downloaded containers (e.g. a season with 6 of 10 episodes).</li>
<li><strong>Disk usage is shown where the user already looks</strong>, in familiar units — see
§7.3.1.</li>
<li><strong>Play always plays the local file</strong> here; nothing on this surface streams.</li>
<li><strong>Remove is available at every level</strong> — item, album/season, series — and
states clearly what it frees. Removing the last downloaded child of a
container removes the container from the browse.</li>
<li><strong>This surface works identically online and offline.</strong> It is "what's on the
device," a question whose answer does not depend on connectivity. It must not
wait for, or be emptied by, server reachability.</li>
</ul>
<h4 id="731-disk-usage--familiar-in-place-not-a-separate-audit"><a class="header" href="#731-disk-usage--familiar-in-place-not-a-separate-audit">7.3.1 Disk usage — familiar, in place, not a separate audit</a></h4>
<p>Users want to know what each thing costs on disk, but that information has to
feel like the storage views they already know (phone Settings → Storage, a
file browser), not a developer's byte dump.</p>
<ul>
<li><strong>Size rides along with the item, on the card and the detail page</strong> — a small
secondary label (<code>1.2 GB</code>, <code>340 MB</code>, <code>48 MB</code>), never a separate "storage
report" screen the user has to go find.</li>
<li><strong>Containers show their total.</strong> A series shows the sum of its downloaded
episodes; an album the sum of its tracks; a season its own subtotal. The
number a user sees on the "Breaking Bad" card is what removing it frees.</li>
<li><strong>Human units, rounded, consistent.</strong> Binary or decimal is a choice — pick one
and use it everywhere. Show 23 significant figures (<code>1.2 GB</code>, not
<code>1,283,048,192 bytes</code> and not <code>1.28394 GB</code>).</li>
<li><strong>A single device total sits at the top</strong> of the Downloaded surface
(<code>3.4 GB on device · 12 items</code>) so the headline number is answered before the
user scans. It reconciles with the sum of what's listed.</li>
<li><strong>Remove restates the reclaim</strong> in the same units at the point of action
("Remove download · frees 1.2 GB"), so the cost of keeping vs. freeing is
legible exactly when the user decides.</li>
<li><strong>Sort/filter by size is a reasonable enhancement</strong> ("biggest first" to find
what to clear) but is not required for v1.</li>
</ul>
<p>The bytes-on-disk per item are a backend fact (the download manager writes the
files and can stat them); this is a display and aggregation task, not new
tracking. See §7.7 deviations for what's missing today.</p>
<h3 id="74-transfers-activity-view"><a class="header" href="#74-transfers-activity-view">7.4 Transfers (activity) view</a></h3>
<p>The existing progress-row list, unchanged in spirit, demoted to a secondary tab.
It is about <em>transfers in flight</em>, so it shows only rows that are doing or
waiting to do something:</p>
<ul>
<li><strong>States:</strong> downloading (with progress), queued, paused, failed,
waiting-for-WiFi (§7.5).</li>
<li><strong>Controls:</strong> Pause / Resume / Cancel / Retry per row; the 3-concurrent cap
and auto-pump are backend concerns and are not surfaced as manual controls.</li>
<li><strong>Completed transfers fall off this view</strong> once done — the finished item lives
in Downloaded, not here. A transient "just finished" confirmation is fine; a
permanent completed-list is not (that's what Downloaded is for).</li>
<li><strong>Empty state</strong> points at the library: "Nothing downloading. Browse your
library and tap download to save media for offline."</li>
</ul>
<h3 id="75-navigation--entry-points"><a class="header" href="#75-navigation--entry-points">7.5 Navigation &amp; entry points</a></h3>
<ul>
<li>Reached via the account menu (§1.2) and, on desktop, the header Downloads
link/icon → <code>/downloads</code>.</li>
<li><code>/downloads</code> opens on <strong>Downloaded</strong> by default; <strong>Transfers</strong> is one tap away
and should draw attention (badge/count) only while transfers are active.</li>
<li>Initiating a download is unchanged (§7.1): the download button lives on
item/album/series detail pages. The Downloads page manages and browses; it is
not where you start a download.</li>
</ul>
<h3 id="77-known-deviations"><a class="header" href="#77-known-deviations">7.7 Known deviations</a></h3>
<p>Recorded so the gap between this spec and the build is explicit.</p>
<ul>
<li><strong>Downloads is a flat two-tab list today</strong> (Active / Completed), rendering one
row per individual transfer with no browsing, grouping, or reuse of the
library screens. Completed downloads never collapse into their album/series.
<em>(UR-055)</em></li>
<li><strong>No offline-scoped browse entry point exists in the client.</strong> All browsing
goes through the hybrid repository, which merges cache <strong>and</strong> server; there is
no way to ask for "downloaded content only" as a browse surface. The offline
repository supports it (§7.2) but is not reachable independently. <em>(UR-055,
DR-082)</em></li>
<li><strong>The "on device" storage summary and per-container remove</strong> are absent from
the completed list. <em>(UR-055, UR-056)</em></li>
<li><strong>Per-item disk usage is not displayed anywhere.</strong> Cards and detail pages show
no size; there is no device total, no container subtotal, and Remove does not
state what it frees. <em>(UR-056)</em></li>
</ul>
<hr />
<h2 id="8-settings--account-flows"><a class="header" href="#8-settings--account-flows">8. Settings &amp; Account Flows</a></h2>
<h3 id="81-settings-navigation"><a class="header" href="#81-settings-navigation">8.1 Settings Navigation</a></h3>
<pre><code class="language-mermaid">flowchart TB
User[User] --&gt; NavChoice{Navigation Path}
NavChoice --&gt;|Desktop| HeaderSettings[Header: Click Settings Link]
NavChoice --&gt;|Mobile| OverflowMenu[Click Overflow Menu&lt;br/&gt;→ Settings]
NavChoice --&gt;|Direct| TypeURL[Navigate to /settings]
HeaderSettings --&gt; SettingsPage[Settings Page&lt;br/&gt;/settings]
OverflowMenu --&gt; SettingsPage
TypeURL --&gt; SettingsPage
SettingsPage --&gt; ShowSections[Show Sections:&lt;br/&gt;- Account&lt;br/&gt;- Playback&lt;br/&gt;- Downloads&lt;br/&gt;- Appearance&lt;br/&gt;- About]
ShowSections --&gt; UserClick{User Clicks Section}
UserClick --&gt;|Account| AccountSettings[Account Settings:&lt;br/&gt;- Server URL&lt;br/&gt;- Username&lt;br/&gt;- Logout button]
UserClick --&gt;|Playback| PlaybackSettings[Playback Settings:&lt;br/&gt;- Gapless playback&lt;br/&gt;- Volume normalization&lt;br/&gt;- Crossfade duration]
UserClick --&gt;|Downloads| DownloadSettings[Download Settings:&lt;br/&gt;- Max concurrent&lt;br/&gt;- WiFi only&lt;br/&gt;- Storage location&lt;br/&gt;- Auto-cache next tracks]
UserClick --&gt;|Appearance| AppearanceSettings[Appearance Settings:&lt;br/&gt;- Dark mode&lt;br/&gt;- Accent color]
</code></pre>
<p><strong>Navigation to Settings:</strong></p>
<ul>
<li><strong>Desktop:</strong> Click "Settings" link in header navigation</li>
<li><strong>Mobile:</strong> Click three-dot overflow menu → Select "Settings"</li>
<li><strong>Direct:</strong> Navigate to <code>/settings</code> route</li>
</ul>
<p><strong>Settings apply instantly.</strong> Every control on the Settings page persists the
moment the user changes it — toggling a switch, picking a level, or releasing a
slider writes that setting immediately. There is <strong>no "Save" button</strong> and no
save/dirty state to reason about; leaving the page never risks losing a change.
Sliders update their live readout while dragging but only persist on release
(<code>change</code>, not each <code>input</code> tick) to avoid flooding the backend.</p>
<h3 id="82-logout-flow"><a class="header" href="#82-logout-flow">8.2 Logout Flow</a></h3>
<pre><code class="language-mermaid">flowchart TB
AnyScreen[Any Screen] --&gt; ClickLogout[Click Logout Button&lt;br/&gt;in Header]
ClickLogout --&gt; ConfirmDialog[Show Confirmation:&lt;br/&gt;"Log out of [Server]?"]
ConfirmDialog --&gt; UserConfirm{User Confirms?}
UserConfirm --&gt;|No| CancelLogout[Cancel - Stay on Current Screen]
UserConfirm --&gt;|Yes| StopPlayer[Stop Playback]
StopPlayer --&gt; ClearToken[Delete Token from Keyring]
ClearToken --&gt; ClearState[Clear App State:&lt;br/&gt;- Player state&lt;br/&gt;- Queue&lt;br/&gt;- Current screen]
ClearState --&gt; NavLogin[Navigate to Login Screen&lt;br/&gt;/login]
NavLogin --&gt; ShowLogin[Show Login Screen:&lt;br/&gt;- No Header&lt;br/&gt;- No Bottom Nav&lt;br/&gt;- No MiniPlayer]
</code></pre>
<p><strong>Logout Button Location:</strong></p>
<ul>
<li>Always visible in header user menu (logout icon)</li>
<li>Accessible from any authenticated screen</li>
</ul>
<hr />
<h2 id="9-background--lock-screen-behavior"><a class="header" href="#9-background--lock-screen-behavior">9. Background &amp; Lock Screen Behavior</a></h2>
<h3 id="91-audio-playback-in-background-android"><a class="header" href="#91-audio-playback-in-background-android">9.1 Audio Playback in Background (Android)</a></h3>
<pre><code class="language-mermaid">flowchart TB
Playing[Audio Playing] --&gt; Background{User Action}
Background --&gt;|Home Button| AppBackground[App to Background]
Background --&gt;|Screen Lock| ScreenLock[Screen Locked]
AppBackground --&gt; ContinuePlay[Playback Continues]
ScreenLock --&gt; ContinuePlay
ContinuePlay --&gt; ShowNotification[Show Media Notification:&lt;br/&gt;- Artwork&lt;br/&gt;- Title/Artist&lt;br/&gt;- Play/Pause&lt;br/&gt;- Next/Previous]
ShowNotification --&gt; LockScreen[Lock Screen Controls:&lt;br/&gt;Media Session Integration]
LockScreen --&gt; UserInteract{User Interaction}
UserInteract --&gt;|Tap Notification| OpenApp[Open App to Last Screen&lt;br/&gt;with MiniPlayer]
UserInteract --&gt;|Lock Screen Controls| SendCommand[Send Command to Player]
UserInteract --&gt;|BLE Headset Button| HeadsetControl[AVRCP Command]
</code></pre>
<p><strong>Notification Layout (Android):</strong></p>
<pre><code>┌─────────────────────────────────────────┐
│ [Artwork] Song Title │
│ Artist Name │
│ Album Name │
│ │
│ [⏮] [⏸] [⏭] [✕] │
└─────────────────────────────────────────┘
</code></pre>
<h3 id="92-video-playback-in-background-android--pip--background-audio"><a class="header" href="#92-video-playback-in-background-android--pip--background-audio">9.2 Video Playback in Background (Android — PiP &amp; Background Audio)</a></h3>
<p>Leaving the app while a <strong>local video</strong> is playing does not simply pause it.
What happens depends on which background behaviour is active. The two are
<strong>mutually exclusive</strong>, and both apply <strong>only to locally-rendering video</strong>
audio-only playback, library/menu browsing, and remote/cast sessions never
trigger PiP (see decision gate below).</p>
<pre><code class="language-mermaid">flowchart TB
Leave[User leaves app&lt;br/&gt;Home / gesture / screen lock] --&gt; Gate{Local video surface&lt;br/&gt;actively rendering?&lt;br/&gt;canEnterPip}
Gate --&gt;|No — audio, browsing,&lt;br/&gt;or remote/cast| Normal[App backgrounds normally&lt;br/&gt;audio, if any, continues via&lt;br/&gt;media notification &amp;#40;§9.1&amp;#41;]
Gate --&gt;|Yes| Mode{Background mode armed?}
Mode --&gt;|Background-audio toggle ON&lt;br/&gt;UR-040| Handoff[Hand off to native audio service&lt;br/&gt;WebView &amp;lt;video&amp;gt; torn down,&lt;br/&gt;video decode stops, audio continues]
Mode --&gt;|Default&lt;br/&gt;UR-041| PiP[Auto-enter Picture-in-Picture&lt;br/&gt;on onUserLeaveHint]
PiP --&gt; PiPWindow[Floating PiP window:&lt;br/&gt;- Video keeps rendering into surface&lt;br/&gt;- WebView hidden&lt;br/&gt;- Play/Pause RemoteAction&lt;br/&gt; &amp;#40;reflects live player state&amp;#41;]
PiPWindow --&gt; PiPReturn{User action}
PiPReturn --&gt;|Tap window| Restore[Return to full player&lt;br/&gt;WebView restored, surface re-fit]
PiPReturn --&gt;|Close window| Stop[Playback stops]
Handoff --&gt; Foreground[On return to foreground:&lt;br/&gt;resume WebView video at position]
</code></pre>
<p><strong>Key rules:</strong></p>
<ul>
<li><strong>Video-only gate.</strong> Auto-PiP is guarded by the native <code>canEnterPip</code> check
(local video surface actively rendering). Audio playback and menu/library
browsing background normally; remote/cast sessions render nothing locally, so
a PiP window would be an empty box and is refused. <em>(UR-041, IR-026)</em></li>
<li><strong>Only one background behaviour at a time.</strong> The background-audio toggle
(UR-040) disarms auto-PiP while it is on, so a video is either handed to the
audio service <em>or</em> floated in PiP, never both.</li>
<li><strong>PiP controls track the player.</strong> The play/pause RemoteAction in the PiP
window reflects the live player state and updates on every playback-state
change, not only when the button is pressed. <em>(DR-053)</em></li>
<li><strong>Non-disruptive transition.</strong> ExoPlayer keeps rendering into the same
surface across enter/exit, so entering or leaving PiP never interrupts the
video; on exit the surface is re-fit to full-screen bounds. <em>(DR-053)</em></li>
</ul>
<p><strong>PiP window (Android):</strong></p>
<pre><code> ┌───────────────────┐
│ │
│ ▶ video frame │
│ │
│ [⏸] │ ← play/pause RemoteAction
└───────────────────┘
sized to the video's aspect ratio
</code></pre>
<hr />
<h2 id="10-error-states--edge-cases"><a class="header" href="#10-error-states--edge-cases">10. Error States &amp; Edge Cases</a></h2>
<h3 id="101-network-loss-during-streaming"><a class="header" href="#101-network-loss-during-streaming">10.1 Network Loss During Streaming</a></h3>
<pre><code class="language-mermaid">flowchart TB
Streaming[Streaming Audio/Video] --&gt; LoseNetwork[Network Connection Lost]
LoseNetwork --&gt; CheckLocal{Local Copy&lt;br/&gt;Available?}
CheckLocal --&gt;|Yes| SwitchLocal[Switch to Local Playback&lt;br/&gt;Seamlessly]
CheckLocal --&gt;|No| ShowBuffer[Show Buffering Spinner]
ShowBuffer --&gt; WaitReconnect[Wait for Reconnection&lt;br/&gt;30 second timeout]
WaitReconnect --&gt; Reconnect{Reconnected?}
Reconnect --&gt;|Yes| Resume[Resume Streaming]
Reconnect --&gt;|No| ShowError[Show Error Toast:&lt;br/&gt;"Unable to stream.&lt;br/&gt;Check connection."]
ShowError --&gt; OfferRetry[Offer Retry Button]
ShowError --&gt; OfferDownload[Offer "Download for Offline"]
</code></pre>
<h3 id="102-server-unreachable"><a class="header" href="#102-server-unreachable">10.2 Server Unreachable</a></h3>
<pre><code class="language-mermaid">flowchart TB
Action[User Action Requires Server] --&gt; TryConnect[Attempt Connection]
TryConnect --&gt; Timeout{Connection&lt;br/&gt;Timeout?}
Timeout --&gt;|Yes| ShowError[Show Error:&lt;br/&gt;"Server unreachable"]
Timeout --&gt;|No| Success[Action Succeeds]
ShowError --&gt; OfferOptions[Offer Options:&lt;br/&gt;- Retry&lt;br/&gt;- Switch to Offline Mode&lt;br/&gt;- Change Server]
</code></pre>
<h3 id="103-download-failed"><a class="header" href="#103-download-failed">10.3 Download Failed</a></h3>
<pre><code class="language-mermaid">flowchart TB
Downloading[Download in Progress] --&gt; Failure{Failure Type?}
Failure --&gt;|Network Error| Retry[Auto-retry&lt;br/&gt;with Backoff]
Failure --&gt;|Disk Full| ShowDiskError[Show Error:&lt;br/&gt;"Not enough storage"]
Failure --&gt;|Server Error| ShowServerError[Show Error:&lt;br/&gt;"Server error"]
Retry --&gt; RetryCount{Retry Count&lt;br/&gt;&lt; 3?}
RetryCount --&gt;|Yes| Downloading
RetryCount --&gt;|No| Failed[Mark as Failed]
ShowDiskError --&gt; Failed
ShowServerError --&gt; Failed
Failed --&gt; UserAction[Show in Downloads:&lt;br/&gt;with Retry Button]
</code></pre>
<hr />
<h2 id="11-platform-specific-ux-patterns"><a class="header" href="#11-platform-specific-ux-patterns">11. Platform-Specific UX Patterns</a></h2>
<h3 id="111-android-specific"><a class="header" href="#111-android-specific">11.1 Android-Specific</a></h3>
<p><strong>Hardware Back Button:</strong></p>
<ul>
<li><strong>In Full Player:</strong> Return to previous screen, show MiniPlayer</li>
<li><strong>In Video Player:</strong> Stop playback, exit fullscreen</li>
<li><strong>In Album Detail:</strong> Return to library grid</li>
<li><strong>At Library Home:</strong> Exit app (show confirmation)</li>
</ul>
<p><strong>System Volume Buttons:</strong></p>
<ul>
<li><strong>While playing audio:</strong> Adjust playback volume</li>
<li><strong>While controlling remote session:</strong> Adjust remote session volume (shows session name in volume panel)</li>
<li><strong>In menus:</strong> Adjust system volume (default behavior)</li>
</ul>
<p><strong>Share Integration:</strong></p>
<ul>
<li>Long-press album/song → Share menu</li>
<li>Options: Share with other apps, Copy link</li>
</ul>
<h3 id="112-linux-desktop-specific"><a class="header" href="#112-linux-desktop-specific">11.2 Linux Desktop-Specific</a></h3>
<p><strong>Keyboard Shortcuts:</strong></p>
<ul>
<li><code>Space</code>: Play/Pause</li>
<li><code></code>: Next track</li>
<li><code></code>: Previous track</li>
<li><code>/</code>: Focus search</li>
<li><code>Ctrl+Q</code>: Quit</li>
</ul>
<p><strong>Window Behavior:</strong></p>
<ul>
<li>Minimize to tray (playback continues)</li>
<li>Close window (show confirmation if playing)</li>
<li>MPRIS integration for desktop media controls</li>
</ul>
<p><strong>Mouse Interactions:</strong></p>
<ul>
<li>Hover over MiniPlayer: Show additional controls (volume, queue peek)</li>
<li>Right-click: Context menu (Add to playlist, Go to artist, Download)</li>
</ul>
<hr />
<h2 id="12-ux-principles-summary"><a class="header" href="#12-ux-principles-summary">12. UX Principles Summary</a></h2>
<h3 id="121-core-principles"><a class="header" href="#121-core-principles">12.1 Core Principles</a></h3>
<ol>
<li>
<p><strong>Playback Persistence:</strong></p>
<ul>
<li>Audio playback never stops unless user explicitly stops it</li>
<li>MiniPlayer visible on all screens (except video/login)</li>
<li>Queue and position preserved across navigation</li>
</ul>
</li>
<li>
<p><strong>Non-Blocking UI:</strong></p>
<ul>
<li>Downloads happen in background</li>
<li>Sync operations never block user interaction</li>
<li>Optimistic updates (favorite, progress) with background sync</li>
</ul>
</li>
<li>
<p><strong>Offline-First:</strong></p>
<ul>
<li>Downloaded content works offline</li>
<li>Seamless switch between online/offline</li>
<li>Progress and preferences saved locally</li>
</ul>
</li>
<li>
<p><strong>Progressive Disclosure:</strong></p>
<ul>
<li>Simple defaults, advanced options hidden</li>
<li>Context menus for secondary actions</li>
<li>Settings organized by category</li>
</ul>
</li>
<li>
<p><strong>Responsive Design:</strong></p>
<ul>
<li>Mobile-first UI</li>
<li>Desktop enhancements (hover states, keyboard shortcuts)</li>
<li>Tablet: Grid layouts with more columns</li>
</ul>
</li>
</ol>
<h3 id="122-animation--transitions"><a class="header" href="#122-animation--transitions">12.2 Animation &amp; Transitions</a></h3>
<div class="table-wrapper"><table><thead><tr><th>Transition</th><th>Duration</th><th>Easing</th></tr></thead><tbody>
<tr><td>MiniPlayer slide up/down</td><td>300ms</td><td>ease-out</td></tr>
<tr><td>Screen navigation</td><td>200ms</td><td>ease-in-out</td></tr>
<tr><td>Video controls fade</td><td>500ms</td><td>ease-out</td></tr>
<tr><td>Download button state change</td><td>150ms</td><td>ease-in-out</td></tr>
<tr><td>Modal appear</td><td>200ms</td><td>ease-out</td></tr>
<tr><td>Toast notification</td><td>250ms</td><td>ease-in-out</td></tr>
</tbody></table>
</div>
<h3 id="123-touch-targets-mobile"><a class="header" href="#123-touch-targets-mobile">12.3 Touch Targets (Mobile)</a></h3>
<div class="table-wrapper"><table><thead><tr><th>Element</th><th>Minimum Size</th></tr></thead><tbody>
<tr><td>Bottom nav buttons</td><td>48x48 dp</td></tr>
<tr><td>List item (track, album)</td><td>Full width x 56 dp</td></tr>
<tr><td>Player controls</td><td>56x56 dp</td></tr>
<tr><td>MiniPlayer</td><td>Full width x 64 dp</td></tr>
<tr><td>Download button</td><td>40x40 dp</td></tr>
<tr><td>Favorite button</td><td>40x40 dp</td></tr>
</tbody></table>
</div>
<hr />
<h2 id="13-future-ux-enhancements"><a class="header" href="#13-future-ux-enhancements">13. Future UX Enhancements</a></h2>
<h3 id="131-planned-features"><a class="header" href="#131-planned-features">13.1 Planned Features</a></h3>
<ol>
<li>
<p><strong>Gesture Navigation:</strong></p>
<ul>
<li>Swipe up on MiniPlayer → Full player</li>
<li>Swipe down on full player → Back to previous screen</li>
<li>Swipe between tracks in full player</li>
</ul>
</li>
<li>
<p><strong>Queue Management UI (DR-020):</strong></p>
<ul>
<li>Drag to reorder</li>
<li>Swipe to remove</li>
<li>Add to queue vs. Play next</li>
</ul>
</li>
<li>
<p><strong>Sleep Timer (UR-026):</strong></p>
<ul>
<li>Accessible from full player menu</li>
<li>Presets: 15min, 30min, 1hr, End of track, End of album</li>
<li>Countdown visible in MiniPlayer</li>
</ul>
</li>
<li>
<p><strong>Home Screen (UR-034):</strong></p>
<ul>
<li>Hero banner carousel</li>
<li>Continue watching/listening</li>
<li>Recently added</li>
<li>Personalized recommendations</li>
</ul>
</li>
<li>
<p><strong>Cast/Remote Control Enhancements:</strong></p>
<ul>
<li>Picture-in-picture for remote sessions</li>
<li>Multi-room audio (play on multiple devices)</li>
<li>Handoff (transfer playback to phone from TV)</li>
</ul>
</li>
</ol>
<h3 id="132-accessibility-enhancements"><a class="header" href="#132-accessibility-enhancements">13.2 Accessibility Enhancements</a></h3>
<ul>
<li>Screen reader optimization</li>
<li>High contrast mode</li>
<li>Larger text option</li>
<li>Voice control integration</li>
<li>Haptic feedback for controls</li>
</ul>
<hr />
<p>This UX flow documentation should be updated as new features are implemented and user feedback is incorporated.</p>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="architecture/09-security.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="specs/index.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="architecture/09-security.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="specs/index.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script>
window.playground_copyable = true;
</script>
<script src="elasticlunr.min.js"></script>
<script src="mark.min.js"></script>
<script src="searcher.js"></script>
<script src="clipboard.min.js"></script>
<script src="highlight.js"></script>
<script src="book.js"></script>
<!-- Custom JS scripts -->
</div>
</body>
</html>