From e649414e3f2bb7a3651125f8297416d3e152470c Mon Sep 17 00:00:00 2001 From: Uruk Date: Fri, 22 May 2026 11:52:28 +0200 Subject: [PATCH] feat(chapters): chapter ticks and list in the native player --- .../video-player/controls/BottomControls.tsx | 42 +++++++++++++++++-- components/video-player/controls/Controls.tsx | 2 + 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/components/video-player/controls/BottomControls.tsx b/components/video-player/controls/BottomControls.tsx index 51abf68c..b2d6d69e 100644 --- a/components/video-player/controls/BottomControls.tsx +++ b/components/video-player/controls/BottomControls.tsx @@ -1,11 +1,18 @@ -import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; -import type { FC } from "react"; -import { View } from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import type { + BaseItemDto, + ChapterInfo, +} from "@jellyfin/sdk/lib/generated-client"; +import { type FC, useState } from "react"; +import { Pressable, View } from "react-native"; import { Slider } from "react-native-awesome-slider"; import { type SharedValue } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { ChapterList } from "@/components/chapters/ChapterList"; +import { ChapterTicks } from "@/components/chapters/ChapterTicks"; import { Text } from "@/components/common/Text"; import { useSettings } from "@/utils/atoms/settings"; +import { chapterMarkers } from "@/utils/chapters"; import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton"; import SkipButton from "./SkipButton"; import { TimeDisplay } from "./TimeDisplay"; @@ -13,6 +20,10 @@ import { TrickplayBubble } from "./TrickplayBubble"; interface BottomControlsProps { item: BaseItemDto; + /** Item chapters, used for the tick overlay and chapter list. */ + chapters?: ChapterInfo[] | null; + /** Total media duration in milliseconds. */ + durationMs: number; showControls: boolean; isSliding: boolean; showRemoteBubble: boolean; @@ -61,6 +72,8 @@ interface BottomControlsProps { export const BottomControls: FC = ({ item, + chapters, + durationMs, showControls, isSliding, showRemoteBubble, @@ -90,6 +103,10 @@ export const BottomControls: FC = ({ }) => { const { settings } = useSettings(); const insets = useSafeAreaInsets(); + const [chapterListVisible, setChapterListVisible] = useState(false); + + // Only expose chapter UI when there are at least two real markers. + const hasChapters = chapterMarkers(chapters, durationMs).length > 1; return ( = ({ {item?.Album} )} - + + {hasChapters && ( + setChapterListVisible(true)} + hitSlop={10} + className='justify-center' + > + + + )} = ({ minimumValue={min} maximumValue={max} /> + = ({ /> + handleSliderComplete(ms)} + onClose={() => setChapterListVisible(false)} + /> ); }; diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 96dfad6b..c89f11b8 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -528,6 +528,8 @@ export const Controls: FC = ({ >