Files
streamyfin/docs/superpowers/plans/2026-05-22-chromecast-player-split.md
Uruk 8c9506c7b5 docs(casting): add casting-player split implementation plan
10-task plan for sub-project C: extract 6 presentational components +
4 hooks from casting-player.tsx, leaving a thin orchestrator. Purely
mechanical, behaviour-neutral.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 00:56:21 +02:00

20 KiB

Chromecast Player Split — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Decompose the 1428-line app/(auth)/casting-player.tsx god-component into 4 hooks + 6 presentational components + a thin orchestrator, with zero behaviour change.

Architecture: Each task extracts one JSX section into a presentational component (components/casting/player/) or one logic cluster into a hook (hooks/), then rewires casting-player.tsx to use it. Purely mechanical — moved code, not new code. State flows orchestrator → components by typed props.

Tech Stack: TypeScript (strict), React Native / Expo, react-native-google-cast, react-native-reanimated, react-native-gesture-handler.

Spec: docs/superpowers/specs/2026-05-22-chromecast-player-split-design.md

Environment note: Windows checkout, core.autocrlf=true — project-wide bun run check reports ~124 pre-existing CRLF errors unrelated to this work. The gate is bun run typecheck (fully green) plus Biome on the files each task edits.


Hard rules for every task

  1. Zero behaviour change. This is a mechanical extraction. Move code; do not rewrite logic, rename behaviour, fix bugs, or "improve" anything. Known issues stay untouched (they belong to a later UX sub-project).
  2. Read app/(auth)/casting-player.tsx first. It is large (~1400 lines). Locate the section by the quoted {/* comment */} anchor, not line numbers.
  3. Derive interfaces from real usage. A component's props = exactly the values the extracted JSX references from the surrounding scope (state, derived values, handlers). A hook's return = exactly the values the rest of casting-player.tsx still needs. Type every prop / return field explicitly — no any.
  4. bun run typecheck must be fully green before each commit. It is the safety net that catches a missed prop or broken wiring.
  5. Preserve imports: when a section moves out, move its imports too; remove imports from casting-player.tsx that it no longer uses.

File Structure

File Responsibility Task
components/casting/player/CastPlayerHeader.tsx Dismiss chevron, connection indicator, settings button 1
components/casting/player/CastPlayerTitle.tsx Title + episode/season info 1
components/casting/player/CastPlayerPoster.tsx Poster image, buffering overlay, skip intro/credits bar 2
components/casting/player/CastPlayerEpisodeControls.tsx 4-button row (Episodes / Previous / Next / Stop) 3
components/casting/player/CastPlayerProgressBar.tsx Slider, trickplay preview, time display 4
components/casting/player/CastPlayerTransportControls.tsx Rewind / play-pause / forward 5
hooks/useCastPlayerItem.ts fetchedItem + fetch + currentItem derivation 6
hooks/useCastEpisodes.ts episodes/nextEpisode/seasonData + fetches + loadEpisode 7
hooks/useCastDismissGesture.ts dismiss gesture / animated style 8
hooks/useCastPlayerProgress.ts slider, scrubbing, live progress, trickplay 9
app/(auth)/casting-player.tsx Thin orchestrator all

Tasks 1-5 extract components (the orchestrator keeps all logic, just renders <X/>). Tasks 6-9 extract hooks. Task 10 finalises and verifies.


Task 1: Extract CastPlayerHeader and CastPlayerTitle

Files:

  • Create: components/casting/player/CastPlayerHeader.tsx, components/casting/player/CastPlayerTitle.tsx

  • Modify: app/(auth)/casting-player.tsx

  • Step 1: Create CastPlayerHeader.tsx

In casting-player.tsx, locate the JSX under {/* Header - Fixed at top */} (the <View> containing the dismiss Pressable with the chevron-down icon, the connection-indicator Pressable, and the settings-button Pressable with settings-outline).

Create components/casting/player/CastPlayerHeader.tsx as a presentational component containing exactly that JSX. Define a CastPlayerHeaderProps interface for every value the JSX references from the outer scope — e.g. the dismiss handler, the connection state / device name shown in the indicator, the connection-indicator press handler, the settings press handler, and any style insets. Type each explicitly. Move the icon/Text/Pressable imports the component needs.

  • Step 2: Create CastPlayerTitle.tsx

Locate the JSX under {/* Title Area */} (the <View> with the title Text and the grey episode/season info Text). Create components/casting/player/CastPlayerTitle.tsx the same way — a CastPlayerTitleProps interface for exactly what it references (title string, episode/season info string or the item fields it derives them from — keep the derivation identical to the original).

  • Step 3: Rewire casting-player.tsx

Replace the {/* Header - Fixed at top */} block with <CastPlayerHeader ... /> and the {/* Title Area */} block with <CastPlayerTitle ... />, passing the props. Add the two imports. Remove now-unused imports from casting-player.tsx.

  • Step 4: Verify

