Files
streamyfin/CLAUDE.md
2026-01-16 13:00:26 +01:00

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 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:
    // ✅ 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):

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:

// 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:
<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>
  1. 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>
  1. Focus handling on cards - Use Pressable with onFocus/onBlur and hasTVPreferredFocus:
<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>
  1. 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