# 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 <name>     │   ← identity, not a menu item
    │  <server host>           │
    ├──────────────────────────┤
    │  ⬇  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

    flowchart TB
        Launch[App Launch] --> CheckAuth{Stored<br/>Credentials?}
        CheckAuth -->|No| LoginScreen[Login Screen<br/>/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<br/>/]

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

    flowchart TB
        Launch[App Launch] --> LoadAuth[Load Stored Token]
        LoadAuth --> Validate{Token Valid?}
        Validate -->|Yes| RestoreState[Restore Last Screen]
        Validate -->|No| LoginScreen[Login Screen<br/>/login]

        RestoreState --> CheckPlayer{Was Player<br/>Active?}
        CheckPlayer -->|Yes| ShowMiniPlayer[Show MiniPlayer<br/>at bottom]
        CheckPlayer -->|No| HideMiniPlayer[No MiniPlayer]

        ShowMiniPlayer --> LastScreen[Last Active Screen<br/>with MiniPlayer]
        HideMiniPlayer --> HomePage[Home Page<br/>/]

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

    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<br/>from Bottom]

        MiniAppears --> StayOnPage[User Stays on<br/>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

    flowchart TB
        Mini[MiniPlayer Visible] --> UserClick{User Action}

        UserClick -->|Click MiniPlayer| NavFullPlayer[Navigate to<br/>/player/[id]]
        UserClick -->|Swipe Up| SwipeGesture[Swipe Gesture<br/>Planned]

        NavFullPlayer --> FullPlayer[Full Audio Player Screen]
        SwipeGesture --> FullPlayer

        FullPlayer --> ShowControls[Show Full Controls:<br/>- Large artwork<br/>- Progress bar<br/>- Volume slider<br/>- Queue button<br/>- Shuffle/Repeat<br/>- 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

    flowchart TB
        FullPlayer[Full Player Screen] --> UserAction{User Action}

        UserAction -->|Back Button / Close| HistoryBack[window.history.back]
        UserAction -->|Bottom Nav Click| NavOther[Navigate to<br/>Other Screen]

        HistoryBack --> PrevScreen[Return to Previous Screen<br/>in Browser History]
        NavOther --> NewScreen[Navigate to New Screen]

        PrevScreen --> MiniReappears[MiniPlayer Slides Up<br/>from Bottom]
        NewScreen --> MiniReappears

        MiniReappears --> PlaybackContinues[Playback Continues<br/>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

    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<br/>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<br/>/player/[id]]
        DirectPlay --> FullscreenVideo

        FullscreenVideo --> HideUI[Hide All UI:<br/>- No Bottom Nav<br/>- No MiniPlayer<br/>- 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

    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<br/>to Local DB + Server]
        VideoEnd --> SaveComplete[Mark as Watched<br/>Save Progress]
        Background --> PauseVideo[Pause Video]

        SaveProgress --> ExitFullscreen[Exit Fullscreen]
        SaveComplete --> AutoNext{Next Episode<br/>Available?}

        AutoNext -->|Yes| ShowCountdown[Show Countdown<br/>Next in 5s...]
        AutoNext -->|No| ExitFullscreen

        ShowCountdown --> UserCancel{User Cancels?}
        UserCancel -->|Yes| ExitFullscreen
        UserCancel -->|No, timeout| PlayNext[Play Next Episode]

        ExitFullscreen --> RestoreUI[Restore UI:<br/>- Bottom Nav<br/>- Previous Screen]

        PlayNext --> VideoPlaying

        PauseVideo --> ShowNotification[Show Notification:<br/>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

    flowchart TB
        LibraryHome[Library Home<br/>/library] --> ClickMusic[Click Music Library]

        ClickMusic --> MusicLanding[Music Landing Page<br/>/library/music]

        MusicLanding --> ShowCategories[Show Category Cards:<br/>- Tracks<br/>- Artists<br/>- Albums<br/>- Playlists<br/>- Genres]

        ShowCategories --> UserClick{User Clicks Category}

        UserClick -->|Tracks| TracksPage[All Tracks Page<br/>/library/music/tracks]
        UserClick -->|Artists| ArtistsPage[Artists Grid<br/>/library/music/artists]
        UserClick -->|Albums| AlbumsPage[Albums Grid<br/>/library/music/albums]
        UserClick -->|Playlists| PlaylistsPage[Playlists Grid<br/>/library/music/playlists]
        UserClick -->|Genres| GenresPage[Genres Browser<br/>/library/music/genres]

Category Cards:

    ┌─────────────────────────────────────────┐
    │  ┌──────┐  ┌──────┐  ┌──────┐          │
    │  │ 🎵  │  │ 👤  │  │ 💿  │          │
    │  │Track│  │Artist│  │Album│          │
    │  └──────┘  └──────┘  └──────┘          │
    │  ┌──────┐  ┌──────┐                    │
    │  │ 📝  │  │ 🎭  │                    │
    │  │List │  │Genre│                    │
    │  └──────┘  └──────┘                    │
    └─────────────────────────────────────────┘

### 5.2 Albums View Flow

    flowchart TB
        AlbumsGrid[Albums Grid<br/>grid/list per §5A] --> UserAction{User Action}

        UserAction -->|Click Album| AlbumDetail[Album Detail Page<br/>/library/[id]]
        UserAction -->|Click Play on Card| PlayAlbum[Play Album Immediately]

        AlbumDetail --> ShowAlbum[Show Album:<br/>- Album Art<br/>- Title, Artist<br/>- Track List<br/>- Download Button<br/>- 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

    flowchart TB
        ArtistsGrid[Artists Grid] --> ClickArtist[Click Artist]

        ClickArtist --> ArtistPage[Artist Detail Page<br/>/library/artist/[id]]

        ArtistPage --> ShowContent[Show Artist Content:<br/>- Artist Photo<br/>- Biography<br/>- Albums Grid<br/>- Top Tracks<br/>- 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

    flowchart TB
        Page[Library browse page] --> Kind{Content kind}

        Kind -->|Visual-first<br/>albums, artists, movies,<br/>shows, playlists| Grid[Card grid<br/>user may switch to list]
        Kind -->|Ordinal<br/>tracks in an album,<br/>episodes in a season| List[Row list<br/>always; no toggle]

        Grid --> Toggle[View toggle in page header]
        Toggle --> Persist[Choice persists globally<br/>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

    flowchart TB
        Nav[Navigate to /library/&#91;id&#93;] --> Type{Item type}

        Type -->|Person| Person[PersonDetailView]
        Type -->|Movie| Movie[Movie detail<br/>§5B.3]
        Type -->|Series| Ep{?episode= param<br/>present?}

        Ep -->|Yes| Focus[Episode Focus View<br/>§5B.2]
        Ep -->|No| Series[Series detail<br/>§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/<seriesId>?episode=<episodeId>`, 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/<seasonId>` redirects to `/library/<seriesId>#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/<id>`) | Confirm → play now (`/player/<id>`)
Episode | Series Episode Focus View (`/library/<seriesId>?episode=<id>`, per §5B.1) | Confirm → play now (`/player/<id>`)
Series / Season / Album / Artist / Playlist / Folder | Detail page (`/library/<id>`) | Same as tap (no single "play now" target)
Channel / live leaf | Player (`/player/<id>`) — 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 "<name>" 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 architecture/01-rust-backend.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.

    flowchart TB
        User[User wants their favourites] --> How{Intent}

        How -->|Passive: show me something| Home[Home carousels<br/>Favourite Movies / Shows / Music]
        How -->|Deliberate: my whole collection| Page[Favourites page<br/>/library/favorites]
        How -->|Narrowing: within this library| Filter[Favourites filter<br/>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

    flowchart TB
        Start[User starts a search] --> Where{Where from?}

        Where -->|Home &#40;/&#41;| All[Scope: All]
        Where -->|Library root &#40;/library&#41;| 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<br/>All chip selected]
        Music --> Chips2[Filter chips shown<br/>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<br/>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.

    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

    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<br/>on Download Button]
        QueueMultiple --> ShowProgress

        ShowProgress --> DownloadActive[Download Active:<br/>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

    flowchart TB
        Nav[Open Downloads] --> Downloads[/downloads]

        Downloads --> View{View}
        View -->|Downloaded &#40;default&#41;| Browse[Offline library browse]
        View -->|Transfers| Activity[Transfer activity list]

        Browse --> Libs[Libraries — only those with<br/>downloaded content]
        Libs --> Grid[Library grid, offline-scoped<br/>same cards/layout as online §5A]
        Grid --> Detail[Detail page<br/>same as online]
        Detail --> Play[Play from local file]
        Detail --> Remove[Remove download<br/>frees space, keeps browsable? — see rules]

        Activity --> Rows[Per-transfer rows:<br/>downloading / queued / paused / failed /<br/>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

    flowchart TB
        User[User] --> NavChoice{Navigation Path}

        NavChoice -->|Desktop| HeaderSettings[Header: Click Settings Link]
        NavChoice -->|Mobile| OverflowMenu[Click Overflow Menu<br/>→ Settings]
        NavChoice -->|Direct| TypeURL[Navigate to /settings]

        HeaderSettings --> SettingsPage[Settings Page<br/>/settings]
        OverflowMenu --> SettingsPage
        TypeURL --> SettingsPage

        SettingsPage --> ShowSections[Show Sections:<br/>- Account<br/>- Playback<br/>- Downloads<br/>- Appearance<br/>- About]

        ShowSections --> UserClick{User Clicks Section}

        UserClick -->|Account| AccountSettings[Account Settings:<br/>- Server URL<br/>- Username<br/>- Logout button]
        UserClick -->|Playback| PlaybackSettings[Playback Settings:<br/>- Gapless playback<br/>- Volume normalization<br/>- Crossfade duration]
        UserClick -->|Downloads| DownloadSettings[Download Settings:<br/>- Max concurrent<br/>- WiFi only<br/>- Storage location<br/>- Auto-cache next tracks]
        UserClick -->|Appearance| AppearanceSettings[Appearance Settings:<br/>- Dark mode<br/>- 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

    flowchart TB
        AnyScreen[Any Screen] --> ClickLogout[Click Logout Button<br/>in Header]

        ClickLogout --> ConfirmDialog[Show Confirmation:<br/>"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:<br/>- Player state<br/>- Queue<br/>- Current screen]

        ClearState --> NavLogin[Navigate to Login Screen<br/>/login]

        NavLogin --> ShowLogin[Show Login Screen:<br/>- No Header<br/>- No Bottom Nav<br/>- 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)

    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:<br/>- Artwork<br/>- Title/Artist<br/>- Play/Pause<br/>- Next/Previous]

        ShowNotification --> LockScreen[Lock Screen Controls:<br/>Media Session Integration]

        LockScreen --> UserInteract{User Interaction}

        UserInteract -->|Tap Notification| OpenApp[Open App to Last Screen<br/>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).

    flowchart TB
        Leave[User leaves app<br/>Home / gesture / screen lock] --> Gate{Local video surface<br/>actively rendering?<br/>canEnterPip}

        Gate -->|No — audio, browsing,<br/>or remote/cast| Normal[App backgrounds normally<br/>audio, if any, continues via<br/>media notification &#40;§9.1&#41;]

        Gate -->|Yes| Mode{Background mode armed?}

        Mode -->|Background-audio toggle ON<br/>UR-040| Handoff[Hand off to native audio service<br/>WebView &lt;video&gt; torn down,<br/>video decode stops, audio continues]
        Mode -->|Default<br/>UR-041| PiP[Auto-enter Picture-in-Picture<br/>on onUserLeaveHint]

        PiP --> PiPWindow[Floating PiP window:<br/>- Video keeps rendering into surface<br/>- WebView hidden<br/>- Play/Pause RemoteAction<br/>  &#40;reflects live player state&#41;]

        PiPWindow --> PiPReturn{User action}
        PiPReturn -->|Tap window| Restore[Return to full player<br/>WebView restored, surface re-fit]
        PiPReturn -->|Close window| Stop[Playback stops]

        Handoff --> Foreground[On return to foreground:<br/>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

    flowchart TB
        Streaming[Streaming Audio/Video] --> LoseNetwork[Network Connection Lost]

        LoseNetwork --> CheckLocal{Local Copy<br/>Available?}

        CheckLocal -->|Yes| SwitchLocal[Switch to Local Playback<br/>Seamlessly]
        CheckLocal -->|No| ShowBuffer[Show Buffering Spinner]

        ShowBuffer --> WaitReconnect[Wait for Reconnection<br/>30 second timeout]

        WaitReconnect --> Reconnect{Reconnected?}

        Reconnect -->|Yes| Resume[Resume Streaming]
        Reconnect -->|No| ShowError[Show Error Toast:<br/>"Unable to stream.<br/>Check connection."]

        ShowError --> OfferRetry[Offer Retry Button]
        ShowError --> OfferDownload[Offer "Download for Offline"]

### 10.2 Server Unreachable

    flowchart TB
        Action[User Action Requires Server] --> TryConnect[Attempt Connection]

        TryConnect --> Timeout{Connection<br/>Timeout?}

        Timeout -->|Yes| ShowError[Show Error:<br/>"Server unreachable"]
        Timeout -->|No| Success[Action Succeeds]

        ShowError --> OfferOptions[Offer Options:<br/>- Retry<br/>- Switch to Offline Mode<br/>- Change Server]

### 10.3 Download Failed

    flowchart TB
        Downloading[Download in Progress] --> Failure{Failure Type?}

        Failure -->|Network Error| Retry[Auto-retry<br/>with Backoff]
        Failure -->|Disk Full| ShowDiskError[Show Error:<br/>"Not enough storage"]
        Failure -->|Server Error| ShowServerError[Show Error:<br/>"Server error"]

        Retry --> RetryCount{Retry Count<br/>< 3?}
        RetryCount -->|Yes| Downloading
        RetryCount -->|No| Failed[Mark as Failed]

        ShowDiskError --> Failed
        ShowServerError --> Failed

        Failed --> UserAction[Show in Downloads:<br/>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.