mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-23 07:16:36 +01:00
fix(chapters): sort chapter list entries, localize strings, fix tick keys
This commit is contained in:
@@ -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)}
|
||||||
|
|||||||
@@ -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}%`,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user