diff --git a/.claude/commands/reflect.md b/.claude/commands/reflect.md index 2ee23479..deedf8d4 100644 --- a/.claude/commands/reflect.md +++ b/.claude/commands/reflect.md @@ -12,26 +12,59 @@ Analyze the current conversation to extract useful facts that should be remember ## Instructions -1. Read the existing facts file at `.claude/learned-facts.md` +1. Read the Learned Facts Index section in `CLAUDE.md` and scan existing files in `.claude/learned-facts/` to understand what's already recorded 2. Review this conversation for learnings worth preserving 3. For each new fact: - - Write it concisely (1-2 sentences max) - - Include context for why it matters - - Add today's date + - Create a new file in `.claude/learned-facts/[kebab-case-name].md` using the template below + - Append a new entry to the appropriate category in the **Learned Facts Index** section of `CLAUDE.md` 4. Skip facts that duplicate existing entries -5. Append new facts to `.claude/learned-facts.md` +5. If a new category is needed, add it to the index in `CLAUDE.md` -## Fact Format +## Fact File Template -Use this format for each fact: -``` -- **[Brief Topic]**: [Concise description of the fact] _(YYYY-MM-DD)_ +Create each file at `.claude/learned-facts/[kebab-case-name].md`: + +```markdown +# [Title] + +**Date**: YYYY-MM-DD +**Category**: navigation | tv | native-modules | state-management | ui +**Key files**: `relevant/paths.ts` + +## Detail + +[Full description of the fact, including context for why it matters] ``` -## Example Facts +## Index Entry Format -- **State management**: Use Jotai atoms for global state, NOT React Context - atoms are in `utils/atoms/` _(2025-01-09)_ -- **Package manager**: Always use `bun`, never npm or yarn - the project is configured for bun only _(2025-01-09)_ -- **TV platform**: Check `Platform.isTV` for TV-specific code paths, not just OS checks _(2025-01-09)_ +Append to the appropriate category in the Learned Facts Index section of `CLAUDE.md`: -After updating the file, summarize what facts you added (or note if nothing new was learned this session). +``` +- `kebab-case-name` | Brief one-line summary of the fact +``` + +Categories: Navigation, UI/Headers, State/Data, Native Modules, TV Platform + +## Example + +File `.claude/learned-facts/state-management-pattern.md`: +```markdown +# State Management Pattern + +**Date**: 2025-01-09 +**Category**: state-management +**Key files**: `utils/atoms/` + +## Detail + +Use Jotai atoms for global state, NOT React Context. Atoms are defined in `utils/atoms/`. +``` + +Index entry in `CLAUDE.md`: +``` +State/Data: +- `state-management-pattern` | Use Jotai atoms for global state, not React Context +``` + +After updating, summarize what facts you added (or note if nothing new was learned this session). diff --git a/.claude/learned-facts.md b/.claude/learned-facts.md index e11daa07..ab5d6eec 100644 --- a/.claude/learned-facts.md +++ b/.claude/learned-facts.md @@ -1,8 +1,11 @@ -# Learned Facts +# Learned Facts (DEPRECATED) -This file contains facts about the codebase learned from past sessions. These are things Claude got wrong or needed clarification on, stored here to prevent the same mistakes in future sessions. +> **DEPRECATED**: This file has been replaced by individual fact files in `.claude/learned-facts/`. +> The compressed index is now inline in `CLAUDE.md` under "Learned Facts Index". +> New facts should be added as individual files using the `/reflect` command. +> This file is kept for reference only and is no longer auto-imported. -This file is auto-imported into CLAUDE.md and loaded at the start of each session. +This file previously contained facts about the codebase learned from past sessions. ## Facts diff --git a/.claude/learned-facts/header-button-locations.md b/.claude/learned-facts/header-button-locations.md new file mode 100644 index 00000000..269b51f1 --- /dev/null +++ b/.claude/learned-facts/header-button-locations.md @@ -0,0 +1,9 @@ +# Header Button Locations + +**Date**: 2026-01-10 +**Category**: ui +**Key files**: `app/(auth)/(tabs)/(home)/_layout.tsx`, `components/common/HeaderBackButton.tsx`, `components/Chromecast.tsx`, `components/RoundButton.tsx`, `components/home/Home.tsx`, `app/(auth)/(tabs)/(home)/downloads/index.tsx` + +## Detail + +Header buttons are defined in multiple places: `app/(auth)/(tabs)/(home)/_layout.tsx` (SettingsButton, SessionsButton, back buttons), `components/common/HeaderBackButton.tsx` (reusable), `components/Chromecast.tsx`, `components/RoundButton.tsx`, and dynamically via `navigation.setOptions()` in `components/home/Home.tsx` and `app/(auth)/(tabs)/(home)/downloads/index.tsx`. diff --git a/.claude/learned-facts/intro-modal-trigger-location.md b/.claude/learned-facts/intro-modal-trigger-location.md new file mode 100644 index 00000000..4409db06 --- /dev/null +++ b/.claude/learned-facts/intro-modal-trigger-location.md @@ -0,0 +1,9 @@ +# Intro Modal Trigger Location + +**Date**: 2025-01-09 +**Category**: navigation +**Key files**: `components/home/Home.tsx`, `app/(auth)/(tabs)/_layout.tsx` + +## Detail + +The intro modal trigger logic should be in the `Home.tsx` component, not in the tabs `_layout.tsx`. Triggering modals from tab layout can interfere with native bottom tabs navigation. diff --git a/.claude/learned-facts/introsheet-rendering-location.md b/.claude/learned-facts/introsheet-rendering-location.md new file mode 100644 index 00000000..b9575cd7 --- /dev/null +++ b/.claude/learned-facts/introsheet-rendering-location.md @@ -0,0 +1,9 @@ +# IntroSheet Rendering Location + +**Date**: 2025-01-09 +**Category**: navigation +**Key files**: `providers/IntroSheetProvider`, `components/IntroSheet` + +## Detail + +The `IntroSheet` component is rendered inside `IntroSheetProvider` which wraps the entire navigation stack. Any hooks in IntroSheet that interact with navigation state can affect the native bottom tabs. diff --git a/.claude/learned-facts/macos-header-buttons-fix.md b/.claude/learned-facts/macos-header-buttons-fix.md new file mode 100644 index 00000000..45d5f31a --- /dev/null +++ b/.claude/learned-facts/macos-header-buttons-fix.md @@ -0,0 +1,9 @@ +# macOS Header Buttons Fix + +**Date**: 2026-01-10 +**Category**: ui +**Key files**: `components/common/HeaderBackButton.tsx`, `app/(auth)/(tabs)/(home)/_layout.tsx` + +## Detail + +Header buttons (`headerRight`/`headerLeft`) don't respond to touches on macOS Catalyst builds when using standard React Native `TouchableOpacity`. Fix by using `Pressable` from `react-native-gesture-handler` instead. The library is already installed and `GestureHandlerRootView` wraps the app. diff --git a/.claude/learned-facts/mark-as-played-flow.md b/.claude/learned-facts/mark-as-played-flow.md new file mode 100644 index 00000000..48603cd0 --- /dev/null +++ b/.claude/learned-facts/mark-as-played-flow.md @@ -0,0 +1,9 @@ +# Mark as Played Flow + +**Date**: 2026-01-10 +**Category**: state-management +**Key files**: `components/PlayedStatus.tsx`, `hooks/useMarkAsPlayed.ts` + +## Detail + +The "mark as played" button uses `PlayedStatus` component → `useMarkAsPlayed` hook → `usePlaybackManager.markItemPlayed()`. The hook does optimistic updates via `setQueriesData` before calling the API. Located in `components/PlayedStatus.tsx` and `hooks/useMarkAsPlayed.ts`. diff --git a/.claude/learned-facts/mpv-avfoundation-composite-osd-ordering.md b/.claude/learned-facts/mpv-avfoundation-composite-osd-ordering.md new file mode 100644 index 00000000..418f862a --- /dev/null +++ b/.claude/learned-facts/mpv-avfoundation-composite-osd-ordering.md @@ -0,0 +1,9 @@ +# MPV avfoundation-composite-osd Ordering + +**Date**: 2026-01-22 +**Category**: native-modules +**Key files**: `modules/mpv-player/ios/MPVLayerRenderer.swift` + +## Detail + +On tvOS, the `avfoundation-composite-osd` option MUST be set immediately after `vo=avfoundation`, before any `hwdec` options. Skipping or reordering this causes the app to freeze when exiting the player. Set to "no" on tvOS (prevents gray tint), "yes" on iOS (for PiP subtitle support). diff --git a/.claude/learned-facts/mpv-tvos-player-exit-freeze.md b/.claude/learned-facts/mpv-tvos-player-exit-freeze.md new file mode 100644 index 00000000..7dfb2017 --- /dev/null +++ b/.claude/learned-facts/mpv-tvos-player-exit-freeze.md @@ -0,0 +1,9 @@ +# MPV tvOS Player Exit Freeze + +**Date**: 2026-01-22 +**Category**: native-modules +**Key files**: `modules/mpv-player/ios/MPVLayerRenderer.swift` + +## Detail + +On tvOS, `mpv_terminate_destroy` can deadlock if called while blocking the main thread (e.g., via `queue.sync`). The fix is to run `mpv_terminate_destroy` on `DispatchQueue.global()` asynchronously, allowing it to access main thread for AVFoundation/GPU cleanup. Send `quit` command and drain events first. diff --git a/.claude/learned-facts/native-bottom-tabs-userouter-conflict.md b/.claude/learned-facts/native-bottom-tabs-userouter-conflict.md new file mode 100644 index 00000000..eda49ef0 --- /dev/null +++ b/.claude/learned-facts/native-bottom-tabs-userouter-conflict.md @@ -0,0 +1,9 @@ +# Native Bottom Tabs + useRouter Conflict + +**Date**: 2025-01-09 +**Category**: navigation +**Key files**: `providers/`, `app/_layout.tsx` + +## Detail + +When using `@bottom-tabs/react-navigation` with Expo Router, avoid using the `useRouter()` hook in components rendered at the provider level (outside the tab navigator). The hook subscribes to navigation state changes and can cause unexpected tab switches. Use the static `router` import from `expo-router` instead. diff --git a/.claude/learned-facts/native-swiftui-view-sizing.md b/.claude/learned-facts/native-swiftui-view-sizing.md new file mode 100644 index 00000000..f36a1837 --- /dev/null +++ b/.claude/learned-facts/native-swiftui-view-sizing.md @@ -0,0 +1,9 @@ +# Native SwiftUI View Sizing + +**Date**: 2026-01-25 +**Category**: native-modules +**Key files**: `modules/` + +## Detail + +When creating Expo native modules with SwiftUI views, the view needs explicit dimensions. Use a `width` prop passed from React Native, set an explicit `.frame(width:height:)` in SwiftUI, and override `intrinsicContentSize` in the ExpoView wrapper to report the correct size to React Native's layout system. Using `.aspectRatio(contentMode: .fit)` alone causes inconsistent sizing. diff --git a/.claude/learned-facts/platform-specific-file-suffix-does-not-work.md b/.claude/learned-facts/platform-specific-file-suffix-does-not-work.md new file mode 100644 index 00000000..d52dca9b --- /dev/null +++ b/.claude/learned-facts/platform-specific-file-suffix-does-not-work.md @@ -0,0 +1,9 @@ +# Platform-Specific File Suffix (.tv.tsx) Does NOT Work + +**Date**: 2026-01-26 +**Category**: tv +**Key files**: `app/`, `components/` + +## Detail + +The `.tv.tsx` file suffix does NOT work for either pages or components in this project. Metro bundler doesn't resolve platform-specific suffixes. Instead, use `Platform.isTV` conditional rendering within a single file. For pages: check `Platform.isTV` at the top and return the TV component early. For components: create separate `MyComponent.tsx` and `TVMyComponent.tsx` files and use `Platform.isTV` to choose which to render. diff --git a/.claude/learned-facts/stack-screen-header-configuration.md b/.claude/learned-facts/stack-screen-header-configuration.md new file mode 100644 index 00000000..24ca01fc --- /dev/null +++ b/.claude/learned-facts/stack-screen-header-configuration.md @@ -0,0 +1,9 @@ +# Stack Screen Header Configuration + +**Date**: 2026-01-10 +**Category**: ui +**Key files**: `app/(auth)/(tabs)/(home)/_layout.tsx` + +## Detail + +Sub-pages under `(home)` need explicit `Stack.Screen` entries in `app/(auth)/(tabs)/(home)/_layout.tsx` with `headerTransparent: Platform.OS === "ios"`, `headerBlurEffect: "none"`, and a back button. Without this, pages show with wrong header styling. diff --git a/.claude/learned-facts/streamystats-components-location.md b/.claude/learned-facts/streamystats-components-location.md new file mode 100644 index 00000000..41652a52 --- /dev/null +++ b/.claude/learned-facts/streamystats-components-location.md @@ -0,0 +1,9 @@ +# Streamystats Components Location + +**Date**: 2026-01-25 +**Category**: tv +**Key files**: `components/home/StreamystatsRecommendations.tv.tsx`, `components/home/StreamystatsPromotedWatchlists.tv.tsx`, `app/(auth)/(tabs)/(watchlists)/[watchlistId].tsx` + +## Detail + +Streamystats TV components are at `components/home/StreamystatsRecommendations.tv.tsx` and `components/home/StreamystatsPromotedWatchlists.tv.tsx`. The watchlist detail page (which shows items in a grid) is at `app/(auth)/(tabs)/(watchlists)/[watchlistId].tsx`. diff --git a/.claude/learned-facts/tab-folder-naming.md b/.claude/learned-facts/tab-folder-naming.md new file mode 100644 index 00000000..7663a609 --- /dev/null +++ b/.claude/learned-facts/tab-folder-naming.md @@ -0,0 +1,9 @@ +# Tab Folder Naming + +**Date**: 2025-01-09 +**Category**: navigation +**Key files**: `app/(auth)/(tabs)/` + +## Detail + +The tab folders use underscore prefix naming like `(_home)` instead of just `(home)` based on the project's file structure conventions. diff --git a/.claude/learned-facts/thread-safe-state-for-stop-flags.md b/.claude/learned-facts/thread-safe-state-for-stop-flags.md new file mode 100644 index 00000000..eaa0d84d --- /dev/null +++ b/.claude/learned-facts/thread-safe-state-for-stop-flags.md @@ -0,0 +1,9 @@ +# Thread-Safe State for Stop Flags + +**Date**: 2026-01-22 +**Category**: native-modules +**Key files**: `modules/mpv-player/ios/MPVLayerRenderer.swift` + +## Detail + +When using flags like `isStopping` that control loop termination across threads, the setter must be synchronous (`stateQueue.sync`) not async, otherwise the value may not be visible to other threads in time. diff --git a/.claude/learned-facts/tv-grid-layout-pattern.md b/.claude/learned-facts/tv-grid-layout-pattern.md new file mode 100644 index 00000000..6f9b234a --- /dev/null +++ b/.claude/learned-facts/tv-grid-layout-pattern.md @@ -0,0 +1,9 @@ +# TV Grid Layout Pattern + +**Date**: 2026-01-25 +**Category**: tv +**Key files**: `components/tv/` + +## Detail + +For TV grids, use ScrollView with flexWrap instead of FlatList/FlashList with numColumns. FlatList's numColumns divides width evenly among columns which causes inconsistent item sizing. Use `flexDirection: "row"`, `flexWrap: "wrap"`, `justifyContent: "center"`, and `gap` for spacing. diff --git a/.claude/learned-facts/tv-horizontal-padding-standard.md b/.claude/learned-facts/tv-horizontal-padding-standard.md new file mode 100644 index 00000000..e9ddc0c8 --- /dev/null +++ b/.claude/learned-facts/tv-horizontal-padding-standard.md @@ -0,0 +1,9 @@ +# TV Horizontal Padding Standard + +**Date**: 2026-01-25 +**Category**: tv +**Key files**: `components/tv/`, `app/(auth)/(tabs)/` + +## Detail + +TV pages should use `TV_HORIZONTAL_PADDING = 60` to match other TV pages like Home, Search, etc. The old `TV_SCALE_PADDING = 20` was too small. diff --git a/.claude/learned-facts/tv-modals-must-use-navigation-pattern.md b/.claude/learned-facts/tv-modals-must-use-navigation-pattern.md new file mode 100644 index 00000000..c6c837d5 --- /dev/null +++ b/.claude/learned-facts/tv-modals-must-use-navigation-pattern.md @@ -0,0 +1,9 @@ +# TV Modals Must Use Navigation Pattern + +**Date**: 2026-01-24 +**Category**: tv +**Key files**: `hooks/useTVOptionModal.ts`, `app/(auth)/tv-option-modal.tsx` + +## Detail + +On TV, never use overlay/absolute-positioned modals (like `TVOptionSelector` at the page level). They don't handle the back button correctly. Always use the navigation-based modal pattern: Jotai atom + hook that calls `router.push()` + page in `app/(auth)/`. Use the existing `useTVOptionModal` hook and `tv-option-modal.tsx` page for option selection. `TVOptionSelector` is only appropriate as a sub-selector *within* a navigation-based modal page. diff --git a/.claude/learned-facts/use-network-aware-query-client-limitations.md b/.claude/learned-facts/use-network-aware-query-client-limitations.md new file mode 100644 index 00000000..36e8f2d8 --- /dev/null +++ b/.claude/learned-facts/use-network-aware-query-client-limitations.md @@ -0,0 +1,9 @@ +# useNetworkAwareQueryClient Limitations + +**Date**: 2026-01-10 +**Category**: state-management +**Key files**: `hooks/useNetworkAwareQueryClient.ts` + +## Detail + +The `useNetworkAwareQueryClient` hook uses `Object.create(queryClient)` which breaks QueryClient methods that use JavaScript private fields (like `getQueriesData`, `setQueriesData`, `setQueryData`). Only use it when you ONLY need `invalidateQueries`. For cache manipulation, use standard `useQueryClient` from `@tanstack/react-query`. diff --git a/CLAUDE.md b/CLAUDE.md index 9fd32e75..0c037d42 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,9 +1,39 @@ # CLAUDE.md -@.claude/learned-facts.md - This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## Learned Facts Index + +IMPORTANT: When encountering issues related to these topics, or when implementing new features that touch these areas, prefer retrieval-led reasoning -- read the relevant fact file in `.claude/learned-facts/` before relying on assumptions. + +Navigation: +- `native-bottom-tabs-userouter-conflict` | useRouter() at provider level causes tab switches; use static router import +- `introsheet-rendering-location` | IntroSheet in IntroSheetProvider affects native bottom tabs via nav state hooks +- `intro-modal-trigger-location` | Trigger in Home.tsx, not tabs _layout.tsx +- `tab-folder-naming` | Use underscore prefix: (_home) not (home) + +UI/Headers: +- `macos-header-buttons-fix` | macOS Catalyst: use RNGH Pressable, not RN TouchableOpacity +- `header-button-locations` | Defined in _layout.tsx, HeaderBackButton, Chromecast, RoundButton, etc. +- `stack-screen-header-configuration` | Sub-pages need explicit Stack.Screen with headerTransparent + back button + +State/Data: +- `use-network-aware-query-client-limitations` | Object.create breaks private fields; only for invalidateQueries +- `mark-as-played-flow` | PlayedStatus→useMarkAsPlayed→playbackManager with optimistic updates + +Native Modules: +- `mpv-tvos-player-exit-freeze` | mpv_terminate_destroy deadlocks main thread; use DispatchQueue.global() +- `mpv-avfoundation-composite-osd-ordering` | MUST follow vo=avfoundation, before hwdec options +- `thread-safe-state-for-stop-flags` | Stop flags need synchronous setter (stateQueue.sync not async) +- `native-swiftui-view-sizing` | Need explicit frame + intrinsicContentSize override in ExpoView + +TV Platform: +- `tv-modals-must-use-navigation-pattern` | Use atom+router.push(), never overlay/absolute modals +- `tv-grid-layout-pattern` | ScrollView+flexWrap, not FlatList numColumns +- `tv-horizontal-padding-standard` | TV_HORIZONTAL_PADDING=60, not old TV_SCALE_PADDING=20 +- `streamystats-components-location` | components/home/Streamystats*.tv.tsx, watchlists/[watchlistId].tsx +- `platform-specific-file-suffix-does-not-work` | .tv.tsx doesn't work; use Platform.isTV conditional rendering + ## Project Overview Streamyfin is a cross-platform Jellyfin video streaming client built with Expo (React Native). It supports mobile (iOS/Android) and TV platforms, with features including offline downloads, Chromecast support, and Jellyseerr integration.