# JellyTau UX Flows & Screen Transitions This document describes the expected user experience flows, screen transitions, and navigation patterns in JellyTau. --- ## 1. Core Navigation Structure ### 1.1 Navigation System JellyTau uses a unified navigation system with a bottom navigation bar visible on all platforms (mobile and desktop) and additional header navigation for desktop. **Bottom Navigation Bar (All Platforms - DR-045, UR-039):** The bottom navigation bar is the primary navigation and is **always visible** on all platforms (mobile and desktop) except when: - Full-screen video player is active - User is on the login screen **Bottom Nav Structure:** ``` ┌─────────────────────────────────────────┐ │ [Home] [Library] [Search] │ └─────────────────────────────────────────┘ ``` **Routes:** - **Home** → `/` (home page with carousels and featured content) - **Library** → `/library` (library selector showing all libraries) - **Search** → `/search` (dedicated search page) **Note:** Available on both mobile and desktop for consistent navigation access. **Header Navigation (Desktop):** On desktop (md breakpoint and above), the header contains: - Logo (links to `/library`) - Navigation links: Home, Library, Downloads, Settings - Search bar (inline) - Account menu (see §1.2) **Mobile Navigation:** On mobile, the header contains: - Logo - Account menu button (see §1.2) ### 1.2 Account Menu Account-level destinations — the ones that are *about the user* rather than about media — live behind a single **account menu**, anchored to the user's name/avatar at the right of the header. **Contents, in order:** ``` ┌──────────────────────────┐ │ Signed in as │ ← identity, not a menu item │ │ ├──────────────────────────┤ │ ⬇ Downloads │ │ ⚙ Settings │ │ ▦ Display │ ← grid/list preference (§5A.2) ├──────────────────────────┤ │ ⇥ Sign out │ └──────────────────────────┘ ``` **Rules:** - **One menu, both platforms.** 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. - **Anchored to identity.** 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". - **Sign out is separated** by a divider and placed last — it is destructive and must not sit adjacent to routine navigation. - **The menu is reachable from every authenticated screen**, not only from library routes. See §1.3. **Access Points Summary:** - **Downloads** → header icon (desktop) + account menu (both) - **Settings** → header nav link (desktop) + account menu (both) - **Sign out** → account menu only ### 1.3 Chrome availability The header is shared across chrome-bearing routes. Routes fall into three groups: | Route group | Header | Bottom nav | Account menu reachable? | |-------------|--------|------------|-------------------------| | `/library/*` | Yes (own layout, shared `AppHeader`) | Yes | Yes | | `/`, `/search`, `/downloads` | Yes (root-owned `AppHeader`) | Yes | Yes | | `/settings` | Own layout | No | n/a — already there | | `/player/*`, `/login` | No | No | No (by design) | 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. ### 1.4 Known deviations *(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 `AccountMenu`, 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.)* --- ## 2. Initial App Launch Flow ### 2.1 First-Time Launch ```mermaid flowchart TB Launch[App Launch] --> CheckAuth{Stored
Credentials?} CheckAuth -->|No| LoginScreen[Login Screen
/login] CheckAuth -->|Yes| AutoLogin[Auto-login] LoginScreen --> EnterURL[Enter Server URL] EnterURL --> EnterCreds[Enter Username/Password] EnterCreds --> LoginSuccess{Success?} LoginSuccess -->|No| LoginError[Show Error] LoginError --> EnterCreds LoginSuccess -->|Yes| StoreToken[Store Token in Keyring] AutoLogin --> TokenValid{Token Valid?} TokenValid -->|No| LoginScreen TokenValid -->|Yes| HomePage StoreToken --> HomePage[Home Page
/] ``` **Screens:** 1. **Login Screen** (`/login`) - Server URL input - Username input - Password input - "Remember me" checkbox (default: on) - Login button - No header, no bottom nav 2. **Home Page** (`/`) - Default landing page after successful login - Shows featured content, carousels, continue watching - No MiniPlayer visible (nothing playing yet) - Bottom nav: Home tab active - Header with navigation links ### 2.2 Subsequent Launches ```mermaid flowchart TB Launch[App Launch] --> LoadAuth[Load Stored Token] LoadAuth --> Validate{Token Valid?} Validate -->|Yes| RestoreState[Restore Last Screen] Validate -->|No| LoginScreen[Login Screen
/login] RestoreState --> CheckPlayer{Was Player
Active?} CheckPlayer -->|Yes| ShowMiniPlayer[Show MiniPlayer
at bottom] CheckPlayer -->|No| HideMiniPlayer[No MiniPlayer] ShowMiniPlayer --> LastScreen[Last Active Screen
with MiniPlayer] HideMiniPlayer --> HomePage[Home Page
/] ``` **State Restoration:** - Last viewed screen (route) is restored (defaults to `/` if none) - If audio was playing, MiniPlayer appears at bottom - Playback state is NOT automatically resumed (user must press play) - Queue is restored if it existed --- ## 3. Audio Playback Flows ### 3.1 Starting Audio Playback ```mermaid flowchart TB Start[User Action] --> Action{Action Type?} Action -->|Click Track| TrackList[TrackList Component] Action -->|Click Album| AlbumDetail[Album Detail Page] Action -->|Click Play on Album| AlbumPlay[Play Album Button] TrackList --> PlayTrack[Play Single Track] PlayTrack --> QueueAll[Queue All Filtered Tracks] AlbumPlay --> PlayAlbum[Play All Album Tracks] PlayAlbum --> QueueAlbum[Queue Album Tracks] QueueAll --> InvokePlay[invoke player_play_queue] QueueAlbum --> InvokePlay InvokePlay --> PlayerStarts[Player State: Playing] PlayerStarts --> MiniAppears[MiniPlayer Slides Up
from Bottom] MiniAppears --> StayOnPage[User Stays on
Current Screen] ``` **Entry Points for Audio Playback:** 1. **TrackList** (`/library/music/tracks`, `/library/music/albums/[id]`) - Click track number → Play track + queue all visible tracks - Clicking track #3 in an album → Play track 3, queue tracks 1-10 2. **Album Card** (grid views) - Click album → Navigate to album detail - Play button on card → Play album immediately 3. **Search Results** - Click track → Play track + queue search results - Click album → Navigate to album detail **MiniPlayer Behavior:** - Slides up from bottom with animation (300ms) - Height: 64px on mobile, 80px on desktop - Shows: artwork, title, artist, play/pause, next, favorite - Stays visible on ALL screens (except video player) - Click anywhere on MiniPlayer → Navigate to full player **Track Highlighting:** When audio is playing, the currently playing track is visually highlighted in track lists and album pages: - Subtle blue background tint - Left border accent in Jellyfin blue - Title text colored in Jellyfin blue - Desktop: Animated pulsing dots indicator next to title - Mobile: Play arrow (▶) inline with title - Highlight updates automatically when skipping to next/previous track ### 3.2 MiniPlayer → Full Player Transition ```mermaid flowchart TB Mini[MiniPlayer Visible] --> UserClick{User Action} UserClick -->|Click MiniPlayer| NavFullPlayer[Navigate to
/player/[id]] UserClick -->|Swipe Up| SwipeGesture[Swipe Gesture
Planned] NavFullPlayer --> FullPlayer[Full Audio Player Screen] SwipeGesture --> FullPlayer FullPlayer --> ShowControls[Show Full Controls:
- Large artwork
- Progress bar
- Volume slider
- Queue button
- Shuffle/Repeat
- Favorite button] ShowControls --> MiniHidden[MiniPlayer Hidden] ``` **Full Player Screen** (`/player/[id]`) - **Header:** Song title, artist (clickable links to artist/album pages) - **Artwork:** Large album art (centered, dominant) - **Progress:** Seek bar with current time / total duration - **Controls:** Previous, Play/Pause, Next (large touch targets) - **Secondary Controls:** Shuffle, Repeat mode, Queue, Favorite - **Volume:** Volume slider - **Bottom Nav:** Still visible (can navigate away while playing) - **Back button:** Returns to previous screen, MiniPlayer reappears ### 3.3 Full Player → Back to Browsing ```mermaid flowchart TB FullPlayer[Full Player Screen] --> UserAction{User Action} UserAction -->|Back Button / Close| HistoryBack[window.history.back] UserAction -->|Bottom Nav Click| NavOther[Navigate to
Other Screen] HistoryBack --> PrevScreen[Return to Previous Screen
in Browser History] NavOther --> NewScreen[Navigate to New Screen] PrevScreen --> MiniReappears[MiniPlayer Slides Up
from Bottom] NewScreen --> MiniReappears MiniReappears --> PlaybackContinues[Playback Continues
in Background] ``` **Navigation Behavior:** - **Back Button:** Uses browser history (`window.history.back()`) to return to the previous page - **Expected behavior:** Returns user to the screen they were on before opening full player - **Example:** User browsing album → clicks track → full player opens → clicks back → returns to album **Key UX Principles:** - **Playback Never Stops:** Navigating away from player does NOT stop playback - **MiniPlayer Persistence:** MiniPlayer visible on ALL screens (except video/login) - **Queue Preserved:** Current queue remains intact - **State Restoration:** Returning to full player shows same state (position, volume, etc.) - **Natural Navigation:** Back button behaves as expected (returns to previous page, not just closes modal) --- ## 4. Video Playback Flows ### 4.1 Starting Video Playback ```mermaid flowchart TB Start[User Action] --> Action{Action Type?} Action -->|Click Movie| MovieDetail[Movie Detail Page] Action -->|Click Episode| EpisodeClick[Episode Click] Action -->|Click Play Button| PlayButton[Play Button] MovieDetail --> PlayMovie[Play Movie Button] EpisodeClick --> PlayEpisode[Play Episode] PlayMovie --> CheckResume{Resume
Position?} PlayEpisode --> CheckResume CheckResume -->|Yes, >30s| ShowDialog[Resume Dialog] CheckResume -->|No| DirectPlay[Start from Beginning] ShowDialog --> UserChoice{User Choice} UserChoice -->|Resume| ResumePlay[Start at Saved Position] UserChoice -->|Start Over| DirectPlay ResumePlay --> FullscreenVideo[Fullscreen Video Player
/player/[id]] DirectPlay --> FullscreenVideo FullscreenVideo --> HideUI[Hide All UI:
- No Bottom Nav
- No MiniPlayer
- Fullscreen only] ``` **Resume Dialog:** ``` ┌─────────────────────────────────────────┐ │ Continue Watching? │ │ │ │ [Movie Title] │ │ Resume from 12:34 / 1:45:00 │ │ │ │ [Start from Beginning] [Resume] │ └─────────────────────────────────────────┘ ``` ### 4.2 Video Player Screen (IR-003, IR-004, UR-003) **Initial State (First 3 seconds):** - Controls visible overlay - Top bar: Back button, title - Bottom bar: Play/Pause, seek bar, time, settings (subtitles, audio track) - Center: Large play/pause button **After 3 Seconds (Idle):** - All controls fade out (500ms animation) - Fullscreen video only - System UI hidden (status bar, nav bar) **User Interaction:** - **Tap screen:** Controls reappear for 3 seconds - **Double tap left side:** Rewind 10 seconds (shows animated feedback with "-10" indicator) - **Double tap right side:** Forward 30 seconds (shows animated feedback with "+30" indicator) - **Single tap play/pause is deferred** by the 300 ms double-tap window, so a double tap skips without also toggling pause (UR-061) - **Swipe up/down on left side:** Adjust brightness (0.3-1.7x, shows brightness indicator with progress bar) - **Swipe up/down on right side:** Adjust volume (0-100%, shows volume indicator with progress bar) - **Keyboard arrows:** ← rewind 10s, → forward 30s (desktop/external keyboard) - **Keyboard space/K:** Toggle play/pause - **Keyboard F:** Toggle fullscreen - **Pinch:** Zoom (planned) ### 4.3 Exiting Video Player ```mermaid flowchart TB VideoPlaying[Video Playing] --> UserAction{User Action} UserAction -->|Back Button| StopVideo[Stop Playback] UserAction -->|Home Button| Background[App to Background] UserAction -->|Video Ends| VideoEnd[Playback Ended] StopVideo --> SaveProgress[Save Progress
to Local DB + Server] VideoEnd --> SaveComplete[Mark as Watched
Save Progress] Background --> PauseVideo[Pause Video] SaveProgress --> ExitFullscreen[Exit Fullscreen] SaveComplete --> AutoNext{Next Episode
Available?} AutoNext -->|Yes| ShowCountdown[Show Countdown
Next in 5s...] AutoNext -->|No| ExitFullscreen ShowCountdown --> UserCancel{User Cancels?} UserCancel -->|Yes| ExitFullscreen UserCancel -->|No, timeout| PlayNext[Play Next Episode] ExitFullscreen --> RestoreUI[Restore UI:
- Bottom Nav
- Previous Screen] PlayNext --> VideoPlaying PauseVideo --> ShowNotification[Show Notification:
Tap to Resume] ``` **Auto-Next Overlay:** ``` ┌─────────────────────────────────────────┐ │ │ │ [Episode Thumbnail] │ │ │ │ Next: S01E02 - Episode Title │ │ Starting in 5 seconds... │ │ │ │ [Cancel] [Play Now] │ └─────────────────────────────────────────┘ ``` --- ## 5. Music Library Navigation Flows ### 5.1 Music Category Landing Page ```mermaid flowchart TB LibraryHome[Library Home
/library] --> ClickMusic[Click Music Library] ClickMusic --> MusicLanding[Music Landing Page
/library/music] MusicLanding --> ShowCategories[Show Category Cards:
- Tracks
- Artists
- Albums
- Playlists
- Genres] ShowCategories --> UserClick{User Clicks Category} UserClick -->|Tracks| TracksPage[All Tracks Page
/library/music/tracks] UserClick -->|Artists| ArtistsPage[Artists Grid
/library/music/artists] UserClick -->|Albums| AlbumsPage[Albums Grid
/library/music/albums] UserClick -->|Playlists| PlaylistsPage[Playlists Grid
/library/music/playlists] UserClick -->|Genres| GenresPage[Genres Browser
/library/music/genres] ``` **Category Cards:** ``` ┌─────────────────────────────────────────┐ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │ 🎵 │ │ 👤 │ │ 💿 │ │ │ │Track│ │Artist│ │Album│ │ │ └──────┘ └──────┘ └──────┘ │ │ ┌──────┐ ┌──────┐ │ │ │ 📝 │ │ 🎭 │ │ │ │List │ │Genre│ │ │ └──────┘ └──────┘ │ └─────────────────────────────────────────┘ ``` ### 5.2 Albums View Flow ```mermaid flowchart TB AlbumsGrid[Albums Grid
grid/list per §5A] --> UserAction{User Action} UserAction -->|Click Album| AlbumDetail[Album Detail Page
/library/[id]] UserAction -->|Click Play on Card| PlayAlbum[Play Album Immediately] AlbumDetail --> ShowAlbum[Show Album:
- Album Art
- Title, Artist
- Track List
- Download Button
- Favorite Button] ShowAlbum --> TrackAction{User Action} TrackAction -->|Click Track| PlayTrack[Play Track + Queue Album] TrackAction -->|Click Artist| NavArtist[Navigate to Artist Page] TrackAction -->|Download Album| DownloadFlow[Download Flow] TrackAction -->|Back Button| BackToGrid[Return to Albums Grid] ``` **Album Detail Layout:** ``` ┌─────────────────────────────────────────┐ │ [←] [♡] [⬇] │ │ │ │ ┌────────────────────┐ │ │ │ │ │ │ │ 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 │ │ ... │ └─────────────────────────────────────────┘ ``` ### 5.3 Artist Navigation ```mermaid flowchart TB ArtistsGrid[Artists Grid] --> ClickArtist[Click Artist] ClickArtist --> ArtistPage[Artist Detail Page
/library/artist/[id]] ArtistPage --> ShowContent[Show Artist Content:
- Artist Photo
- Biography
- Albums Grid
- Top Tracks
- Similar Artists] ShowContent --> UserAction{User Action} UserAction -->|Click Album| AlbumDetail[Album Detail Page] UserAction -->|Play Top Tracks| PlayArtist[Play Artist Radio] UserAction -->|Click Similar Artist| OtherArtist[Other Artist Page] ``` --- ## 5A. Library Page Layouts Every browse page is one of two shapes: a **card grid** or a **row list**. This section is the rule for which shape a page takes, what a card looks like, and what the user is allowed to change. ### 5A.1 Card shape follows the media, not the page Card aspect ratio is a property of *what the item is*, 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. | Item type | Aspect | Rationale | |-----------|--------|-----------| | Album, Artist, Track, Playlist | **1:1 square** | Matches album art; the universal music convention (Spotify) | | Movie, Series, Season | **2:3 poster** | Matches printed poster art; the universal video convention (Netflix) | | Episode | **16:9 thumbnail** | A frame from the episode, not cover art — signals "a thing you watch next" | | Library / collection folder | **16:9** | Reads as a container, distinct from the items inside it | Artist cards are square but rendered **circular-masked**, so artists are distinguishable from albums at a glance within the same music grid. ### 5A.2 Grid vs. list ```mermaid flowchart TB Page[Library browse page] --> Kind{Content kind} Kind -->|Visual-first
albums, artists, movies,
shows, playlists| Grid[Card grid
user may switch to list] Kind -->|Ordinal
tracks in an album,
episodes in a season| List[Row list
always; no toggle] Grid --> Toggle[View toggle in page header] Toggle --> Persist[Choice persists globally
across all grid pages] ``` - **Grids are the default** for anything with cover art worth scanning. - **Lists are mandatory, not optional**, 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 **no toggle**. - **The toggle is global, not per-page.** A user who prefers dense lists prefers them everywhere; making them re-set it on each page is friction. The choice persists across launches. **Responsive columns** (grid mode), tuned so cards stay large enough to read cover art on a phone and don't become postage stamps on a desktop: | Breakpoint | Columns | |------------|---------| | base (phone) | 2 | | sm | 3 | | md | 4 | | lg | 5 | | xl | 6 | ### 5A.3 What a card shows ``` ┌─────────────┐ │ │ ← 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 ``` - **Two lines of text maximum.** Titles truncate rather than wrap; a card that grows to fit its title breaks grid alignment and makes scanning harder. - **Progress and watched state live on the artwork**, not in the text — they must be readable while scanning, without reading. - **Hover/focus reveals play**, so a card is both a navigation target and a playback target without a second control competing for space at rest. ### 5A.4 Known deviations 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. - **The view toggle is discoverable only on a browse page.** 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. *(UR-029)* --- ## 5B. Video Detail Page Composition Movie, Series, and Episode detail pages all live at `/library/[id]`. Which surface renders is decided by item type plus the `?episode=` query param, and **section order is part of the spec** — it is what makes "keep watching this show" the path of least resistance. ### 5B.1 Which surface renders ```mermaid flowchart TB Nav[Navigate to /library/[id]] --> Type{Item type} Type -->|Person| Person[PersonDetailView] Type -->|Movie| Movie[Movie detail
§5B.3] Type -->|Series| Ep{?episode= param
present?} Ep -->|Yes| Focus[Episode Focus View
§5B.2] Ep -->|No| Series[Series detail
§5B.4] Focus -->|Back to series| Series Series -->|Click episode| Focus ``` An episode is **never** browsed as a bare `Episode` item page. Clicking an episode anywhere — a series' season list, a Home carousel (§5B.5), etc. — navigates to `/library/?episode=`, 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 `seriesId` (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. ### 5B.2 Episode Focus View — section order **The next episodes appear directly below the current episode, above cast and similar shows.** Nothing may be inserted between the episode hero and the episode strip. ``` ┌─────────────────────────────────────────────────┐ │ [←] │ │ ┌───────────────────────────────────────────┐ │ │ │ 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 │ ┌────┐┌────┐┌────┐┌────┐ │ └─────────────────────────────────────────────────┘ ``` **Rules for the episode strip:** - **Position is fixed.** Hero → episode strip → cast → similar. The strip sits between the current episode and every other section; cast and related content are *below* it, never above. - **Window, not full list.** 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. - **Forward bias.** More episodes are shown *after* the current one than before it: the dominant intent on this screen is "watch the next one." - **The current episode is present and marked.** 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. - **Cross-season continuity.** 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. - **Per-episode state.** Each card shows a thumbnail, `SxEy` + title, a resume progress bar when partially watched, and a watched checkmark when complete. - **Clicking an episode swaps focus in place** (`?episode=` changes); it does not start playback. Playback starts only from the hero's Play button. ### 5B.3 Movie detail — section order ``` Hero (poster, title, metadata, Play / Download / Favorite) → Crew links (Directed by / Written by / Music by) → Genre tags → Cast → More Like This ``` A movie has no continuation set, so cast follows the hero directly. ### 5B.4 Series detail — section order ``` 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 ``` The same principle as §5B.2: **episodes come before cast and similar shows.** The reason a user opens a series page is to pick an episode; discovery content is secondary and sits underneath. **Rules for the seasons block** *(UR-062, UR-064)*: - **The page opens where the viewer is.** 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 `Up next` badge and a highlight ring. Never season 1 by default, unless season 1 *is* where the viewer is. - **Seasons collapse; only the current one is expanded.** 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. - **The hero button opens, it does not play.** It reads `Resume S2E4` / `Play S1E1` — naming its target — and navigates to that episode's Focus View, where Play commits. Play on a *container* is navigation (§5B.5); Play on a *leaf* is the commitment. - **A season is never its own page.** `/library/` redirects to `/library/#season-N`. 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. - **Watch history is erasable** 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. ### 5B.5 Home-card interaction — tap opens, long-press plays Cards on the Home screen carousels (Next Movie, Next Episode, Continue Watching, Recently Added, …) **do not play on tap.** A plain tap opens the item; playback is the deliberate, second gesture. | Card kind | Tap (short) | Long-press (~500 ms hold) | |-----------|-------------|---------------------------| | Movie | Movie detail page (`/library/`) | Confirm → play now (`/player/`) | | Episode | Series Episode Focus View (`/library/?episode=`, per §5B.1) | Confirm → play now (`/player/`) | | Series / Season / Album / Artist / Playlist / Folder | Detail page (`/library/`) | Same as tap (no single "play now" target) | | Channel / live leaf | Player (`/player/`) — no detail page exists | Confirm → play now | Rationale and rules: - **Tap is navigation, not commitment.** 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. - **Long-press is the shortcut for "just play it."** It surfaces a native confirm (`Play "" now?`) before starting playback, so an accidental hold never blows away a resume position silently. - **The long-press must not fight the carousel.** Detection cancels if the pointer moves more than ~10 px (a horizontal scroll of the row), so holding to scroll never triggers play. - **Episodes still obey §5B.1** — a home tap on an episode opens the series Focus View, never a bare Episode page, so the series context loads. This behavior lives in `MediaCard` (`onLongPress` 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. --- ## 5C. Favourites 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. See [specs/favorites-browsing.md](specs/favorites-browsing.md) for the layer assignment and wire shapes. ### 5C.1 The heart appears wherever an item does A favourite is a property of an *item*, so the affordance follows the item rather than living on one privileged screen. Any surface that shows a whole item shows its heart. | Surface | Heart position | Notes | |---------|----------------|-------| | Movie / Series detail hero | In the button row, after Play and Download | §5B.3, §5B.4 | | Episode Focus View hero | Same row as Play / Download | §5B.2 | | Album, Artist, Playlist detail | In the header button row | §5.2 | | Media card (any grid or carousel) | Top-right overlay on the artwork | Hidden on server-only (greyed) cards | | Mini player | Right of the track metadata | Existing behaviour, unchanged | | Full player | Secondary controls row | §3.2 — **not yet built**, see §5C.5 | Rules: - **The heart never competes with the card.** 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. - **State is shown, not guessed.** A filled heart means the *server* 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. - **Feedback is immediate.** The heart fills on tap and a toast confirms; neither waits for the server round-trip. ### 5C.2 Three ways back to what you favourited Favourites are not one destination — they are a lens, and the right surface depends on whether the user is *browsing*, *deciding*, or *hunting*. ```mermaid flowchart TB User[User wants their favourites] --> How{Intent} How -->|Passive: show me something| Home[Home carousels
Favourite Movies / Shows / Music] How -->|Deliberate: my whole collection| Page[Favourites page
/library/favorites] How -->|Narrowing: within this library| Filter[Favourites filter
on a library page] Home -->|See all| Page Page --> Detail[Item detail page] Filter --> Detail ``` **Home carousels.** Rows for favourite movies, shows and music sit below *Recently Added*. A row with nothing in it **does not render** — a fresh install shows no empty favourite rows. Each row ends with *See all*, landing on the matching tab of the Favourites page. **The Favourites page** (`/library/favorites`) is the complete collection, scoped by tabs: ``` ┌─────────────────────────────────────────────────┐ │ [←] Favourites │ │ ┌─────┬────────┬───────┬───────┐ │ │ │ All │ Movies │ Shows │ Music │ ← scope tabs │ │ └─────┴────────┴───────┴───────┘ │ │ │ │ ┌────┐┌────┐┌────┐┌────┐┌────┐ │ │ │ ♥ ││ ♥ ││ ♥ ││ ♥ ││ ♥ │ grid/list │ │ └────┘└────┘└────┘└────┘└────┘ per §5A │ └─────────────────────────────────────────────────┘ ``` - Cards obey §5A in full — shape follows the media, so a mixed *All* tab reads as posters, squares and thumbnails side by side rather than one forced shape. - Reached from a card on the library overview (`/library`) and from *See all* on any home favourites row. - Sorted by name. Jellyfin does not record *when* an item was favourited, so "recently favourited" is not offerable — see §5C.5. - Empty state, per tab: *"Nothing favourited yet — tap the heart on anything you like."* **The in-library filter** 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 **session-scoped and not persisted** — a sticky filter that silently hides most of a library reads as data loss on the next launch. ### 5C.3 Removing a favourite removes it everywhere, at once 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. ### 5C.4 Offline - **Marking works offline.** The heart fills, the toast confirms, and the change is held locally. - **It reaches the server on reconnect**, without the user returning to the screen where they made it. - **Browsing offline shows favourites among media on the device**, 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 *nothing favourited is downloaded*, and the page does not quietly fall back to the server catalog. ### 5C.5 Known deviations - **The full player has no heart.** §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. *(UR-067)* - **No "recently favourited" sort.** 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 *this device's* favourites only, which is worse than a consistent name sort. - **Music is one tab, not three.** 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. --- ## 6. Search Flow Search is **context-scoped**: 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. ### 6.1 Scope is inherited from context ```mermaid flowchart TB Start[User starts a search] --> Where{Where from?} Where -->|Home (/)| All[Scope: All] Where -->|Library root (/library)| All Where -->|Search tab| All Where -->|Inside Music| Music[Scope: Music] Where -->|Inside Movies| Movies[Scope: Movies] Where -->|Inside TV| TV[Scope: TV] All --> Chips[Filter chips shown
All chip selected] Music --> Chips2[Filter chips shown
Music chip preselected] Movies --> Chips2 TV --> Chips2 Chips --> Results[Results, grouped by type] Chips2 --> Results Results --> Change{User taps a chip} Change --> Rescope[Re-run search at new scope
query preserved] Rescope --> Results ``` **Rules:** - **Context sets the *initial* chip, never a locked filter.** 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. - **Home, `/library`, and the search tab all start at "All".** These are the places a user has expressed no narrower intent. - **Changing scope preserves the query** and re-runs the search. Changing the query preserves the scope. - **Scope maps to item types**, resolved at the point of search: | Chip | `includeItemTypes` | |------|--------------------| | All | *(unset — every type)* | | Music | `MusicAlbum`, `MusicArtist`, `Audio`, `Playlist` | | Movies | `Movie` | | TV | `Series`, `Episode` | - **Chips render under the search bar**, 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. ### 6.2 Search page layout ``` ┌─────────────────────────────────────────┐ │ [🔍 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) │ └─────────────────────────────────────────┘ ``` - Results stay **grouped by type** even when a scope is selected — a Music search still separates Songs / Albums / Artists. - Each group shows a bounded preview with a **See all (n)** affordance rather than an unbounded list, so no single type can bury the others. - Live search is **debounced** as the user types; a query that becomes empty clears results rather than searching for the empty string. ### 6.3 Result group order is user-configurable Which *kind* 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. ```mermaid flowchart TB Settings[Settings → Search] --> List[Draggable list of result groups] List --> Drag[User drags a group up or down] Drag --> Persist[Order persisted] Persist --> Render[Rendering a result set] Scope[Active scope chip §6.1] --> Render Render --> Filter[1 - Drop groups outside the active scope] Filter --> Sort[2 - Sort remaining groups by user order] Sort --> Prune[3 - Omit groups with no results] Prune --> Show[Render] ``` **Scope and order compose — they are two independent axes.** The scope chip decides *which* groups are eligible; the settings list decides *what sequence* the eligible ones appear in. Order is preserved as a relative ranking, never renumbered per scope: - Scope **Music** with order `Movies → Songs → Albums → Artists → TV` renders `Songs → Albums → Artists`. Movies and TV are filtered out; the surviving groups keep their relative order. - Scope **All** with the same setting renders all five in exactly that order. - **Changing scope never rewrites the saved order.** A user who narrows to Music and back to All sees their original arrangement intact. **Rules:** - **Drag and drop to reorder**, in a settings list showing every result group (Songs, Albums, Artists, Movies, TV Shows). - **The order applies to grouped results everywhere** — the search page and the in-library header search alike. - **Order is presentation-only.** It never changes which results are returned or how they are ranked *within* a group, only the sequence groups appear in. - **Empty groups are skipped, not gapped.** A group with no results is omitted entirely; it does not reserve space or leave a stray heading. - **A sensible default ships** (Songs → Albums → Artists → Movies → TV Shows) so the setting is an adjustment, never a prerequisite. - **Keyboard/accessible reordering must exist** alongside dragging — a drag-only control is unusable with a screen reader or without a pointer. ### 6.4 Known deviations Recorded so the gap between this spec and the build is explicit. - **Scope is not implemented.** 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 `includeItemTypes` on both the online and offline paths, and the per-page list search already uses it — only the global path ignores it. *(UR-049)* - **Filter chips do not exist** on either search surface. *(UR-049)* - **Group order is hardcoded** to Music → Movies → TV in the results markup, with no setting. *(UR-050)* --- ## 7. Download Flows ### 7.1 Initiating Downloads ```mermaid flowchart TB User[User on Album/Track Page] --> ClickDownload[Click Download Button] ClickDownload --> CheckType{Download Type?} CheckType -->|Single Track| DownloadTrack[Download Single File] CheckType -->|Album| DownloadAlbum[Download All Tracks] CheckType -->|Artist| ShowOptions[Show Options Dialog] ShowOptions --> UserChoice{User Choice} UserChoice -->|Discography| DownloadAll[Download All Albums] UserChoice -->|Select Albums| AlbumPicker[Album Selection UI] DownloadTrack --> QueueDownload[Queue in Download Manager] DownloadAlbum --> QueueMultiple[Queue Multiple Files] QueueDownload --> ShowProgress[Show Progress Ring
on Download Button] QueueMultiple --> ShowProgress ShowProgress --> DownloadActive[Download Active:
Button shows % complete] ``` **Download Button States:** ``` 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 ``` ### 7.2 Downloads = a browsable offline library, not a flat list **The central idea:** "my downloads" is not a list of file-transfer rows — it is *the library, filtered to what's on the device*. 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 **reuses the library browse screens**, scoped to downloaded content, and keeps the transfer-progress list as a secondary "Transfers" view for the *act* of downloading. This splits one overloaded page into two clear jobs: | Surface | Answers | Reuses | |---------|---------|--------| | **Downloaded** (browse) | "What do I have offline, and let me play it" | Library grids, detail pages, cards (§5A) | | **Transfers** (activity) | "What is downloading right now, and control it" | The existing progress-row list | ```mermaid flowchart TB Nav[Open Downloads] --> Downloads[/downloads] Downloads --> View{View} View -->|Downloaded (default)| Browse[Offline library browse] View -->|Transfers| Activity[Transfer activity list] Browse --> Libs[Libraries — only those with
downloaded content] Libs --> Grid[Library grid, offline-scoped
same cards/layout as online §5A] Grid --> Detail[Detail page
same as online] Detail --> Play[Play from local file] Detail --> Remove[Remove download
frees space, keeps browsable? — see rules] Activity --> Rows[Per-transfer rows:
downloading / queued / paused / failed /
waiting-for-WiFi] Rows --> Ctl[Pause / Resume / Cancel / Retry] ``` **Why reuse the library screens (not a bespoke list):** - **One mental model.** Browsing offline should feel identical to browsing online — same grids, same card shapes, same detail pages, same play action. The only difference is *what's present*, not *how it looks*. - **It already works in the backend.** The offline repository's `get_items` already returns downloaded items **plus** their containers (an album with any downloaded track, a series/season with any downloaded episode). That is a browsable tree today — see §7.4. - **It scales.** A flat completed-list becomes unusable at a few dozen items; a browsable library does not. ### 7.3 The Downloaded browse surface ``` ┌─────────────────────────────────────────────┐ │ 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│ │ │ └────┘└────┘ │ └─────────────────────────────────────────────┘ ``` **Rules:** - **Libraries with nothing downloaded are omitted**, not shown empty. If only music is downloaded, only Music appears. - **Cards, grids, and detail pages are the library's own** (§5A) — offline browse is the same components with an offline-scoped data source, never a parallel re-implementation. - **A downloaded badge / "on device" affordance** distinguishes fully-downloaded from partially-downloaded containers (e.g. a season with 6 of 10 episodes). - **Disk usage is shown where the user already looks**, in familiar units — see §7.3.1. - **Play always plays the local file** here; nothing on this surface streams. - **Remove is available at every level** — item, album/season, series — and states clearly what it frees. Removing the last downloaded child of a container removes the container from the browse. - **This surface works identically online and offline.** 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. #### 7.3.1 Disk usage — familiar, in place, not a separate audit 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. - **Size rides along with the item, on the card and the detail page** — a small secondary label (`1.2 GB`, `340 MB`, `48 MB`), never a separate "storage report" screen the user has to go find. - **Containers show their total.** 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. - **Human units, rounded, consistent.** Binary or decimal is a choice — pick one and use it everywhere. Show 2–3 significant figures (`1.2 GB`, not `1,283,048,192 bytes` and not `1.28394 GB`). - **A single device total sits at the top** of the Downloaded surface (`3.4 GB on device · 12 items`) so the headline number is answered before the user scans. It reconciles with the sum of what's listed. - **Remove restates the reclaim** 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. - **Sort/filter by size is a reasonable enhancement** ("biggest first" to find what to clear) but is not required for v1. 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. ### 7.4 Transfers (activity) view The existing progress-row list, unchanged in spirit, demoted to a secondary tab. It is about *transfers in flight*, so it shows only rows that are doing or waiting to do something: - **States:** downloading (with progress), queued, paused, failed, waiting-for-WiFi (§7.5). - **Controls:** Pause / Resume / Cancel / Retry per row; the 3-concurrent cap and auto-pump are backend concerns and are not surfaced as manual controls. - **Completed transfers fall off this view** 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). - **Empty state** points at the library: "Nothing downloading. Browse your library and tap download to save media for offline." ### 7.5 Navigation & entry points - Reached via the account menu (§1.2) and, on desktop, the header Downloads link/icon → `/downloads`. - `/downloads` opens on **Downloaded** by default; **Transfers** is one tap away and should draw attention (badge/count) only while transfers are active. - 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. ### 7.7 Known deviations Recorded so the gap between this spec and the build is explicit. - **Downloads is a flat two-tab list today** (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. *(UR-055)* - **No offline-scoped browse entry point exists in the client.** All browsing goes through the hybrid repository, which merges cache **and** 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. *(UR-055, DR-082)* - **The "on device" storage summary and per-container remove** are absent from the completed list. *(UR-055, UR-056)* - **Per-item disk usage is not displayed anywhere.** Cards and detail pages show no size; there is no device total, no container subtotal, and Remove does not state what it frees. *(UR-056)* --- ## 8. Settings & Account Flows ### 8.1 Settings Navigation ```mermaid flowchart TB User[User] --> NavChoice{Navigation Path} NavChoice -->|Desktop| HeaderSettings[Header: Click Settings Link] NavChoice -->|Mobile| OverflowMenu[Click Overflow Menu
→ Settings] NavChoice -->|Direct| TypeURL[Navigate to /settings] HeaderSettings --> SettingsPage[Settings Page
/settings] OverflowMenu --> SettingsPage TypeURL --> SettingsPage SettingsPage --> ShowSections[Show Sections:
- Account
- Playback
- Downloads
- Appearance
- About] ShowSections --> UserClick{User Clicks Section} UserClick -->|Account| AccountSettings[Account Settings:
- Server URL
- Username
- Logout button] UserClick -->|Playback| PlaybackSettings[Playback Settings:
- Gapless playback
- Volume normalization
- Crossfade duration] UserClick -->|Downloads| DownloadSettings[Download Settings:
- Max concurrent
- WiFi only
- Storage location
- Auto-cache next tracks] UserClick -->|Appearance| AppearanceSettings[Appearance Settings:
- Dark mode
- Accent color] ``` **Navigation to Settings:** - **Desktop:** Click "Settings" link in header navigation - **Mobile:** Click three-dot overflow menu → Select "Settings" - **Direct:** Navigate to `/settings` route **Settings apply instantly.** 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 **no "Save" button** 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 (`change`, not each `input` tick) to avoid flooding the backend. ### 8.2 Logout Flow ```mermaid flowchart TB AnyScreen[Any Screen] --> ClickLogout[Click Logout Button
in Header] ClickLogout --> ConfirmDialog[Show Confirmation:
"Log out of [Server]?"] ConfirmDialog --> UserConfirm{User Confirms?} UserConfirm -->|No| CancelLogout[Cancel - Stay on Current Screen] UserConfirm -->|Yes| StopPlayer[Stop Playback] StopPlayer --> ClearToken[Delete Token from Keyring] ClearToken --> ClearState[Clear App State:
- Player state
- Queue
- Current screen] ClearState --> NavLogin[Navigate to Login Screen
/login] NavLogin --> ShowLogin[Show Login Screen:
- No Header
- No Bottom Nav
- No MiniPlayer] ``` **Logout Button Location:** - Always visible in header user menu (logout icon) - Accessible from any authenticated screen --- ## 9. Background & Lock Screen Behavior ### 9.1 Audio Playback in Background (Android) ```mermaid flowchart TB Playing[Audio Playing] --> Background{User Action} Background -->|Home Button| AppBackground[App to Background] Background -->|Screen Lock| ScreenLock[Screen Locked] AppBackground --> ContinuePlay[Playback Continues] ScreenLock --> ContinuePlay ContinuePlay --> ShowNotification[Show Media Notification:
- Artwork
- Title/Artist
- Play/Pause
- Next/Previous] ShowNotification --> LockScreen[Lock Screen Controls:
Media Session Integration] LockScreen --> UserInteract{User Interaction} UserInteract -->|Tap Notification| OpenApp[Open App to Last Screen
with MiniPlayer] UserInteract -->|Lock Screen Controls| SendCommand[Send Command to Player] UserInteract -->|BLE Headset Button| HeadsetControl[AVRCP Command] ``` **Notification Layout (Android):** ``` ┌─────────────────────────────────────────┐ │ [Artwork] Song Title │ │ Artist Name │ │ Album Name │ │ │ │ [⏮] [⏸] [⏭] [✕] │ └─────────────────────────────────────────┘ ``` ### 9.2 Video Playback in Background (Android — PiP & Background Audio) Leaving the app while a **local video** is playing does not simply pause it. What happens depends on which background behaviour is active. The two are **mutually exclusive**, and both apply **only to locally-rendering video** — audio-only playback, library/menu browsing, and remote/cast sessions never trigger PiP (see decision gate below). ```mermaid flowchart TB Leave[User leaves app
Home / gesture / screen lock] --> Gate{Local video surface
actively rendering?
canEnterPip} Gate -->|No — audio, browsing,
or remote/cast| Normal[App backgrounds normally
audio, if any, continues via
media notification (§9.1)] Gate -->|Yes| Mode{Background mode armed?} Mode -->|Background-audio toggle ON
UR-040| Handoff[Hand off to native audio service
WebView <video> torn down,
video decode stops, audio continues] Mode -->|Default
UR-041| PiP[Auto-enter Picture-in-Picture
on onUserLeaveHint] PiP --> PiPWindow[Floating PiP window:
- Video keeps rendering into surface
- WebView hidden
- Play/Pause RemoteAction
(reflects live player state)] PiPWindow --> PiPReturn{User action} PiPReturn -->|Tap window| Restore[Return to full player
WebView restored, surface re-fit] PiPReturn -->|Close window| Stop[Playback stops] Handoff --> Foreground[On return to foreground:
resume WebView video at position] ``` **Key rules:** - **Video-only gate.** Auto-PiP is guarded by the native `canEnterPip` 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. *(UR-041, IR-026)* - **Only one background behaviour at a time.** The background-audio toggle (UR-040) disarms auto-PiP while it is on, so a video is either handed to the audio service *or* floated in PiP, never both. - **PiP controls track the player.** 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. *(DR-053)* - **Non-disruptive transition.** 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. *(DR-053)* **PiP window (Android):** ``` ┌───────────────────┐ │ │ │ ▶ video frame │ │ │ │ [⏸] │ ← play/pause RemoteAction └───────────────────┘ sized to the video's aspect ratio ``` --- ## 10. Error States & Edge Cases ### 10.1 Network Loss During Streaming ```mermaid flowchart TB Streaming[Streaming Audio/Video] --> LoseNetwork[Network Connection Lost] LoseNetwork --> CheckLocal{Local Copy
Available?} CheckLocal -->|Yes| SwitchLocal[Switch to Local Playback
Seamlessly] CheckLocal -->|No| ShowBuffer[Show Buffering Spinner] ShowBuffer --> WaitReconnect[Wait for Reconnection
30 second timeout] WaitReconnect --> Reconnect{Reconnected?} Reconnect -->|Yes| Resume[Resume Streaming] Reconnect -->|No| ShowError[Show Error Toast:
"Unable to stream.
Check connection."] ShowError --> OfferRetry[Offer Retry Button] ShowError --> OfferDownload[Offer "Download for Offline"] ``` ### 10.2 Server Unreachable ```mermaid flowchart TB Action[User Action Requires Server] --> TryConnect[Attempt Connection] TryConnect --> Timeout{Connection
Timeout?} Timeout -->|Yes| ShowError[Show Error:
"Server unreachable"] Timeout -->|No| Success[Action Succeeds] ShowError --> OfferOptions[Offer Options:
- Retry
- Switch to Offline Mode
- Change Server] ``` ### 10.3 Download Failed ```mermaid flowchart TB Downloading[Download in Progress] --> Failure{Failure Type?} Failure -->|Network Error| Retry[Auto-retry
with Backoff] Failure -->|Disk Full| ShowDiskError[Show Error:
"Not enough storage"] Failure -->|Server Error| ShowServerError[Show Error:
"Server error"] Retry --> RetryCount{Retry Count
< 3?} RetryCount -->|Yes| Downloading RetryCount -->|No| Failed[Mark as Failed] ShowDiskError --> Failed ShowServerError --> Failed Failed --> UserAction[Show in Downloads:
with Retry Button] ``` --- ## 11. Platform-Specific UX Patterns ### 11.1 Android-Specific **Hardware Back Button:** - **In Full Player:** Return to previous screen, show MiniPlayer - **In Video Player:** Stop playback, exit fullscreen - **In Album Detail:** Return to library grid - **At Library Home:** Exit app (show confirmation) **System Volume Buttons:** - **While playing audio:** Adjust playback volume - **While controlling remote session:** Adjust remote session volume (shows session name in volume panel) - **In menus:** Adjust system volume (default behavior) **Share Integration:** - Long-press album/song → Share menu - Options: Share with other apps, Copy link ### 11.2 Linux Desktop-Specific **Keyboard Shortcuts:** - `Space`: Play/Pause - `→`: Next track - `←`: Previous track - `/`: Focus search - `Ctrl+Q`: Quit **Window Behavior:** - Minimize to tray (playback continues) - Close window (show confirmation if playing) - MPRIS integration for desktop media controls **Mouse Interactions:** - Hover over MiniPlayer: Show additional controls (volume, queue peek) - Right-click: Context menu (Add to playlist, Go to artist, Download) --- ## 12. UX Principles Summary ### 12.1 Core Principles 1. **Playback Persistence:** - Audio playback never stops unless user explicitly stops it - MiniPlayer visible on all screens (except video/login) - Queue and position preserved across navigation 2. **Non-Blocking UI:** - Downloads happen in background - Sync operations never block user interaction - Optimistic updates (favorite, progress) with background sync 3. **Offline-First:** - Downloaded content works offline - Seamless switch between online/offline - Progress and preferences saved locally 4. **Progressive Disclosure:** - Simple defaults, advanced options hidden - Context menus for secondary actions - Settings organized by category 5. **Responsive Design:** - Mobile-first UI - Desktop enhancements (hover states, keyboard shortcuts) - Tablet: Grid layouts with more columns ### 12.2 Animation & Transitions | Transition | Duration | Easing | |------------|----------|--------| | MiniPlayer slide up/down | 300ms | ease-out | | Screen navigation | 200ms | ease-in-out | | Video controls fade | 500ms | ease-out | | Download button state change | 150ms | ease-in-out | | Modal appear | 200ms | ease-out | | Toast notification | 250ms | ease-in-out | ### 12.3 Touch Targets (Mobile) | Element | Minimum Size | |---------|--------------| | Bottom nav buttons | 48x48 dp | | List item (track, album) | Full width x 56 dp | | Player controls | 56x56 dp | | MiniPlayer | Full width x 64 dp | | Download button | 40x40 dp | | Favorite button | 40x40 dp | --- ## 13. Future UX Enhancements ### 13.1 Planned Features 1. **Gesture Navigation:** - Swipe up on MiniPlayer → Full player - Swipe down on full player → Back to previous screen - Swipe between tracks in full player 2. **Queue Management UI (DR-020):** - Drag to reorder - Swipe to remove - Add to queue vs. Play next 3. **Sleep Timer (UR-026):** - Accessible from full player menu - Presets: 15min, 30min, 1hr, End of track, End of album - Countdown visible in MiniPlayer 4. **Home Screen (UR-034):** - Hero banner carousel - Continue watching/listening - Recently added - Personalized recommendations 5. **Cast/Remote Control Enhancements:** - Picture-in-picture for remote sessions - Multi-room audio (play on multiple devices) - Handoff (transfer playback to phone from TV) ### 13.2 Accessibility Enhancements - Screen reader optimization - High contrast mode - Larger text option - Voice control integration - Haptic feedback for controls --- This UX flow documentation should be updated as new features are implemented and user feedback is incorporated.