Run: bun run typecheck Expected: PASS — fully green.

Run: bunx biome check components/casting/player/CastPlayerHeader.tsx components/casting/player/CastPlayerTitle.tsx "app/(auth)/casting-player.tsx" Expected: PASS on these files.

  • Step 5: Commit
git add components/casting/player/ "app/(auth)/casting-player.tsx"
git commit -m "refactor(casting): extract CastPlayerHeader and CastPlayerTitle"

Task 2: Extract CastPlayerPoster

Files:

  • Create: components/casting/player/CastPlayerPoster.tsx

  • Modify: app/(auth)/casting-player.tsx

  • Step 1: Create the component

Locate the JSX under {/* Poster with buffering overlay */} (inside the scrollable content area) — the poster Image, the empty-poster fallback View with the film-outline icon, the {/* Skip intro/credits bar */} block, and the {/* Buffering overlay */} block with the ActivityIndicator.

Create components/casting/player/CastPlayerPoster.tsx containing that JSX. Define CastPlayerPosterProps for everything it references: the poster URL, buffering state, the skip-segment data and skip handlers, the translation function if used, etc. Type each field. Move the imports it needs.

  • Step 2: Rewire

Replace the poster block in casting-player.tsx with <CastPlayerPoster ... />. Add the import; drop unused imports.

  • Step 3: Verify

Run: bun run typecheck Expected: PASS — fully green.

Run: bunx biome check components/casting/player/CastPlayerPoster.tsx "app/(auth)/casting-player.tsx" Expected: PASS on these files.

  • Step 4: Commit
git add components/casting/player/CastPlayerPoster.tsx "app/(auth)/casting-player.tsx"
git commit -m "refactor(casting): extract CastPlayerPoster"

Task 3: Extract CastPlayerEpisodeControls

Files:

  • Create: components/casting/player/CastPlayerEpisodeControls.tsx

  • Modify: app/(auth)/casting-player.tsx

  • Step 1: Create the component

Locate the JSX under {/* Fixed 4-button control row for episodes - positioned independently */} — the <View> with the four Pressables: Episodes (list icon), Previous episode (play-skip-back), Next episode (play-skip-forward), Stop (stop-circle).

Create components/casting/player/CastPlayerEpisodeControls.tsx with that JSX. Define CastPlayerEpisodeControlsProps for each button's press handler and each button's enabled/visible condition exactly as the original computes it (e.g. whether a previous/next episode exists). Keep the conditions identical. Move the imports.

  • Step 2: Rewire

Replace the 4-button block in casting-player.tsx with <CastPlayerEpisodeControls ... />. Add the import; drop unused imports.

  • Step 3: Verify

Run: bun run typecheck Expected: PASS — fully green.

Run: bunx biome check components/casting/player/CastPlayerEpisodeControls.tsx "app/(auth)/casting-player.tsx" Expected: PASS on these files.

  • Step 4: Commit
git add components/casting/player/CastPlayerEpisodeControls.tsx "app/(auth)/casting-player.tsx"
git commit -m "refactor(casting): extract CastPlayerEpisodeControls"

Task 4: Extract CastPlayerProgressBar

Files:

  • Create: components/casting/player/CastPlayerProgressBar.tsx

  • Modify: app/(auth)/casting-player.tsx

  • Step 1: Create the component

Locate the JSX under {/* Progress slider with trickplay preview */} and the adjacent {/* Time display */} block (both inside {/* Fixed bottom controls area */}). This includes the <Slider> with its trickplay-preview render callback and the two time Texts.

Create components/casting/player/CastPlayerProgressBar.tsx with that JSX. Define CastPlayerProgressBarProps for everything it references: the slider shared values, the scrub handlers, the trickplay URL/time/info, the formatted current/end time strings (or the values they derive from), protocolColor, etc. Type each field. Move the imports (Slider, reanimated, Image, Text, …).

The slider passes react-native-reanimated shared values. They pass through props unchanged — keep their types (SharedValue<number>).

  • Step 2: Rewire

Replace the progress-slider + time-display JSX in casting-player.tsx with <CastPlayerProgressBar ... /> (keep it inside the bottom-controls <View>). Add the import; drop unused imports.

  • Step 3: Verify

Run: bun run typecheck Expected: PASS — fully green.

Run: bunx biome check components/casting/player/CastPlayerProgressBar.tsx "app/(auth)/casting-player.tsx" Expected: PASS on these files.

  • Step 4: Commit
git add components/casting/player/CastPlayerProgressBar.tsx "app/(auth)/casting-player.tsx"
git commit -m "refactor(casting): extract CastPlayerProgressBar"

Task 5: Extract CastPlayerTransportControls

