fix(chapters): sort chapter list entries, localize strings, fix tick keys

This commit is contained in:
Uruk
2026-05-22 12:06:53 +02:00
parent e649414e3f
commit 87b181b66f
5 changed files with 50 additions and 9 deletions

View File

@@ -6,12 +6,13 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import type { ChapterInfo } from "@jellyfin/sdk/lib/generated-client/models"; import type { ChapterInfo } from "@jellyfin/sdk/lib/generated-client/models";
import { useTranslation } from "react-i18next";
import { FlatList, Modal, Pressable, View } from "react-native"; import { FlatList, Modal, Pressable, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { import {
chapterStartsMs,
currentChapterIndex, currentChapterIndex,
formatChapterTime, formatChapterTime,
sortedChapters,
} from "@/utils/chapters"; } from "@/utils/chapters";
interface ChapterListProps { interface ChapterListProps {
@@ -31,9 +32,9 @@ export function ChapterList({
onSeek, onSeek,
onClose, onClose,
}: ChapterListProps) { }: ChapterListProps) {
const starts = chapterStartsMs(chapters); const { t } = useTranslation();
const entries = sortedChapters(chapters);
const activeIndex = currentChapterIndex(currentPositionMs, chapters); const activeIndex = currentChapterIndex(currentPositionMs, chapters);
const list = chapters ?? [];
return ( return (
<Modal <Modal
@@ -69,17 +70,17 @@ export function ChapterList({
}} }}
> >
<Text style={{ color: "#fff", fontSize: 17, fontWeight: "700" }}> <Text style={{ color: "#fff", fontSize: 17, fontWeight: "700" }}>
Chapters {t("chapters.title")}
</Text> </Text>
<Pressable onPress={onClose} hitSlop={10}> <Pressable onPress={onClose} hitSlop={10}>
<Ionicons name='close' size={24} color='#fff' /> <Ionicons name='close' size={24} color='#fff' />
</Pressable> </Pressable>
</View> </View>
<FlatList <FlatList
data={list} data={entries}
keyExtractor={(_, i) => String(i)} keyExtractor={(_, i) => String(i)}
renderItem={({ item, index }) => { renderItem={({ item, index }) => {
const positionMs = starts[index] ?? 0; const positionMs = item.positionMs;
const isActive = index === activeIndex; const isActive = index === activeIndex;
return ( return (
<Pressable <Pressable
@@ -104,7 +105,8 @@ export function ChapterList({
}} }}
numberOfLines={1} numberOfLines={1}
> >
{item.Name || `Chapter ${index + 1}`} {item.chapter.Name ||
t("chapters.chapter_number", { number: index + 1 })}
</Text> </Text>
<Text style={{ color: "#999", fontSize: 13, marginLeft: 12 }}> <Text style={{ color: "#999", fontSize: 13, marginLeft: 12 }}>
{formatChapterTime(positionMs)} {formatChapterTime(positionMs)}

View File

@@ -30,9 +30,9 @@ export function ChapterTicks({
pointerEvents='none' pointerEvents='none'
style={{ position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }} style={{ position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }}
> >
{markers.map((marker) => ( {markers.map((marker, index) => (
<View <View
key={marker.positionMs} key={`${marker.positionMs}-${index}`}
style={{ style={{
position: "absolute", position: "absolute",
left: `${marker.percent}%`, left: `${marker.percent}%`,

View File

@@ -610,6 +610,10 @@
"downloaded_file_no": "No", "downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel" "downloaded_file_cancel": "Cancel"
}, },
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}"
},
"item_card": { "item_card": {
"next_up": "Next Up", "next_up": "Next Up",
"no_items_to_display": "No Items to Display", "no_items_to_display": "No Items to Display",

View File

@@ -3,6 +3,7 @@ import {
chapterMarkers, chapterMarkers,
currentChapterIndex, currentChapterIndex,
formatChapterTime, formatChapterTime,
sortedChapters,
} from "./chapters"; } from "./chapters";
// Helper: a ChapterInfo with a start in milliseconds. // Helper: a ChapterInfo with a start in milliseconds.
@@ -48,6 +49,23 @@ describe("currentChapterIndex", () => {
}); });
}); });
describe("sortedChapters", () => {
test("pairs each chapter with its ms start, sorted ascending", () => {
const a = ch(60_000, "C");
const b = ch(0, "A");
const c = ch(30_000, "B");
expect(sortedChapters([a, b, c])).toEqual([
{ chapter: b, positionMs: 0 },
{ chapter: c, positionMs: 30_000 },
{ chapter: a, positionMs: 60_000 },
]);
});
test("returns [] for null/undefined", () => {
expect(sortedChapters(null)).toEqual([]);
expect(sortedChapters(undefined)).toEqual([]);
});
});
describe("formatChapterTime", () => { describe("formatChapterTime", () => {
test("formats m:ss and h:mm:ss", () => { test("formats m:ss and h:mm:ss", () => {
expect(formatChapterTime(65_000)).toBe("1:05"); expect(formatChapterTime(65_000)).toBe("1:05");

View File

@@ -14,6 +14,23 @@ export interface ChapterMarker {
percent: number; percent: number;
} }
export interface ChapterEntry {
chapter: ChapterInfo;
/** Chapter start, in milliseconds. */
positionMs: number;
}
/** Chapters paired with their millisecond start, sorted ascending by start. */
export const sortedChapters = (
chapters: ChapterInfo[] | null | undefined,
): ChapterEntry[] =>
(chapters ?? [])
.map((chapter) => ({
chapter,
positionMs: (chapter.StartPositionTicks ?? 0) / TICKS_PER_MS,
}))
.sort((a, b) => a.positionMs - b.positionMs);
/** Chapter start positions in milliseconds, ascending. */ /** Chapter start positions in milliseconds, ascending. */
export const chapterStartsMs = ( export const chapterStartsMs = (
chapters: ChapterInfo[] | null | undefined, chapters: ChapterInfo[] | null | undefined,