mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-07 23:18:38 +08:00
Merge pull request #30 from ac666666666/feat/ac-feature
fix(web): 补全缺失的 web shims 并修复渲染崩溃问题
This commit is contained in:
33
.kiro/steering/commit-convention.md
Normal file
33
.kiro/steering/commit-convention.md
Normal 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 快速开始步骤
|
||||
```
|
||||
@@ -10,6 +10,8 @@ import { useTheme } from '../utils/theme';
|
||||
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 ?? '',
|
||||
@@ -24,13 +26,18 @@ function RootLayout() {
|
||||
const restoreSettings = useSettingsStore(s => s.restore);
|
||||
const darkMode = useSettingsStore(s => s.darkMode);
|
||||
|
||||
const [fontsLoaded] = useFonts({
|
||||
...Ionicons.font,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
restore();
|
||||
loadDownloads();
|
||||
restoreSettings();
|
||||
|
||||
}, []);
|
||||
|
||||
if (!fontsLoaded) return null;
|
||||
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<StatusBar style={darkMode ? 'light' : 'dark'} />
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
7
shims/expo-clipboard.web.ts
Normal file
7
shims/expo-clipboard.web.ts
Normal 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 ''; }
|
||||
}
|
||||
14
shims/expo-file-system.web.ts
Normal file
14
shims/expo-file-system.web.ts
Normal 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 () => ({}) };
|
||||
}
|
||||
5
shims/expo-intent-launcher.web.ts
Normal file
5
shims/expo-intent-launcher.web.ts
Normal 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
10
shims/expo-network.web.ts
Normal 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; }
|
||||
@@ -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<View, PagerViewProps>(
|
||||
({ children, style, initialPage = 0 }, ref) => {
|
||||
export interface PagerViewHandle {
|
||||
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);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
setPage(page: number) {
|
||||
setCurrentPage(page);
|
||||
onPageSelected?.({ nativeEvent: { position: page } });
|
||||
},
|
||||
setPageWithoutAnimation(page: number) {
|
||||
setCurrentPage(page);
|
||||
onPageSelected?.({ nativeEvent: { position: page } });
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<View ref={ref} style={[{ flex: 1 }, style]}>
|
||||
{pages[initialPage] ?? pages[0] ?? null}
|
||||
<View style={[{ flex: 1 }, style]}>
|
||||
{pages[currentPage] ?? pages[0] ?? null}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
|
||||
8
shims/react-native-static-server.web.ts
Normal file
8
shims/react-native-static-server.web.ts
Normal 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 ''; }
|
||||
}
|
||||
42
shims/react-native-video.web.tsx
Normal file
42
shims/react-native-video.web.tsx
Normal 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;
|
||||
30
shims/sentry-react-native.web.tsx
Normal file
30
shims/sentry-react-native.web.tsx
Normal 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 };
|
||||
Reference in New Issue
Block a user