Files:

  • Create: components/casting/player/CastPlayerTransportControls.tsx

  • Modify: app/(auth)/casting-player.tsx

  • Step 1: Create the component

Locate the JSX under {/* Playback controls */} — the <View> with the Rewind Pressable, the Play/Pause Pressable, and the Forward Pressable (the rewind/forward show the configured skip seconds).

Create components/casting/player/CastPlayerTransportControls.tsx with that JSX. Define CastPlayerTransportControlsProps for: the play/pause state, the play/pause handler, the rewind/forward handlers, the rewind/forward skip-second values shown on the buttons. Type each. Move the imports.

  • Step 2: Rewire

Replace the playback-controls block in casting-player.tsx with <CastPlayerTransportControls ... />. Add the import; drop unused imports.

  • Step 3: Verify

Run: bun run typecheck Expected: PASS — fully green.

Run: bunx biome check components/casting/player/CastPlayerTransportControls.tsx "app/(auth)/casting-player.tsx" Expected: PASS on these files.

  • Step 4: Commit
git add components/casting/player/CastPlayerTransportControls.tsx "app/(auth)/casting-player.tsx"
git commit -m "refactor(casting): extract CastPlayerTransportControls"

Task 6: Extract useCastPlayerItem

Files:

  • Create: hooks/useCastPlayerItem.ts

  • Modify: app/(auth)/casting-player.tsx

  • Step 1: Create the hook

In casting-player.tsx, identify: the fetchedItem useState, the useEffect that fetches the full item from the Jellyfin API into fetchedItem, and the currentItem useMemo (which derives the effective item from fetchedItem and the cast customData).

Create hooks/useCastPlayerItem.ts exporting useCastPlayerItem. Move that state, effect, and memo into it. The hook takes whatever inputs those blocks reference (e.g. api, user, mediaStatus, the route params) as parameters, and returns { fetchedItem, currentItem }. Keep the fetch logic and the derivation byte-for-byte identical.

  • Step 2: Rewire

In casting-player.tsx, replace the moved useState / useEffect / useMemo with const { fetchedItem, currentItem } = useCastPlayerItem({ ... });. Add the import; drop now-unused imports.

  • Step 3: Verify

Run: bun run typecheck Expected: PASS — fully green.

Run: bunx biome check hooks/useCastPlayerItem.ts "app/(auth)/casting-player.tsx" Expected: PASS on these files.

  • Step 4: Commit
git add hooks/useCastPlayerItem.ts "app/(auth)/casting-player.tsx"
git commit -m "refactor(casting): extract useCastPlayerItem hook"

Task 7: Extract useCastEpisodes

Files:

  • Create: hooks/useCastEpisodes.ts

  • Modify: app/(auth)/casting-player.tsx

  • Step 1: Create the hook

Identify in casting-player.tsx: the episodes, nextEpisode, and seasonData useStates; the useEffect(s) that fetch the season / episode list; and the loadEpisode useCallback.

Create hooks/useCastEpisodes.ts exporting useCastEpisodes. Move that state, those effects, and loadEpisode into it. The hook takes the inputs they reference (api, user, currentItem, remoteMediaClient, castDevice, settings, …) as parameters and returns { episodes, nextEpisode, seasonData, loadEpisode }. Keep all logic identical, including the loadEpisode body.

  • Step 2: Rewire

Replace the moved declarations with const { episodes, nextEpisode, seasonData, loadEpisode } = useCastEpisodes({ ... });. Add the import; drop unused imports.

  • Step 3: Verify

Run: bun run typecheck Expected: PASS — fully green.

Run: bunx biome check hooks/useCastEpisodes.ts "app/(auth)/casting-player.tsx" Expected: PASS on these files.

  • Step 4: Commit
git add hooks/useCastEpisodes.ts "app/(auth)/casting-player.tsx"
git commit -m "refactor(casting): extract useCastEpisodes hook"

Task 8: Extract useCastDismissGesture

Files:

  • Create: hooks/useCastDismissGesture.ts

  • Modify: app/(auth)/casting-player.tsx

  • Step 1: Create the hook

Identify in casting-player.tsx: the translateY and context shared values, the dismissModal useCallback, the panGesture (Gesture.Pan()...), and the animatedStyle (useAnimatedStyle).

Create hooks/useCastDismissGesture.ts exporting useCastDismissGesture. Move those into it. The hook takes whatever the dismiss logic references (e.g. the navigation/router used to close the screen) as parameters and returns { panGesture, animatedStyle, dismissModal }. Keep the gesture thresholds and animation logic identical.

  • Step 2: Rewire

Replace the moved declarations with const { panGesture, animatedStyle, dismissModal } = useCastDismissGesture({ ... });. Add the import; drop unused imports.

  • Step 3: Verify

