├── hooks ├── useColorScheme.ts ├── useTextStyles.ts ├── useColorScheme.web.ts ├── useThemeColor.ts └── useInterval.ts ├── assets ├── bar_icons │ ├── tv.png │ ├── code.png │ ├── home.png │ └── video.png ├── images │ ├── icon.png │ ├── favicon.png │ ├── splash.png │ ├── react-logo.png │ ├── adaptive-icon.png │ ├── react-logo@2x.png │ ├── react-logo@3x.png │ └── partial-react-logo.png ├── audio │ └── paza-moduless.mp3 ├── tv_icons │ ├── icon-1280x768.png │ ├── icon-1920x720.png │ ├── icon-2320x720.png │ ├── icon-400x240.png │ ├── icon-760x760.png │ ├── icon-800x480.png │ ├── icon-3840x1440.png │ └── icon-4640x1440.png └── fonts │ └── SpaceMono-Regular.ttf ├── .eslintrc.js ├── app ├── (tabs) │ ├── _layout.tsx │ ├── video.tsx │ ├── tv_focus.tsx │ ├── index.tsx │ └── explore.tsx ├── +not-found.tsx ├── +html.tsx ├── _layout.tsx └── modal.tsx ├── tsconfig.json ├── .gitignore ├── components ├── ThemedView.tsx ├── navigation │ └── TabBarIcon.tsx ├── DemoButton.tsx ├── HelloWave.tsx ├── ThemedText.tsx ├── Collapsible.tsx ├── ProgressBar.tsx ├── ExternalLink.tsx ├── AudioTest.tsx ├── ParallaxScrollView.tsx ├── VideoTest.tsx └── EventHandlingDemo.tsx ├── constants ├── ReactNativeInfo.ts ├── TextStyles.ts └── Colors.ts ├── layouts ├── TabLayout.tsx └── TabLayout.web.tsx ├── patches └── react-native-screens+4.16.0.patch ├── metro.config.js ├── eas.json ├── app.json ├── package.json ├── scripts └── reset-project.js └── README.md /hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | export { useColorScheme } from 'react-native'; 2 | -------------------------------------------------------------------------------- /assets/bar_icons/tv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/bar_icons/tv.png -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /assets/bar_icons/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/bar_icons/code.png -------------------------------------------------------------------------------- /assets/bar_icons/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/bar_icons/home.png -------------------------------------------------------------------------------- /assets/bar_icons/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/bar_icons/video.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/images/splash.png -------------------------------------------------------------------------------- /assets/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/images/react-logo.png -------------------------------------------------------------------------------- /assets/audio/paza-moduless.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/audio/paza-moduless.mp3 -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/images/react-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/images/react-logo@2x.png -------------------------------------------------------------------------------- /assets/images/react-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/images/react-logo@3x.png -------------------------------------------------------------------------------- /assets/tv_icons/icon-1280x768.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/tv_icons/icon-1280x768.png -------------------------------------------------------------------------------- /assets/tv_icons/icon-1920x720.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/tv_icons/icon-1920x720.png -------------------------------------------------------------------------------- /assets/tv_icons/icon-2320x720.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/tv_icons/icon-2320x720.png -------------------------------------------------------------------------------- /assets/tv_icons/icon-400x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/tv_icons/icon-400x240.png -------------------------------------------------------------------------------- /assets/tv_icons/icon-760x760.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/tv_icons/icon-760x760.png -------------------------------------------------------------------------------- /assets/tv_icons/icon-800x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/tv_icons/icon-800x480.png -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /assets/images/partial-react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/images/partial-react-logo.png -------------------------------------------------------------------------------- /assets/tv_icons/icon-3840x1440.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/tv_icons/icon-3840x1440.png -------------------------------------------------------------------------------- /assets/tv_icons/icon-4640x1440.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-tvos/ExpoRouterTV/HEAD/assets/tv_icons/icon-4640x1440.png -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://docs.expo.dev/guides/using-eslint/ 2 | module.exports = { 3 | extends: 'expo', 4 | ignorePatterns: ['/dist/*'], 5 | }; 6 | -------------------------------------------------------------------------------- /app/(tabs)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import TabLayout from '../../layouts/TabLayout'; 2 | 3 | export default function PlatformTabLayout() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /hooks/useTextStyles.ts: -------------------------------------------------------------------------------- 1 | import { textStyles } from '@/constants/TextStyles'; 2 | import { useThemeColor } from './useThemeColor'; 3 | 4 | export function useTextStyles() { 5 | const linkColor = useThemeColor({}, 'link'); 6 | return textStyles(linkColor); 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": [ 7 | "./*" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "**/*.tsx", 14 | ".expo/types/**/*.ts", 15 | "expo-env.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | ios/ 5 | android/ 6 | npm-debug.* 7 | *.jks 8 | *.p8 9 | *.p12 10 | *.key 11 | *.mobileprovision 12 | *.orig.* 13 | web-build/ 14 | 15 | # macOS 16 | .DS_Store 17 | 18 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 19 | # The following patterns were generated by expo-cli 20 | 21 | expo-env.d.ts 22 | # @end expo-cli -------------------------------------------------------------------------------- /hooks/useColorScheme.web.ts: -------------------------------------------------------------------------------- 1 | // NOTE: The default React Native styling doesn't support server rendering. 2 | // Server rendered styles should not change between the first render of the HTML 3 | // and the first render on the client. Typically, web developers will use CSS media queries 4 | // to render different styles on the client and server, these aren't directly supported in React Native 5 | // but can be achieved using a styling library like Nativewind. 6 | export function useColorScheme() { 7 | return 'light'; 8 | } 9 | -------------------------------------------------------------------------------- /components/ThemedView.tsx: -------------------------------------------------------------------------------- 1 | import { View, type ViewProps } from 'react-native'; 2 | 3 | import { useThemeColor } from '@/hooks/useThemeColor'; 4 | 5 | export type ThemedViewProps = ViewProps & { 6 | lightColor?: string; 7 | darkColor?: string; 8 | }; 9 | 10 | export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) { 11 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); 12 | 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /hooks/useThemeColor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about light and dark modes: 3 | * https://docs.expo.dev/guides/color-schemes/ 4 | */ 5 | 6 | import { useColorScheme } from 'react-native'; 7 | 8 | import { Colors } from '@/constants/Colors'; 9 | 10 | export function useThemeColor( 11 | props: { light?: string; dark?: string }, 12 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark 13 | ) { 14 | const theme = useColorScheme() ?? 'light'; 15 | const colorFromProps = props[theme]; 16 | 17 | if (colorFromProps) { 18 | return colorFromProps; 19 | } else { 20 | return Colors[theme][colorName]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /constants/ReactNativeInfo.ts: -------------------------------------------------------------------------------- 1 | import { version as rnVersion } from 'react-native/package.json'; 2 | import { version as routerVersion } from 'expo-router/package.json'; 3 | import { Platform } from 'react-native'; 4 | 5 | const hermesVersion = (global as any)?.HermesInternal?.getRuntimeProperties(); 6 | const jsEngine = 7 | Platform.OS === 'web' ? 'Browser' : hermesVersion ? `Hermes` : 'JSC'; 8 | 9 | const uiManager = 10 | ((global as any)?.nativeFabricUIManager as any) !== undefined 11 | ? 'Fabric' 12 | : 'Paper'; 13 | 14 | export const reactNativeInfo = { 15 | rnVersion, 16 | routerVersion, 17 | hermesVersion, 18 | uiManager, 19 | jsEngine, 20 | }; 21 | -------------------------------------------------------------------------------- /components/navigation/TabBarIcon.tsx: -------------------------------------------------------------------------------- 1 | // You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ 2 | 3 | import Ionicons from '@expo/vector-icons/Ionicons'; 4 | import { type IconProps } from '@expo/vector-icons/build/createIconSet'; 5 | import { type ComponentProps } from 'react'; 6 | import { scale } from 'react-native-size-matters'; 7 | 8 | export function TabBarIcon({ 9 | style, 10 | ...rest 11 | }: IconProps['name']>) { 12 | return ( 13 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /hooks/useInterval.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react" 2 | 3 | /** 4 | * Hook that calls a callback periodically 5 | * Based on https://balavishnuvj.com/blog/using-callbacks-in-custom-hooks/ 6 | * 7 | * @param callback The callback to be called 8 | * @param interval The interval between callbacks, in milliseconds 9 | */ 10 | export const useInterval: (callback: () => void, interval: number) => void = ( 11 | callback, 12 | interval, 13 | ) => { 14 | const callbackRef = useRef() 15 | useEffect(() => { 16 | callbackRef.current = callback 17 | }, [callback]) 18 | 19 | // Set up the interval. 20 | useEffect(() => { 21 | function cb() { 22 | callbackRef.current && callbackRef.current() 23 | } 24 | const id = setInterval(cb, interval) 25 | return () => clearInterval(id) 26 | }, [interval]) 27 | } 28 | -------------------------------------------------------------------------------- /app/+not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Link, Stack } from 'expo-router'; 2 | import { StyleSheet } from 'react-native'; 3 | 4 | import { ThemedText } from '@/components/ThemedText'; 5 | import { ThemedView } from '@/components/ThemedView'; 6 | 7 | export default function NotFoundScreen() { 8 | return ( 9 | <> 10 | 11 | 12 | This screen doesn't exist. 13 | 14 | Go to home screen! 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | const styles = StyleSheet.create({ 22 | container: { 23 | flex: 1, 24 | alignItems: 'center', 25 | justifyContent: 'center', 26 | padding: 20, 27 | }, 28 | link: { 29 | marginTop: 15, 30 | paddingVertical: 15, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /components/DemoButton.tsx: -------------------------------------------------------------------------------- 1 | import { scale } from 'react-native-size-matters'; 2 | import { Pressable, StyleSheet, Text } from 'react-native'; 3 | 4 | export const DemoButton = (props: { title: string; onPress: () => void }) => { 5 | const styles = useVideoStyles(); 6 | return ( 7 | props.onPress()} 9 | style={({ pressed, focused }) => [ 10 | styles.button, 11 | pressed || focused ? { backgroundColor: 'blue' } : {}, 12 | ]} 13 | > 14 | {props.title} 15 | 16 | ); 17 | }; 18 | 19 | export const useVideoStyles = () => { 20 | return StyleSheet.create({ 21 | button: { 22 | backgroundColor: 'darkblue', 23 | margin: scale(5), 24 | borderRadius: scale(2), 25 | padding: scale(5), 26 | }, 27 | buttonText: { 28 | color: 'white', 29 | fontSize: scale(8), 30 | }, 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /layouts/TabLayout.tsx: -------------------------------------------------------------------------------- 1 | import { NativeTabs, Label, Icon } from 'expo-router/unstable-native-tabs'; 2 | import { Platform } from 'react-native'; 3 | 4 | import WebTabLayout from './TabLayout.web'; 5 | 6 | export default function TabLayout() { 7 | if (Platform.OS === 'android' && Platform.isTV) { 8 | return ; 9 | } 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /patches/react-native-screens+4.16.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-native-screens/ios/bottom-tabs/RNSTabBarController.mm b/node_modules/react-native-screens/ios/bottom-tabs/RNSTabBarController.mm 2 | index 1c70eec..375cba4 100644 3 | --- a/node_modules/react-native-screens/ios/bottom-tabs/RNSTabBarController.mm 4 | +++ b/node_modules/react-native-screens/ios/bottom-tabs/RNSTabBarController.mm 5 | @@ -19,6 +19,15 @@ 6 | _tabBarAppearanceCoordinator = [RNSTabBarAppearanceCoordinator new]; 7 | _tabsHostComponentView = nil; 8 | 9 | +#if !TARGET_OS_TV 10 | + if (@available(iOS 18.0, *)) { 11 | + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { 12 | + [self setMode:UITabBarControllerModeTabSidebar]; // Enable the sidebar 13 | + self.sidebar.hidden = YES; // Hide it by default 14 | + } 15 | + } 16 | +#endif 17 | + 18 | #if !RCT_NEW_ARCH_ENABLED 19 | _isControllerFlushBlockScheduled = NO; 20 | #endif // !RCT_NEW_ARCH_ENABLED 21 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more https://docs.expo.io/guides/customizing-metro 2 | const { getDefaultConfig } = require('expo/metro-config'); 3 | 4 | /** @type {import('expo/metro-config').MetroConfig} */ 5 | const config = getDefaultConfig(__dirname); // eslint-disable-line no-undef 6 | 7 | // Add Hermes parser 8 | config.transformer.hermesParser = true; 9 | 10 | // When enabled, the optional code below will allow Metro to resolve 11 | // and bundle source files with TV-specific extensions 12 | // (e.g., *.ios.tv.tsx, *.android.tv.tsx, *.tv.tsx) 13 | // 14 | // Metro will still resolve source files with standard extensions 15 | // as usual if TV-specific files are not found for a module. 16 | // 17 | if (process.env?.EXPO_TV === '1') { 18 | const originalSourceExts = config.resolver.sourceExts; 19 | const tvSourceExts = [ 20 | ...originalSourceExts.map((e) => `tv.${e}`), 21 | ...originalSourceExts, 22 | ]; 23 | config.resolver.sourceExts = tvSourceExts; 24 | } 25 | 26 | module.exports = config; 27 | -------------------------------------------------------------------------------- /components/HelloWave.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import Animated, { 3 | useSharedValue, 4 | useAnimatedStyle, 5 | withTiming, 6 | withRepeat, 7 | withSequence, 8 | } from 'react-native-reanimated'; 9 | 10 | import { ThemedText } from '@/components/ThemedText'; 11 | 12 | export function HelloWave() { 13 | const rotationAnimation = useSharedValue(0); 14 | 15 | rotationAnimation.value = withRepeat( 16 | withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })), 17 | 4 // Run the animation 4 times 18 | ); 19 | 20 | const animatedStyle = useAnimatedStyle(() => ({ 21 | transform: [{ rotate: `${rotationAnimation.value}deg` }], 22 | })); 23 | 24 | return ( 25 | 26 | 👋 27 | 28 | ); 29 | } 30 | 31 | const styles = StyleSheet.create({ 32 | text: { 33 | fontSize: 28, 34 | lineHeight: 32, 35 | marginTop: -6, 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /constants/TextStyles.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Below are text styles used in the app, primarily in the ThemedText component. 3 | */ 4 | 5 | import { TextStyle } from 'react-native'; 6 | import { scale } from 'react-native-size-matters'; 7 | 8 | export const textStyles = function (linkColor: string): { 9 | [key: string]: TextStyle & { fontSize: number; lineHeight: number }; 10 | } { 11 | return { 12 | default: { 13 | fontSize: scale(10), 14 | lineHeight: scale(12), 15 | }, 16 | defaultSemiBold: { 17 | fontSize: scale(10), 18 | lineHeight: scale(12), 19 | fontWeight: '600', 20 | }, 21 | title: { 22 | fontSize: scale(16), 23 | fontWeight: 'bold', 24 | lineHeight: scale(20), 25 | }, 26 | subtitle: { 27 | fontSize: scale(12), 28 | lineHeight: scale(15), 29 | fontWeight: 'bold', 30 | }, 31 | link: { 32 | lineHeight: scale(8), 33 | fontSize: scale(10), 34 | color: linkColor, 35 | }, 36 | small: { 37 | lineHeight: scale(5), 38 | fontSize: scale(4), 39 | }, 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /constants/Colors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Below are the colors that are used in the app. The colors are defined in the light and dark mode. 3 | * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. 4 | */ 5 | 6 | const tintColorLight = '#0a7ea4'; 7 | const tintColorDark = '#aaa'; 8 | 9 | const containerBackgroundLight = '#D0D0D0'; 10 | const containerBackgroundDark = '#353636'; 11 | 12 | export const Colors = { 13 | light: { 14 | text: '#11181C', 15 | background: '#fff', 16 | tint: tintColorLight, 17 | icon: '#687076', 18 | tabIconDefault: '#687076', 19 | tabIconSelected: tintColorLight, 20 | link: '#0a7ea4', 21 | containerBackground: containerBackgroundLight, 22 | }, 23 | dark: { 24 | text: '#ECEDEE', 25 | background: '#151718', 26 | tint: tintColorDark, 27 | icon: '#9BA1A6', 28 | tabIconDefault: '#9BA1A6', 29 | tabIconSelected: tintColorDark, 30 | link: '#0a7ea4', 31 | containerBackground: containerBackgroundDark, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /components/ThemedText.tsx: -------------------------------------------------------------------------------- 1 | import { Text, type TextProps } from 'react-native'; 2 | 3 | import { useThemeColor } from '@/hooks/useThemeColor'; 4 | import { useTextStyles } from '@/hooks/useTextStyles'; 5 | 6 | export type ThemedTextProps = TextProps & { 7 | lightColor?: string; 8 | darkColor?: string; 9 | type?: 10 | | 'default' 11 | | 'title' 12 | | 'defaultSemiBold' 13 | | 'subtitle' 14 | | 'link' 15 | | 'small'; 16 | }; 17 | 18 | export function ThemedText({ 19 | style, 20 | lightColor, 21 | darkColor, 22 | type = 'default', 23 | ...rest 24 | }: ThemedTextProps) { 25 | const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); 26 | const styles = useTextStyles(); 27 | 28 | return ( 29 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 3.15.1", 4 | "appVersionSource": "remote" 5 | }, 6 | "build": { 7 | "development": { 8 | "extends": "production", 9 | "distribution": "internal", 10 | "android": { 11 | "buildType": "apk", 12 | "withoutCredentials": true, 13 | "gradleCommand": ":app:assembleDebug" 14 | }, 15 | "ios": { 16 | "buildConfiguration": "Debug", 17 | "simulator": true 18 | } 19 | }, 20 | "development:tv": { 21 | "extends": "development", 22 | "env": { 23 | "EXPO_TV": "1" 24 | } 25 | }, 26 | "preview": { 27 | "extends": "production", 28 | "autoIncrement": true, 29 | "distribution": "internal", 30 | "ios": { 31 | "simulator": true 32 | }, 33 | "android": { 34 | "buildType": "apk", 35 | "withoutCredentials": true 36 | } 37 | }, 38 | "preview:tv": { 39 | "autoIncrement": true, 40 | "extends": "preview", 41 | "env": { 42 | "EXPO_TV": "1" 43 | } 44 | }, 45 | "production": {}, 46 | "production:tv": { 47 | "extends": "production", 48 | "env": { 49 | "EXPO_TV": "1" 50 | } 51 | } 52 | }, 53 | "submit": { 54 | "production": {} 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/(tabs)/video.tsx: -------------------------------------------------------------------------------- 1 | import Ionicons from '@expo/vector-icons/Ionicons'; 2 | import { StyleSheet } from 'react-native'; 3 | import { scale } from 'react-native-size-matters'; 4 | 5 | import ParallaxScrollView from '@/components/ParallaxScrollView'; 6 | import { ThemedText } from '@/components/ThemedText'; 7 | import { ThemedView } from '@/components/ThemedView'; 8 | import VideoTest from '@/components/VideoTest'; 9 | import AudioTest from '@/components/AudioTest'; 10 | 11 | export default function VideoDemoScreen() { 12 | return ( 13 | 21 | } 22 | > 23 | 24 | Audio demo 25 | 26 | 27 | 28 | Video demo 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | const styles = StyleSheet.create({ 36 | headerImage: { 37 | color: '#808080', 38 | bottom: scale(30), 39 | left: 0, 40 | position: 'absolute', 41 | }, 42 | titleContainer: { 43 | flexDirection: 'row', 44 | gap: scale(8), 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /components/Collapsible.tsx: -------------------------------------------------------------------------------- 1 | import Ionicons from '@expo/vector-icons/Ionicons'; 2 | import { PropsWithChildren, useState } from 'react'; 3 | import { StyleSheet, TouchableOpacity, useColorScheme } from 'react-native'; 4 | import { scale } from 'react-native-size-matters'; 5 | 6 | import { ThemedText } from '@/components/ThemedText'; 7 | import { ThemedView } from '@/components/ThemedView'; 8 | import { Colors } from '@/constants/Colors'; 9 | 10 | export function Collapsible({ 11 | children, 12 | title, 13 | }: PropsWithChildren & { title: string }) { 14 | const [isOpen, setIsOpen] = useState(false); 15 | const theme = useColorScheme() ?? 'light'; 16 | 17 | return ( 18 | 19 | setIsOpen((value) => !value)} 22 | activeOpacity={0.6} 23 | > 24 | 29 | {title} 30 | 31 | {isOpen && {children}} 32 | 33 | ); 34 | } 35 | 36 | const styles = StyleSheet.create({ 37 | heading: { 38 | flexDirection: 'row', 39 | alignItems: 'center', 40 | gap: scale(6), 41 | }, 42 | content: { 43 | marginTop: scale(6), 44 | marginLeft: scale(24), 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /components/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import { scale } from 'react-native-size-matters'; 2 | import { StyleSheet, View } from 'react-native'; 3 | 4 | export const ProgressBar = (props: any) => { 5 | const styles = useProgressBarStyles(); 6 | const progressBarStyles = { 7 | container: styles.progressContainer, 8 | left: [styles.progressLeft, { flex: props?.fractionComplete || 0.0 }], 9 | right: [ 10 | styles.progressRight, 11 | { flex: 1.0 - props?.fractionComplete || 1.0 }, 12 | ], 13 | }; 14 | return ( 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export const fractionCompleteFromPosition = ( 23 | position: number | undefined, 24 | duration: number | undefined, 25 | ) => { 26 | return duration !== undefined ? (position ?? 0) / duration : 0; 27 | }; 28 | 29 | const useProgressBarStyles = () => { 30 | const vidHeight = scale(200); 31 | const vidWidth = 2 * vidHeight; 32 | return StyleSheet.create({ 33 | progressContainer: { 34 | flexDirection: 'row', 35 | width: vidWidth, 36 | height: scale(5), 37 | margin: 0, 38 | }, 39 | progressLeft: { 40 | backgroundColor: 'blue', 41 | borderTopRightRadius: scale(5), 42 | borderBottomRightRadius: scale(5), 43 | flexDirection: 'row', 44 | height: '100%', 45 | }, 46 | progressRight: { 47 | flexDirection: 'row', 48 | height: '100%', 49 | }, 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /app/+html.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollViewStyleReset } from 'expo-router/html'; 2 | import { type PropsWithChildren } from 'react'; 3 | 4 | /** 5 | * This file is web-only and used to configure the root HTML for every web page during static rendering. 6 | * The contents of this function only run in Node.js environments and do not have access to the DOM or browser APIs. 7 | */ 8 | export default function Root({ children }: PropsWithChildren) { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | {/* 17 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. 18 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. 19 | */} 20 | 21 | 22 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} 23 |