🏗️ Build and Test JellyTau / Run Tests (push) Successful in 16m59s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m36s
Traceability Validation / Check Requirement Traces (push) Successful in 18s
Build & Release / Run Tests (push) Successful in 5m9s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 8m54s
Build & Release / Build Linux (push) Successful in 18m49s
Build & Release / Build Windows (push) Successful in 14m4s
Build & Release / Build Android (push) Successful in 30m17s
Build & Release / Create Release (push) Successful in 18s
Opening a series dumped the viewer at the top of season 1, and its Play button played nothing at all: it resolved `$libraryItems[0]` — the first *season* by SortName — and navigated to `/player/<seasonId>`, which the player route bounced straight back to `/library/<seasonId>`. The backend could already answer "where is this viewer in this show": `repository_get_next_up_episodes` has accepted a `series_id` since it was written and no caller had ever passed one. Backend (DR-101, DR-106) - `repository/series_progress.rs`: `pick_current_episode` — in progress, else Next Up, else first unwatched, else the premiere. The third rung is the offline path, where Next Up is always empty. `sort_series_order` puts specials (season 0) after the numbered seasons. - `repository_get_series_episodes` takes over the season fan-out and the flat-series fallback, which were domain knowledge living in the frontend. - `clear_watch_history` maps to Jellyfin's mark-unplayed (recursive over a container, also zeroes resume). Offline it refuses rather than diverging state the next sync would undo. Frontend (DR-102, DR-103, DR-104, DR-107) - Seasons collapse; only the current one is expanded, and the current episode is badged and scrolled into view. - Hero button reads `Resume S2E4` / `Play S1E1` and opens that episode's focus view, where Play commits (ux-flows §5B.5). - Seasons are no longer a destination: `/library/<seasonId>` redirects to `/library/<seriesId>#season-N`, and every inbound link follows. - The "More Episodes" strip spans the whole series, so a season finale offers the next premiere instead of dead-ending (§5B.2). - Clear-history buttons on the series hero and each season header. Routes (DR-105) - `/library/tv` and `/library/movies` absorb their all-titles and genres pages as `?view=` tabs; the four legacy routes redirect. 6 video routes become 2, and `/library/shows/genres` stops being the odd one out. Logic extracted to `seriesNavigation.ts`, `episodeStrip.ts` and `libraryView.ts` so it is unit-tested rather than buried in components. Spec: docs/specs/series-current-episode-navigation.md
1459 lines
60 KiB
Markdown
1459 lines
60 KiB
Markdown
# 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
|
||
|
||
```mermaid
|
||
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
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
Launch[App Launch] --> LoadAuth[Load Stored Token]
|
||
LoadAuth --> Validate{Token Valid?}
|
||
Validate -->|Yes| RestoreState[Restore Last Screen]
|
||
Validate -->|No| LoginScreen[Login Screen<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
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
Start[User Action] --> Action{Action Type?}
|
||
|
||
Action -->|Click Track| TrackList[TrackList Component]
|
||
Action -->|Click Album| AlbumDetail[Album Detail Page]
|
||
Action -->|Click Play on Album| AlbumPlay[Play Album Button]
|
||
|
||
TrackList --> PlayTrack[Play Single Track]
|
||
PlayTrack --> QueueAll[Queue All Filtered Tracks]
|
||
|
||
AlbumPlay --> PlayAlbum[Play All Album Tracks]
|
||
PlayAlbum --> QueueAlbum[Queue Album Tracks]
|
||
|
||
QueueAll --> InvokePlay[invoke player_play_queue]
|
||
QueueAlbum --> InvokePlay
|
||
|
||
InvokePlay --> PlayerStarts[Player State: Playing]
|
||
PlayerStarts --> MiniAppears[MiniPlayer Slides Up<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
|
||
|
||
```mermaid
|
||
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
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
FullPlayer[Full Player Screen] --> UserAction{User Action}
|
||
|
||
UserAction -->|Back Button / Close| HistoryBack[window.history.back]
|
||
UserAction -->|Bottom Nav Click| NavOther[Navigate to<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
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
Start[User Action] --> Action{Action Type?}
|
||
|
||
Action -->|Click Movie| MovieDetail[Movie Detail Page]
|
||
Action -->|Click Episode| EpisodeClick[Episode Click]
|
||
Action -->|Click Play Button| PlayButton[Play Button]
|
||
|
||
MovieDetail --> PlayMovie[Play Movie Button]
|
||
EpisodeClick --> PlayEpisode[Play Episode]
|
||
|
||
PlayMovie --> CheckResume{Resume<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
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
VideoPlaying[Video Playing] --> UserAction{User Action}
|
||
|
||
UserAction -->|Back Button| StopVideo[Stop Playback]
|
||
UserAction -->|Home Button| Background[App to Background]
|
||
UserAction -->|Video Ends| VideoEnd[Playback Ended]
|
||
|
||
StopVideo --> SaveProgress[Save Progress<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
|
||
|
||
```mermaid
|
||
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
|
||
|
||
```mermaid
|
||
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
|
||
|
||
```mermaid
|
||
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
|
||
|
||
```mermaid
|
||
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
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
Nav[Navigate to /library/[id]] --> 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 / 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.
|
||
|
||
---
|
||
|
||
## 6. Search Flow
|
||
|
||
Search is **context-scoped**: what you are looking at when you start a search
|
||
determines what the search covers. A search begun inside the Music library
|
||
searches music. A search begun from Home or the top-level library page searches
|
||
everything. The scope is always shown, and always overridable.
|
||
|
||
### 6.1 Scope is inherited from context
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
Start[User starts a search] --> Where{Where from?}
|
||
|
||
Where -->|Home (/)| All[Scope: All]
|
||
Where -->|Library root (/library)| All
|
||
Where -->|Search tab| All
|
||
Where -->|Inside Music| Music[Scope: Music]
|
||
Where -->|Inside Movies| Movies[Scope: Movies]
|
||
Where -->|Inside TV| TV[Scope: TV]
|
||
|
||
All --> Chips[Filter chips shown<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.
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
Settings[Settings → Search] --> List[Draggable list of result groups]
|
||
|
||
List --> Drag[User drags a group up or down]
|
||
Drag --> Persist[Order persisted]
|
||
|
||
Persist --> Render[Rendering a result set]
|
||
Scope[Active scope chip §6.1] --> Render
|
||
|
||
Render --> Filter[1 - Drop groups outside the active scope]
|
||
Filter --> Sort[2 - Sort remaining groups by user order]
|
||
Sort --> Prune[3 - Omit groups with no results]
|
||
Prune --> Show[Render]
|
||
```
|
||
|
||
**Scope and order compose — they are two independent axes.** The scope chip
|
||
decides *which* groups are eligible; the settings list decides *what sequence*
|
||
the eligible ones appear in. Order is preserved as a relative ranking, never
|
||
renumbered per scope:
|
||
|
||
- Scope **Music** with order `Movies → Songs → Albums → Artists → TV` renders
|
||
`Songs → Albums → Artists`. Movies and TV are filtered out; the surviving
|
||
groups keep their relative order.
|
||
- Scope **All** with the same setting renders all five in exactly that order.
|
||
- **Changing scope never rewrites the saved order.** A user who narrows to
|
||
Music and back to All sees their original arrangement intact.
|
||
|
||
**Rules:**
|
||
|
||
- **Drag and drop to reorder**, in a settings list showing every result group
|
||
(Songs, Albums, Artists, Movies, TV Shows).
|
||
- **The order applies to grouped results everywhere** — the search page and
|
||
the in-library header search alike.
|
||
- **Order is presentation-only.** It never changes which results are returned
|
||
or how they are ranked *within* a group, only the sequence groups appear in.
|
||
- **Empty groups are skipped, not gapped.** A group with no results is omitted
|
||
entirely; it does not reserve space or leave a stray heading.
|
||
- **A sensible default ships** (Songs → Albums → Artists → Movies → TV Shows)
|
||
so the setting is an adjustment, never a prerequisite.
|
||
- **Keyboard/accessible reordering must exist** alongside dragging — a
|
||
drag-only control is unusable with a screen reader or without a pointer.
|
||
|
||
### 6.4 Known deviations
|
||
|
||
Recorded so the gap between this spec and the build is explicit.
|
||
|
||
- **Scope is not implemented.** The in-library header search calls the same
|
||
unscoped query as the global search page, so searching inside TV returns
|
||
music. The backend already accepts `includeItemTypes` on both the online and
|
||
offline paths, and the per-page list search already uses it — only the global
|
||
path ignores it. *(UR-049)*
|
||
- **Filter chips do not exist** on either search surface. *(UR-049)*
|
||
- **Group order is hardcoded** to Music → Movies → TV in the results markup,
|
||
with no setting. *(UR-050)*
|
||
|
||
---
|
||
|
||
## 7. Download Flows
|
||
|
||
### 7.1 Initiating Downloads
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
User[User on Album/Track Page] --> ClickDownload[Click Download Button]
|
||
|
||
ClickDownload --> CheckType{Download Type?}
|
||
|
||
CheckType -->|Single Track| DownloadTrack[Download Single File]
|
||
CheckType -->|Album| DownloadAlbum[Download All Tracks]
|
||
CheckType -->|Artist| ShowOptions[Show Options Dialog]
|
||
|
||
ShowOptions --> UserChoice{User Choice}
|
||
UserChoice -->|Discography| DownloadAll[Download All Albums]
|
||
UserChoice -->|Select Albums| AlbumPicker[Album Selection UI]
|
||
|
||
DownloadTrack --> QueueDownload[Queue in Download Manager]
|
||
DownloadAlbum --> QueueMultiple[Queue Multiple Files]
|
||
|
||
QueueDownload --> ShowProgress[Show Progress Ring<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 |
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
Nav[Open Downloads] --> Downloads[/downloads]
|
||
|
||
Downloads --> View{View}
|
||
View -->|Downloaded (default)| Browse[Offline library browse]
|
||
View -->|Transfers| Activity[Transfer activity list]
|
||
|
||
Browse --> Libs[Libraries — only those with<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
|
||
|
||
```mermaid
|
||
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
|
||
|
||
```mermaid
|
||
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)
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
Playing[Audio Playing] --> Background{User Action}
|
||
|
||
Background -->|Home Button| AppBackground[App to Background]
|
||
Background -->|Screen Lock| ScreenLock[Screen Locked]
|
||
|
||
AppBackground --> ContinuePlay[Playback Continues]
|
||
ScreenLock --> ContinuePlay
|
||
|
||
ContinuePlay --> ShowNotification[Show Media Notification:<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).
|
||
|
||
```mermaid
|
||
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 (§9.1)]
|
||
|
||
Gate -->|Yes| Mode{Background mode armed?}
|
||
|
||
Mode -->|Background-audio toggle ON<br/>UR-040| Handoff[Hand off to native audio service<br/>WebView <video> 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/> (reflects live player state)]
|
||
|
||
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
|
||
|
||
```mermaid
|
||
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
|
||
|
||
```mermaid
|
||
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
|
||
|
||
```mermaid
|
||
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.
|