Run: bun run typecheck Expected: PASS — fully green.

Run: bunx biome check hooks/useCastDismissGesture.ts "app/(auth)/casting-player.tsx" Expected: PASS on these files.

  • Step 4: Commit
git add hooks/useCastDismissGesture.ts "app/(auth)/casting-player.tsx"
git commit -m "refactor(casting): extract useCastDismissGesture hook"

Task 9: Extract useCastPlayerProgress

Files:

  • Create: hooks/useCastPlayerProgress.ts
  • Modify: app/(auth)/casting-player.tsx

This is the most intricate extraction — reanimated shared values, timing refs, the live-progress interpolation, scrubbing, and the useTrickplay integration. Move it as one cohesive cluster; do not split or simplify it.

  • Step 1: Create the hook

Identify in casting-player.tsx the progress/slider cluster: the sliderProgress / sliderMin / sliderMax shared values; the isScrubbing ref; the trickplayTime and scrubPercentage state; the liveProgress state with lastSyncPositionRef / lastSyncTimestampRef; the resumePositionRef; the effects that sync liveProgress from mediaStatus and update the refs; the useTrickplay(...) call; and any scrub-start/move/end handlers.

Create hooks/useCastPlayerProgress.ts exporting useCastPlayerProgress. Move that whole cluster in. The hook takes the inputs the cluster references (mediaStatus, duration, the trickplay inputs, …) as parameters and returns everything the JSX and the rest of the orchestrator still need — e.g. { sliderProgress, sliderMin, sliderMax, isScrubbing, trickplayTime, scrubPercentage, progress, liveProgress, resumePositionRef, trickPlayUrl, calculateTrickplayUrl, trickplayInfo, ...scrub handlers }. Keep every effect, ref, and computation byte-for-byte identical and in the same order.

  • Step 2: Rewire

Replace the moved cluster with const { ... } = useCastPlayerProgress({ ... });. The CastPlayerProgressBar component (Task 4) and reloadWithSelection consume these values — make sure they still receive them. Add the import; drop unused imports.

  • Step 3: Verify

Run: bun run typecheck Expected: PASS — fully green.

Run: bun test utils/casting/ Expected: PASS — all suites pass (the pure-logic suites are unaffected).

Run: bunx biome check hooks/useCastPlayerProgress.ts "app/(auth)/casting-player.tsx" Expected: PASS on these files.

  • Step 4: Commit
git add hooks/useCastPlayerProgress.ts "app/(auth)/casting-player.tsx"
git commit -m "refactor(casting): extract useCastPlayerProgress hook"

Task 10: Finalise the orchestrator

Files:

  • Modify: app/(auth)/casting-player.tsx

  • Step 1: Clean up

casting-player.tsx should now be a thin orchestrator: hook calls, a small amount of wiring, and the JSX tree composing the 6 new components plus the 3 modal components (ChromecastDeviceSheet, ChromecastEpisodeList, ChromecastSettingsMenu) inside the GestureDetector / Animated.View.

Read the whole file. Remove any now-dead code: unused imports, leftover variables, commented-out fragments, intermediate values that are no longer referenced. Do not change behaviour — only delete what is provably unused.

  • Step 2: Verify

Run: bun run typecheck Expected: PASS — fully green.

Run: bun test utils/casting/ Expected: PASS.

Run: bunx biome check "app/(auth)/casting-player.tsx" Expected: PASS.

Confirm casting-player.tsx is in the ~150-250 line range and contains no large inline JSX section or logic cluster.

  • Step 3: Commit
git add "app/(auth)/casting-player.tsx"
git commit -m "refactor(casting): finalise casting-player orchestrator"

Final verification

  • Checks

Run: bun run typecheck → PASS. Run: bun test utils/casting/ → PASS.

  • Manual re-test (behaviour must be identical to before the split)

Cast a movie and an episode to the Chromecast. Verify: playback starts; the header / title / poster render; the 4 episode buttons work; audio / subtitle / quality / version switching still works; episode navigation works; the progress slider scrubs and shows trickplay; play/pause/rewind/forward work; the buffering overlay appears; swiping down dismisses the player. Nothing should behave differently from before sub-project C.


Notes for the implementer

  • Line numbers drift across tasks — always Read the file and match on the quoted {/* comment */} anchors.
  • This is a refactor: there is no new behaviour to unit-test. bun run typecheck per task is the safety net; the final manual re-test is the behavioural check.
  • If extracting a section reveals it is entangled with another (a shared variable that does not cleanly belong to one unit), keep that variable in the orchestrator and pass it as a prop / hook argument — do not duplicate it.
  • Out of scope: the trickplay truncation bug, the progress-bar touch-overlap bug, the time-label position, mini-player changes, the loadEpisode/currentItem race. Do not fix them here — they belong to the later UX sub-project.