diff --git a/app/_layout.tsx b/app/_layout.tsx index b89e078..db1ae7d 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -9,6 +9,8 @@ import { useSettingsStore } from '../store/settingsStore'; import { MiniPlayer } from '../components/MiniPlayer'; import * as Sentry from '@sentry/react-native'; import { ErrorBoundary } from '@sentry/react-native'; +import { useFonts } from 'expo-font'; +import { Ionicons } from '@expo/vector-icons'; Sentry.init({ dsn: process.env.EXPO_PUBLIC_SENTRY_DSN ?? '', @@ -22,13 +24,18 @@ function RootLayout() { const loadDownloads = useDownloadStore(s => s.loadFromStorage); const restoreSettings = useSettingsStore(s => s.restore); + const [fontsLoaded] = useFonts({ + ...Ionicons.font, + }); + useEffect(() => { restore(); loadDownloads(); restoreSettings(); - }, []); + if (!fontsLoaded) return null; + return ( diff --git a/metro.config.js b/metro.config.js index 9d82ec1..8fac6c6 100644 --- a/metro.config.js +++ b/metro.config.js @@ -1,16 +1,29 @@ const path = require('path'); -const { - getSentryExpoConfig -} = require("@sentry/react-native/metro"); +const { getSentryExpoConfig } = require("@sentry/react-native/metro"); const config = getSentryExpoConfig(__dirname); +// Ensure shims directory is watched by Metro +config.watchFolders = [...(config.watchFolders ?? []), path.resolve(__dirname, 'shims')]; + const originalResolveRequest = config.resolver.resolveRequest; +const WEB_SHIMS = { + 'react-native-pager-view': 'shims/react-native-pager-view.web.tsx', + '@sentry/react-native': 'shims/sentry-react-native.web.tsx', + '@dr.pogodin/react-native-static-server': 'shims/react-native-static-server.web.ts', + 'expo-network': 'shims/expo-network.web.ts', + 'expo-intent-launcher': 'shims/expo-intent-launcher.web.ts', + 'react-native-video': 'shims/react-native-video.web.tsx', + 'expo-file-system': 'shims/expo-file-system.web.ts', + 'expo-file-system/legacy': 'shims/expo-file-system.web.ts', + 'expo-clipboard': 'shims/expo-clipboard.web.ts', +}; + config.resolver.resolveRequest = (context, moduleName, platform) => { - if (platform === 'web' && moduleName === 'react-native-pager-view') { + if (platform === 'web' && WEB_SHIMS[moduleName]) { return { - filePath: path.resolve(__dirname, 'shims/react-native-pager-view.web.tsx'), + filePath: path.resolve(__dirname, WEB_SHIMS[moduleName]), type: 'sourceFile', }; } @@ -20,4 +33,4 @@ config.resolver.resolveRequest = (context, moduleName, platform) => { return context.resolveRequest(context, moduleName, platform); }; -module.exports = config; \ No newline at end of file +module.exports = config; diff --git a/shims/expo-clipboard.web.ts b/shims/expo-clipboard.web.ts new file mode 100644 index 0000000..8472b00 --- /dev/null +++ b/shims/expo-clipboard.web.ts @@ -0,0 +1,7 @@ +/** Web shim for expo-clipboard - use native browser clipboard API */ +export async function setStringAsync(text: string): Promise { + try { await navigator.clipboard.writeText(text); } catch {} +} +export async function getStringAsync(): Promise { + try { return await navigator.clipboard.readText(); } catch { return ''; } +} diff --git a/shims/expo-file-system.web.ts b/shims/expo-file-system.web.ts new file mode 100644 index 0000000..3812f98 --- /dev/null +++ b/shims/expo-file-system.web.ts @@ -0,0 +1,14 @@ +/** Web shim for expo-file-system */ +export const documentDirectory = ''; +export const cacheDirectory = ''; +export async function getInfoAsync(_uri: string) { return { exists: false, isDirectory: false }; } +export async function readAsStringAsync(_uri: string) { return ''; } +export async function writeAsStringAsync(_uri: string, _contents: string) {} +export async function deleteAsync(_uri: string) {} +export async function moveAsync(_opts: any) {} +export async function copyAsync(_opts: any) {} +export async function makeDirectoryAsync(_uri: string) {} +export async function getContentUriAsync(_uri: string) { return ''; } +export function createDownloadResumable(_url: string, _fileUri: string, _opts?: any, _cb?: any) { + return { downloadAsync: async () => ({}) }; +} diff --git a/shims/expo-intent-launcher.web.ts b/shims/expo-intent-launcher.web.ts new file mode 100644 index 0000000..b2e98b6 --- /dev/null +++ b/shims/expo-intent-launcher.web.ts @@ -0,0 +1,5 @@ +/** Web shim for expo-intent-launcher - no-op for web */ +export async function startActivityAsync(_activity: string, _params?: unknown): Promise { + return {}; +} +export const ActivityAction = {}; diff --git a/shims/expo-network.web.ts b/shims/expo-network.web.ts new file mode 100644 index 0000000..0986cfc --- /dev/null +++ b/shims/expo-network.web.ts @@ -0,0 +1,10 @@ +/** Web shim for expo-network */ +export enum NetworkStateType { + NONE = 0, UNKNOWN = 1, CELLULAR = 2, WIFI = 3, BLUETOOTH = 4, + ETHERNET = 5, WIMAX = 6, VPN = 7, OTHER = 8, +} +export async function getNetworkStateAsync() { + return { isConnected: navigator.onLine, isInternetReachable: navigator.onLine, type: NetworkStateType.UNKNOWN }; +} +export async function getIpAddressAsync(): Promise { return '0.0.0.0'; } +export async function isAirplaneModeEnabledAsync(): Promise { return false; } diff --git a/shims/react-native-pager-view.web.tsx b/shims/react-native-pager-view.web.tsx index 22e990d..2d7cc88 100644 --- a/shims/react-native-pager-view.web.tsx +++ b/shims/react-native-pager-view.web.tsx @@ -1,7 +1,6 @@ /** * Web shim for react-native-pager-view. - * eas update exports for web; this replaces the native-only module - * with a simple View-based container that renders the first child only. + * Supports setPage/setPageWithoutAnimation via imperative handle. */ import React from 'react'; import { View, type ViewStyle } from 'react-native'; @@ -11,16 +10,35 @@ interface PagerViewProps { style?: ViewStyle; initialPage?: number; scrollEnabled?: boolean; - onPageSelected?: (e: any) => void; + onPageSelected?: (e: { nativeEvent: { position: number } }) => void; + onPageScrollStateChanged?: (e: any) => void; [key: string]: any; } -const PagerView = React.forwardRef( - ({ children, style, initialPage = 0 }, ref) => { +export interface PagerViewHandle { + setPage: (page: number) => void; + setPageWithoutAnimation: (page: number) => void; +} + +const PagerView = React.forwardRef( + ({ children, style, initialPage = 0, onPageSelected }, ref) => { + const [currentPage, setCurrentPage] = React.useState(initialPage); const pages = React.Children.toArray(children); + + React.useImperativeHandle(ref, () => ({ + setPage(page: number) { + setCurrentPage(page); + onPageSelected?.({ nativeEvent: { position: page } }); + }, + setPageWithoutAnimation(page: number) { + setCurrentPage(page); + onPageSelected?.({ nativeEvent: { position: page } }); + }, + })); + return ( - - {pages[initialPage] ?? pages[0] ?? null} + + {pages[currentPage] ?? pages[0] ?? null} ); }, diff --git a/shims/react-native-static-server.web.ts b/shims/react-native-static-server.web.ts new file mode 100644 index 0000000..a927e88 --- /dev/null +++ b/shims/react-native-static-server.web.ts @@ -0,0 +1,8 @@ +/** Web shim for @dr.pogodin/react-native-static-server - no-op for web */ +export default class StaticServer { + constructor(_port?: number, _root?: string, _options?: unknown) {} + start(): Promise { return Promise.resolve(''); } + stop(): Promise { return Promise.resolve(); } + isRunning(): boolean { return false; } + get origin(): string { return ''; } +} diff --git a/shims/react-native-video.web.tsx b/shims/react-native-video.web.tsx new file mode 100644 index 0000000..7c38001 --- /dev/null +++ b/shims/react-native-video.web.tsx @@ -0,0 +1,42 @@ +/** Web shim for react-native-video - uses HTML5