From ff3a656d8a4b3ecc0bfeaa76ac5fb5caebb5cc3a Mon Sep 17 00:00:00 2001 From: Uruk Date: Fri, 22 May 2026 11:50:04 +0200 Subject: [PATCH] feat(chapters): add ChapterList modal --- components/chapters/ChapterList.tsx | 120 ++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 components/chapters/ChapterList.tsx diff --git a/components/chapters/ChapterList.tsx b/components/chapters/ChapterList.tsx new file mode 100644 index 00000000..c9143ee4 --- /dev/null +++ b/components/chapters/ChapterList.tsx @@ -0,0 +1,120 @@ +/** + * A modal listing an item's chapters. Each row shows the chapter name and its + * timestamp; the current chapter is highlighted. Tapping a row seeks to that + * chapter and closes the modal. Player-agnostic — the seek is injected. + */ + +import { Ionicons } from "@expo/vector-icons"; +import type { ChapterInfo } from "@jellyfin/sdk/lib/generated-client/models"; +import { FlatList, Modal, Pressable, View } from "react-native"; +import { Text } from "@/components/common/Text"; +import { + chapterStartsMs, + currentChapterIndex, + formatChapterTime, +} from "@/utils/chapters"; + +interface ChapterListProps { + visible: boolean; + chapters: ChapterInfo[] | null | undefined; + /** Current playback position in milliseconds (to highlight the row). */ + currentPositionMs: number; + /** Seek the player to this millisecond position. */ + onSeek: (positionMs: number) => void; + onClose: () => void; +} + +export function ChapterList({ + visible, + chapters, + currentPositionMs, + onSeek, + onClose, +}: ChapterListProps) { + const starts = chapterStartsMs(chapters); + const activeIndex = currentChapterIndex(currentPositionMs, chapters); + const list = chapters ?? []; + + return ( + + + e.stopPropagation()} + style={{ + backgroundColor: "#1a1a1a", + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + maxHeight: "70%", + paddingBottom: 24, + }} + > + + + Chapters + + + + + + String(i)} + renderItem={({ item, index }) => { + const positionMs = starts[index] ?? 0; + const isActive = index === activeIndex; + return ( + { + onSeek(positionMs); + onClose(); + }} + style={{ + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingHorizontal: 16, + paddingVertical: 14, + backgroundColor: isActive ? "#a855f733" : "transparent", + }} + > + + {item.Name || `Chapter ${index + 1}`} + + + {formatChapterTime(positionMs)} + + + ); + }} + /> + + + + ); +}