Files
jellytau/docs/ux-flows.md
dtourolle 62873cab3d feat(search): answer search from a local index; tier downloads by lifetime
Search's instant leg read only downloaded items, so with no downloads it
returned nothing and every keystroke fell through to a full Recursive=true
server query. It now reads the whole synced catalog through the same
availability CTE get_items uses, gated on the same include_catalog_browse
flag so search and browse cannot diverge. (UR-065, DR-108)

Also fixes three defects found while confirming that:

- items_fts grew by a full duplicate index every catalog pass. INSERT OR
  REPLACE fires no AFTER DELETE trigger without recursive_triggers, so the
  old index row was orphaned, and a TEXT PRIMARY KEY meant the replacement
  took a fresh rowid and inserted a second entry. Now a real upsert, with
  migration 021 rebuilding existing indexes. (DR-110)
- DELETE FROM items existed nowhere, so server-side deletions never
  propagated. Adds a post-crawl mark-and-sweep, scoped to crawled types,
  skipping downloaded items, and refusing to run after a partial crawl
  because items.parent_id cascades. (DR-110)
- The index omitted MusicArtist, Playlist and People, which search groups
  results by. Adds them plus people_fts (migration 022). (DR-111)

Re-indexing moves from a frontend startup call to a Rust background task
with a 6h TTL, so a long session no longer searches a stale catalog and a
restart no longer forces a crawl regardless of freshness. (DR-109, IR-030)

Downloads gain a lifetime tier. Eviction selected every completed row by
age with no download_source filter, so hitting the storage limit deleted
the oldest download -- typically one saved deliberately for offline -- to
make room for a precached track. It now reclaims only 'auto' rows, and
expired ones are reclaimed first, before live cache is evicted.
(DR-126, DR-127)

Downloaded video and audio-only handoffs now play from disk instead of
streaming; the video path had never consulted downloads at all. No
transcode is involved: MPV runs video=no and ExoPlayer has no surface for
an Audio item. (DR-123 in part, DR-128)

FTS queries are built as quoted phrases so apostrophes, hyphens and
slashes are data rather than operator syntax, and the item-type filter is
bound rather than interpolated.

Specs: docs/specs/catalog-index-search.md,
docs/specs/read-through-media-cache.md

Includes concurrently-developed favourites browsing and background-audio
stream-end handling; the two workstreams share offline.rs, lib.rs and
online.rs, so no subset of files builds independently.
2026-08-04 17:35:17 +02:00

66 KiB
Raw Permalink Blame History

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 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.

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 23 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.