diff --git a/docs/superpowers/plans/2026-05-22-chromecast-player-split.md b/docs/superpowers/plans/2026-05-22-chromecast-player-split.md new file mode 100644 index 000000000..0c7c8b685 --- /dev/null +++ b/docs/superpowers/plans/2026-05-22-chromecast-player-split.md @@ -0,0 +1,409 @@ +# 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 ``). +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 `` 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 `` 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 `` and the `{/* Title Area */}` block with ``, 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** + +```bash +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 ``. 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** + +```bash +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 `` with the four `Pressable`s: 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 ``. 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** + +```bash +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 `` with its trickplay-preview render callback and the two time `Text`s. + +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`). + +- [ ] **Step 2: Rewire** + +Replace the progress-slider + time-display JSX in `casting-player.tsx` with `` (keep it inside the bottom-controls ``). 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** + +```bash +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 `` 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 ``. 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** + +```bash +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** + +```bash +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` `useState`s; 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** + +```bash +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** + +```bash +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** + +```bash +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** + +```bash +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.