7.6 KiB
CLAUDE.md
@.claude/learned-facts.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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.
Development Commands
CRITICAL: Always use bun for package management. Never use npm, yarn, or npx.
# Setup
bun i && bun run submodule-reload
# Development builds
bun run prebuild # Mobile prebuild
bun run ios # Run iOS
bun run android # Run Android
# TV builds (suffix with :tv)
bun run prebuild:tv
bun run ios:tv
bun run android:tv
# Code quality
bun run typecheck # TypeScript check
bun run check # BiomeJS check
bun run lint # BiomeJS lint + fix
bun run format # BiomeJS format
bun run test # Run all checks (typecheck, lint, format, doctor)
# iOS-specific
bun run ios:install-metal-toolchain # Fix "missing Metal Toolchain" build errors
Tech Stack
- Runtime: Bun
- Framework: React Native (Expo SDK 54)
- Language: TypeScript (strict mode)
- State Management: Jotai (global state atoms) + React Query (server state)
- API: Jellyfin SDK (
@jellyfin/sdk) - Navigation: Expo Router (file-based)
- Linting/Formatting: BiomeJS
- Storage: react-native-mmkv
Architecture
File Structure
app/- Expo Router screens with file-based routingcomponents/- Reusable UI componentsproviders/- React Context providershooks/- Custom React hooksutils/- Utilities including Jotai atomsmodules/- Native modules (vlc-player, mpv-player, background-downloader)translations/- i18n translation files
Key Patterns
State Management:
- Global state uses Jotai atoms in
utils/atoms/ settingsAtominutils/atoms/settings.tsfor app settingsapiAtomanduserAtominproviders/JellyfinProvider.tsxfor auth state- Server state uses React Query with
@tanstack/react-query
Jellyfin API Access:
- Use
apiAtomfromJellyfinProviderfor authenticated API calls - Access user via
userAtom - Use Jellyfin SDK utilities from
@jellyfin/sdk/lib/utils/api
Navigation:
- File-based routing in
app/directory - Tab navigation:
(home),(search),(favorites),(libraries),(watchlists) - Shared routes use parenthesized groups like
(home,libraries,search,favorites,watchlists) - IMPORTANT: Always use
useAppRouterfrom@/hooks/useAppRouterinstead ofuseRouterfromexpo-router. This custom hook automatically handles offline mode state preservation across navigation:// ✅ Correct import useRouter from "@/hooks/useAppRouter"; const router = useRouter(); // ❌ Never use this import { useRouter } from "expo-router"; import { router } from "expo-router";
Offline Mode:
- Use
OfflineModeProviderfrom@/providers/OfflineModeProviderto wrap pages that support offline content - Use
useOfflineMode()hook to check if current context is offline - The
useAppRouterhook automatically injectsoffline=trueparam when navigating within an offline context
Providers (wrapping order in app/_layout.tsx):
- JotaiProvider
- QueryClientProvider
- JellyfinProvider (auth, API)
- NetworkStatusProvider
- PlaySettingsProvider
- WebSocketProvider
- DownloadProvider
- MusicPlayerProvider
Native Modules
Located in modules/:
vlc-player- VLC video player integrationmpv-player- MPV video player integration (iOS)background-downloader- Background download functionalitysf-player- Swift player module
Path Aliases
Use @/ prefix for imports (configured in tsconfig.json):
import { useSettings } from "@/utils/atoms/settings";
import { apiAtom } from "@/providers/JellyfinProvider";
Coding Standards
- Use TypeScript for all files (no .js)
- Use functional React components with hooks
- Use Jotai atoms for global state, React Query for server state
- Follow BiomeJS formatting rules (2-space indent, semicolons, LF line endings)
- Handle both mobile and TV navigation patterns
- Use existing atoms, hooks, and utilities before creating new ones
- Use Conventional Commits:
feat(scope):,fix(scope):,chore(scope):
Platform Considerations
- TV version uses
:tvsuffix for scripts - Platform checks:
Platform.isTV,Platform.OS === "android"or"ios" - Some features disabled on TV (e.g., notifications, Chromecast)
TV Component Rendering Pattern
IMPORTANT: The .tv.tsx file suffix only works for pages in the app/ directory (resolved by Expo Router). It does NOT work for components - Metro bundler doesn't resolve platform-specific suffixes for component imports.
Pattern for TV-specific components:
// In page file (e.g., app/login.tsx)
import { Platform } from "react-native";
import { Login } from "@/components/login/Login";
import { TVLogin } from "@/components/login/TVLogin";
const LoginPage: React.FC = () => {
if (Platform.isTV) {
return <TVLogin />;
}
return <Login />;
};
export default LoginPage;
- Create separate component files for mobile and TV (e.g.,
MyComponent.tsxandTVMyComponent.tsx) - Use
Platform.isTVto conditionally render the appropriate component - TV components typically use
TVInput,TVServerCard, and other TV-prefixed components with focus handling
TV Option Selector Pattern (Dropdowns/Multi-select)
For dropdown/select components on TV, use a bottom sheet with horizontal scrolling. This pattern is ideal for TV because:
- Horizontal scrolling is natural for TV remotes (left/right D-pad)
- Bottom sheet takes minimal screen space
- Focus-based navigation works reliably
Key implementation details:
- Use absolute positioning instead of Modal - React Native's
Modalbreaks the TV focus chain. Use an absolutely positionedViewoverlay instead:
<View style={{
position: "absolute",
top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.5)",
justifyContent: "flex-end",
zIndex: 1000,
}}>
<BlurView intensity={80} tint="dark" style={{ borderTopLeftRadius: 24, borderTopRightRadius: 24 }}>
{/* Content */}
</BlurView>
</View>
- Horizontal ScrollView with focusable cards:
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={{ overflow: "visible" }}
contentContainerStyle={{ paddingHorizontal: 48, paddingVertical: 10, gap: 12 }}
>
{options.map((option, index) => (
<TVOptionCard
key={index}
hasTVPreferredFocus={index === selectedIndex}
onPress={() => { onSelect(option.value); onClose(); }}
// ...
/>
))}
</ScrollView>
- Focus handling on cards - Use
PressablewithonFocus/onBlurandhasTVPreferredFocus:
<Pressable
onPress={onPress}
onFocus={() => { setFocused(true); animateTo(1.05); }}
onBlur={() => { setFocused(false); animateTo(1); }}
hasTVPreferredFocus={hasTVPreferredFocus}
>
<Animated.View style={{ transform: [{ scale }], backgroundColor: focused ? "#fff" : "rgba(255,255,255,0.08)" }}>
<Text style={{ color: focused ? "#000" : "#fff" }}>{label}</Text>
</Animated.View>
</Pressable>
- Add padding for scale animations - When items scale on focus, add enough padding (
overflow: "visible"+paddingVertical) so scaled items don't clip.
Reference implementation: See TVOptionSelector and TVOptionCard in components/ItemContent.tv.tsx