Merge pull request #30 from ac666666666/feat/ac-feature

fix(web): 补全缺失的 web shims 并修复渲染崩溃问题
This commit is contained in:
jinsha
2026-03-25 11:35:14 +08:00
committed by GitHub
11 changed files with 201 additions and 14 deletions

View File

@@ -0,0 +1,33 @@
---
inclusion: always
---
# 提交规范Conventional Commits
所有 git commit 必须遵循以下格式:
```
<type>(<scope>): <描述>
[可选正文]
```
## 类型说明
| 类型 | 含义 |
|------|------|
| feat | 新功能 |
| fix | Bug 修复 |
| refactor | 重构(不改变功能) |
| docs | 文档更新 |
| chore | 构建脚本、依赖更新等 |
| style | 代码格式(不影响逻辑) |
| perf | 性能优化 |
## 示例
```
feat(danmaku): 添加弹幕字体大小设置
fix(player): 修复 DASH MPD 解析在 Android 12 上崩溃的问题
docs: 更新 README 快速开始步骤
```

View File

@@ -10,6 +10,8 @@ import { useTheme } from '../utils/theme';
import { MiniPlayer } from '../components/MiniPlayer'; import { MiniPlayer } from '../components/MiniPlayer';
import * as Sentry from '@sentry/react-native'; import * as Sentry from '@sentry/react-native';
import { ErrorBoundary } from '@sentry/react-native'; import { ErrorBoundary } from '@sentry/react-native';
import { useFonts } from 'expo-font';
import { Ionicons } from '@expo/vector-icons';
Sentry.init({ Sentry.init({
dsn: process.env.EXPO_PUBLIC_SENTRY_DSN ?? '', dsn: process.env.EXPO_PUBLIC_SENTRY_DSN ?? '',
@@ -24,13 +26,18 @@ function RootLayout() {
const restoreSettings = useSettingsStore(s => s.restore); const restoreSettings = useSettingsStore(s => s.restore);
const darkMode = useSettingsStore(s => s.darkMode); const darkMode = useSettingsStore(s => s.darkMode);
const [fontsLoaded] = useFonts({
...Ionicons.font,
});
useEffect(() => { useEffect(() => {
restore(); restore();
loadDownloads(); loadDownloads();
restoreSettings(); restoreSettings();
}, []); }, []);
if (!fontsLoaded) return null;
return ( return (
<SafeAreaProvider> <SafeAreaProvider>
<StatusBar style={darkMode ? 'light' : 'dark'} /> <StatusBar style={darkMode ? 'light' : 'dark'} />

View File

@@ -1,16 +1,29 @@
const path = require('path'); const path = require('path');
const { const { getSentryExpoConfig } = require("@sentry/react-native/metro");
getSentryExpoConfig
} = require("@sentry/react-native/metro");
const config = getSentryExpoConfig(__dirname); 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 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) => { config.resolver.resolveRequest = (context, moduleName, platform) => {
if (platform === 'web' && moduleName === 'react-native-pager-view') { if (platform === 'web' && WEB_SHIMS[moduleName]) {
return { return {
filePath: path.resolve(__dirname, 'shims/react-native-pager-view.web.tsx'), filePath: path.resolve(__dirname, WEB_SHIMS[moduleName]),
type: 'sourceFile', type: 'sourceFile',
}; };
} }

View File

@@ -0,0 +1,7 @@
/** Web shim for expo-clipboard - use native browser clipboard API */
export async function setStringAsync(text: string): Promise<void> {
try { await navigator.clipboard.writeText(text); } catch {}
}
export async function getStringAsync(): Promise<string> {
try { return await navigator.clipboard.readText(); } catch { return ''; }
}

View File

@@ -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 () => ({}) };
}

View File

@@ -0,0 +1,5 @@
/** Web shim for expo-intent-launcher - no-op for web */
export async function startActivityAsync(_activity: string, _params?: unknown): Promise<unknown> {
return {};
}
export const ActivityAction = {};

10
shims/expo-network.web.ts Normal file
View File

@@ -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<string> { return '0.0.0.0'; }
export async function isAirplaneModeEnabledAsync(): Promise<boolean> { return false; }

View File

@@ -1,7 +1,6 @@
/** /**
* Web shim for react-native-pager-view. * Web shim for react-native-pager-view.
* eas update exports for web; this replaces the native-only module * Supports setPage/setPageWithoutAnimation via imperative handle.
* with a simple View-based container that renders the first child only.
*/ */
import React from 'react'; import React from 'react';
import { View, type ViewStyle } from 'react-native'; import { View, type ViewStyle } from 'react-native';
@@ -11,16 +10,35 @@ interface PagerViewProps {
style?: ViewStyle; style?: ViewStyle;
initialPage?: number; initialPage?: number;
scrollEnabled?: boolean; scrollEnabled?: boolean;
onPageSelected?: (e: any) => void; onPageSelected?: (e: { nativeEvent: { position: number } }) => void;
onPageScrollStateChanged?: (e: any) => void;
[key: string]: any; [key: string]: any;
} }
const PagerView = React.forwardRef<View, PagerViewProps>( export interface PagerViewHandle {
({ children, style, initialPage = 0 }, ref) => { setPage: (page: number) => void;
setPageWithoutAnimation: (page: number) => void;
}
const PagerView = React.forwardRef<PagerViewHandle, PagerViewProps>(
({ children, style, initialPage = 0, onPageSelected }, ref) => {
const [currentPage, setCurrentPage] = React.useState(initialPage);
const pages = React.Children.toArray(children); 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 ( return (
<View ref={ref} style={[{ flex: 1 }, style]}> <View style={[{ flex: 1 }, style]}>
{pages[initialPage] ?? pages[0] ?? null} {pages[currentPage] ?? pages[0] ?? null}
</View> </View>
); );
}, },

View File

@@ -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<string> { return Promise.resolve(''); }
stop(): Promise<void> { return Promise.resolve(); }
isRunning(): boolean { return false; }
get origin(): string { return ''; }
}

View File

@@ -0,0 +1,42 @@
/** Web shim for react-native-video - uses HTML5 <video> element */
import React from 'react';
interface VideoProps {
source?: { uri?: string } | number;
style?: React.CSSProperties;
paused?: boolean;
muted?: boolean;
repeat?: boolean;
onLoad?: (data: unknown) => void;
onError?: (error: unknown) => void;
onProgress?: (data: unknown) => void;
onEnd?: () => void;
resizeMode?: string;
[key: string]: unknown;
}
const Video = React.forwardRef<HTMLVideoElement, VideoProps>(
({ source, style, paused, muted, repeat, onLoad, onError, onProgress, onEnd }, ref) => {
const uri = typeof source === 'object' && source !== null ? (source as { uri?: string }).uri : undefined;
return (
<video
ref={ref}
src={uri}
style={style as React.CSSProperties}
autoPlay={!paused}
muted={muted}
loop={repeat}
onLoadedData={onLoad ? () => onLoad({}) : undefined}
onError={onError ? (e) => onError(e) : undefined}
onTimeUpdate={onProgress ? (e) => {
const t = e.currentTarget;
onProgress({ currentTime: t.currentTime, seekableDuration: t.duration });
} : undefined}
onEnded={onEnd}
playsInline
/>
);
}
);
export default Video;

View File

@@ -0,0 +1,30 @@
/** Web shim for @sentry/react-native - no-op stubs for web platform */
import React from 'react';
export const init = (_options?: unknown) => {};
export const wrap = <T>(component: T): T => component;
export const captureException = (_error: unknown, _hint?: unknown) => '';
export const captureMessage = (_message: string, _level?: unknown) => '';
export const setUser = (_user: unknown) => {};
export const setTag = (_key: string, _value: unknown) => {};
export const setExtra = (_key: string, _extra: unknown) => {};
export const addBreadcrumb = (_breadcrumb: unknown) => {};
export const configureScope = (_callback: unknown) => {};
export const withScope = (_callback: unknown) => {};
export const getCurrentHub = () => ({ getClient: () => undefined });
export const ReactNativeTracing = class {};
export const ReactNavigationInstrumentation = class {};
export const TouchEventBoundary = ({ children }: { children: React.ReactNode }) => children;
export class ErrorBoundary extends React.Component<
{ fallback: React.ReactNode; children: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
render() {
return this.state.hasError ? this.props.fallback : this.props.children;
}
}
export default { init, wrap, captureException, captureMessage, setUser, setTag, setExtra };