Copilot + CodeRabbit review findings:
- React.memo ChapterTicks and ChapterList (project guideline: hot-path
components must use React.memo to cut redraw work during control
updates).
- chapterNameAt now sorts the chapter array once instead of twice per
call. The previous version went through currentChapterIndex
(chapterStartsMs + sort) then sortedChapters (sort again). Runs on
every playback tick, so the duplicate work added up.
- Import getUserLibraryApi from the public barrel
(@jellyfin/sdk/lib/utils/api) instead of the deep internal path
(@jellyfin/sdk/lib/utils/api/user-library-api) to match the rest of
the codebase and avoid coupling to SDK file layout.
TrickplayBubble polish:
- Sit just above the slider (bottom: 0) so the bubble no longer overlaps
the progress bar.
- Move the chapter-name + timestamp overlay to the bottom-left of the
preview frame, smaller font, in front of the surrounding overlays
(zIndex + elevation).
BottomControls cleanup:
- Drop dev-only "pick one to test" comment in favour of a one-line note
on TICK_HEIGHT.
- Inline scrubMs into its useMemo callback so the scrub-chapter-name
lookup only recomputes while a slide is active.
Some screens fetch items with a fields filter (e.g. series detail uses
["MediaSources", "MediaStreams", "Overview", "Trickplay"]) and pass the
resulting DTO straight into the download flow. Jellyfin only returns
Chapters in that case when explicitly listed, so the snapshot we store
under DownloadedItem.item carries Chapters: undefined and the offline
player renders no ticks / list / current-chapter label.
initiateDownload now re-fetches the item via getUserLibraryApi.getItem
(no fields filter -> full DTO incl. Chapters) when Chapters is missing,
and uses the enriched item for both getDownloadUrl and the snapshot. If
the refresh call fails we log and fall back to the original item — the
download itself still proceeds.
Trickplay offline already worked (useTrickplay reads trickPlayData.path
from the downloaded sheets).
Threads chapters + duration through Controls -> BottomControls so the
ChapterTicks overlay, ChapterList modal and current-chapter label have
the data they need.
BottomControls
- Memoizes chapterMarkerList (markers within the media duration) once
per (chapters, durationMs) change and feeds it to ChapterTicks.
- hasChapters gates the bookmark icon + list modal; nothing renders
when chapters are missing or below two real markers.
- Current chapter name shown as a small label below the title/year
during playback; the same helper feeds the trickplay bubble while
scrubbing. Both labels disappear gracefully when chapters are absent.
useVideoSlider
- Adds seekTo(value): a programmatic seek for non-gesture entry points
(chapter list, hot-keys). Reads isPlaying directly instead of
wasPlayingRef — which is only populated inside handleSliderStart, so
a tap-to-seek on the chapter list previously either stranded paused
playback or auto-resumed against a manual pause.
TrickplayBubble
- Adds an optional chapterName prop; renders a small left-aligned
overlay inside the preview frame (Jellyfin web style) showing chapter
name above the timestamp. Hides the chapter line entirely when null.
- zIndex + elevation so the bubble lands in front of the title /
surrounding overlays.
- Slight reposition (bottom -20, paddingTop 12) brings the bubble
closer to the slider.
translations/en.json
- chapters.title / chapters.chapter_number / chapters.open /
chapters.close keys for the list modal and the bookmark a11y label.
Two presentational components, both player-agnostic:
ChapterTicks — absolute overlay that draws tick marks on the progress
slider, one per chapter start (skipping the leading 0ms marker).
- Reads markers from a memoized prop computed by the caller so the
filter/sort runs at most once per chapters change, not per render.
- Snaps tick position AND width to the device pixel grid via
PixelRatio.roundToNearestPixel(). Without this, fractional dp values
land at different sub-pixel fractions on non-integer density displays
(420dpi -> 2.625x ratio) and Android anti-aliases each tick
differently, making some look visibly thicker than others.
- Tick colour defaults to rgba(0,0,0,0.55), contrasting against both
the filled progress (#fff) and the unfilled track so ticks stay
visible as playback advances.
- pointerEvents="none" so the slider underneath still receives touches.
- overflow: "visible" so taller ticks can bleed past the parent track.
ChapterList — bottom-sheet modal listing chapters with their timestamps.
- Highlights the currently active row (purple primary tint).
- Falls back to a localized "Chapter N" label when a chapter has no name.
- Imperatively scrolls to the active row each time the sheet becomes
visible. <Modal> keeps its children mounted across visible toggles,
so FlatList.initialScrollIndex (which only fires at first mount) would
only work on the very first open. Uses a ref + useEffect on `visible`
+ scrollToIndex inside requestAnimationFrame, with an
onScrollToIndexFailed fallback for indices outside the render window.
- All static styles in StyleSheet.create() — only dynamic backgroundColor
/ text colour stays inline. The list re-renders on every playback tick
so cutting the per-render style allocations is worth it.
- Colors from constants/Colors.ts (primary, background, text, icon),
no hardcoded hex.
Add new setting to completely disable the auto-play next episode feature.
When disabled, the countdown button is hidden and the max episode count
setting appears greyed out.