mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-14 21:12:01 +01:00
223 lines
7.6 KiB
Markdown
223 lines
7.6 KiB
Markdown
# 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`.**
|
|
|
|
```bash
|
|
# 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 routing
|
|
- `components/` - Reusable UI components
|
|
- `providers/` - React Context providers
|
|
- `hooks/` - Custom React hooks
|
|
- `utils/` - Utilities including Jotai atoms
|
|
- `modules/` - Native modules (vlc-player, mpv-player, background-downloader)
|
|
- `translations/` - i18n translation files
|
|
|
|
### Key Patterns
|
|
|
|
**State Management**:
|
|
- Global state uses Jotai atoms in `utils/atoms/`
|
|
- `settingsAtom` in `utils/atoms/settings.ts` for app settings
|
|
- `apiAtom` and `userAtom` in `providers/JellyfinProvider.tsx` for auth state
|
|
- Server state uses React Query with `@tanstack/react-query`
|
|
|
|
**Jellyfin API Access**:
|
|
- Use `apiAtom` from `JellyfinProvider` for 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 `useAppRouter` from `@/hooks/useAppRouter` instead of `useRouter` from `expo-router`. This custom hook automatically handles offline mode state preservation across navigation:
|
|
```typescript
|
|
// ✅ 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 `OfflineModeProvider` from `@/providers/OfflineModeProvider` to wrap pages that support offline content
|
|
- Use `useOfflineMode()` hook to check if current context is offline
|
|
- The `useAppRouter` hook automatically injects `offline=true` param when navigating within an offline context
|
|
|
|
**Providers** (wrapping order in `app/_layout.tsx`):
|
|
1. JotaiProvider
|
|
2. QueryClientProvider
|
|
3. JellyfinProvider (auth, API)
|
|
4. NetworkStatusProvider
|
|
5. PlaySettingsProvider
|
|
6. WebSocketProvider
|
|
7. DownloadProvider
|
|
8. MusicPlayerProvider
|
|
|
|
### Native Modules
|
|
|
|
Located in `modules/`:
|
|
- `vlc-player` - VLC video player integration
|
|
- `mpv-player` - MPV video player integration (iOS)
|
|
- `background-downloader` - Background download functionality
|
|
- `sf-player` - Swift player module
|
|
|
|
### Path Aliases
|
|
|
|
Use `@/` prefix for imports (configured in `tsconfig.json`):
|
|
```typescript
|
|
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 `:tv` suffix 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**:
|
|
```typescript
|
|
// 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.tsx` and `TVMyComponent.tsx`)
|
|
- Use `Platform.isTV` to 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:**
|
|
|
|
1. **Use absolute positioning instead of Modal** - React Native's `Modal` breaks the TV focus chain. Use an absolutely positioned `View` overlay instead:
|
|
```typescript
|
|
<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>
|
|
```
|
|
|
|
2. **Horizontal ScrollView with focusable cards**:
|
|
```typescript
|
|
<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>
|
|
```
|
|
|
|
3. **Focus handling on cards** - Use `Pressable` with `onFocus`/`onBlur` and `hasTVPreferredFocus`:
|
|
```typescript
|
|
<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>
|
|
```
|
|
|
|
4. **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`
|