mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-28 09:38:25 +01:00
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.
88 lines
3.0 KiB
TypeScript
88 lines
3.0 KiB
TypeScript
/**
|
|
* Chapter tick marks drawn as an absolute overlay over a progress slider.
|
|
* Renders nothing for media with one or zero chapters. `pointerEvents: "none"`
|
|
* so the slider underneath still receives touches.
|
|
*/
|
|
|
|
import { memo, useState } from "react";
|
|
import { type LayoutChangeEvent, PixelRatio, View } from "react-native";
|
|
import type { ChapterMarker } from "@/utils/chapters";
|
|
|
|
interface ChapterTicksProps {
|
|
/** Pre-computed markers (caller memoizes — avoids double-computing here). */
|
|
markers: ChapterMarker[];
|
|
/** Tick colour. */
|
|
color?: string;
|
|
/** Tick height in px — slightly less than the slider track thickness. */
|
|
height?: number;
|
|
/** Tick width in px — integer to avoid sub-pixel anti-aliasing. */
|
|
width?: number;
|
|
}
|
|
|
|
function ChapterTicksComponent({
|
|
markers,
|
|
// Semi-transparent black contrasts against both the filled progress
|
|
// (#fff) and the unfilled track (rgba(255,255,255,0.2)) so the ticks
|
|
// stay visible across the whole bar as playback advances.
|
|
color = "rgba(0,0,0,0.55)",
|
|
height = 14,
|
|
width = 2,
|
|
}: ChapterTicksProps) {
|
|
// Hooks must run unconditionally — keep them before any early return.
|
|
const [sliderWidth, setSliderWidth] = useState(0);
|
|
|
|
const handleLayout = (e: LayoutChangeEvent) => {
|
|
setSliderWidth(e.nativeEvent.layout.width);
|
|
};
|
|
|
|
// One chapter (typically a single marker at 0) is not worth marking.
|
|
if (markers.length <= 1) return null;
|
|
|
|
return (
|
|
<View
|
|
pointerEvents='none'
|
|
onLayout={handleLayout}
|
|
style={{
|
|
position: "absolute",
|
|
left: 0,
|
|
right: 0,
|
|
top: 0,
|
|
bottom: 0,
|
|
// Let ticks taller than this container bleed beyond its bounds.
|
|
overflow: "visible",
|
|
}}
|
|
>
|
|
{sliderWidth > 0 &&
|
|
markers
|
|
// Skip the leading 0ms marker — it overlaps the slider start and
|
|
// adds visual noise at an already-rendered boundary.
|
|
.filter((marker) => marker.positionMs > 0)
|
|
.map((marker, index) => {
|
|
// Align both the position AND the width onto the device's
|
|
// physical pixel grid. Without this, fractional dp values land
|
|
// at different sub-pixel fractions per tick — Android samples
|
|
// each one differently and some ticks render visibly thicker.
|
|
const centerDp = (marker.percent / 100) * sliderWidth;
|
|
const left = PixelRatio.roundToNearestPixel(centerDp - width / 2);
|
|
const snappedWidth = PixelRatio.roundToNearestPixel(width);
|
|
return (
|
|
<View
|
|
key={`${marker.positionMs}-${index}`}
|
|
style={{
|
|
position: "absolute",
|
|
left,
|
|
top: "50%",
|
|
marginTop: -height / 2,
|
|
height,
|
|
width: snappedWidth,
|
|
backgroundColor: color,
|
|
}}
|
|
/>
|
|
);
|
|
})}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
export const ChapterTicks = memo(ChapterTicksComponent);
|