mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-07 23:18:38 +08:00
header
This commit is contained in:
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"liveServer.settings.port": 5501
|
||||||
|
}
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
import React, { useEffect, useState, useRef, useMemo, useCallback } from "react";
|
import React, {
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
useMemo,
|
||||||
|
useCallback,
|
||||||
|
} from "react";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -40,18 +46,20 @@ export default function HomeScreen() {
|
|||||||
const rows = useMemo(() => toListRows(videos), [videos]);
|
const rows = useMemo(() => toListRows(videos), [videos]);
|
||||||
|
|
||||||
// useRef-wrapped to satisfy FlatList's requirement that onViewableItemsChanged never changes identity after mount
|
// useRef-wrapped to satisfy FlatList's requirement that onViewableItemsChanged never changes identity after mount
|
||||||
|
//
|
||||||
const onViewableItemsChangedRef = useRef(
|
const onViewableItemsChangedRef = useRef(
|
||||||
({ viewableItems }: { viewableItems: ViewToken[] }) => {
|
({ viewableItems }: { viewableItems: ViewToken[] }) => {
|
||||||
const bigRow = viewableItems.find(
|
const bigRow = viewableItems.find(
|
||||||
(v) => v.item && (v.item as ListRow).type === 'big',
|
(v) => v.item && (v.item as ListRow).type === "big",
|
||||||
);
|
|
||||||
setVisibleBigKey(
|
|
||||||
bigRow ? (bigRow.item as BigRow).item.bvid : null,
|
|
||||||
);
|
);
|
||||||
|
setVisibleBigKey(bigRow ? (bigRow.item as BigRow).item.bvid : null);
|
||||||
},
|
},
|
||||||
).current;
|
).current;
|
||||||
|
|
||||||
const scrollY = useRef(new Animated.Value(0)).current;
|
const scrollY = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const headerTranslate = scrollY.interpolate({
|
const headerTranslate = scrollY.interpolate({
|
||||||
inputRange: [0, NAV_H],
|
inputRange: [0, NAV_H],
|
||||||
outputRange: [0, -NAV_H],
|
outputRange: [0, -NAV_H],
|
||||||
@@ -62,8 +70,16 @@ export default function HomeScreen() {
|
|||||||
load();
|
load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onScroll = useMemo(
|
||||||
|
() =>
|
||||||
|
Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], {
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const renderItem = useCallback(({ item: row }: { item: ListRow }) => {
|
const renderItem = useCallback(({ item: row }: { item: ListRow }) => {
|
||||||
if (row.type === 'big') {
|
if (row.type === "big") {
|
||||||
return (
|
return (
|
||||||
<BigVideoCard
|
<BigVideoCard
|
||||||
item={row.item}
|
item={row.item}
|
||||||
@@ -92,7 +108,7 @@ export default function HomeScreen() {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}, [visibleBigKey]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safe} edges={["left", "right"]}>
|
<SafeAreaView style={styles.safe} edges={["left", "right"]}>
|
||||||
@@ -100,9 +116,9 @@ export default function HomeScreen() {
|
|||||||
style={styles.listContainer}
|
style={styles.listContainer}
|
||||||
data={rows}
|
data={rows}
|
||||||
keyExtractor={(row: any) =>
|
keyExtractor={(row: any) =>
|
||||||
row.type === 'big'
|
row.type === "big"
|
||||||
? `big-${row.item.bvid}`
|
? `big-${row.item.bvid}`
|
||||||
: `pair-${row.left.bvid}-${row.right?.bvid ?? 'empty'}`
|
: `pair-${row.left.bvid}-${row.right?.bvid ?? "empty"}`
|
||||||
}
|
}
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
paddingTop: insets.top + NAV_H + 6,
|
paddingTop: insets.top + NAV_H + 6,
|
||||||
@@ -126,13 +142,9 @@ export default function HomeScreen() {
|
|||||||
{loading && <ActivityIndicator color="#00AEEC" />}
|
{loading && <ActivityIndicator color="#00AEEC" />}
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
onScroll={Animated.event(
|
onScroll={onScroll}
|
||||||
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
|
|
||||||
{ useNativeDriver: true },
|
|
||||||
)}
|
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 绝对定位导航栏:paddingTop 手动适配刘海/状态栏 */}
|
{/* 绝对定位导航栏:paddingTop 手动适配刘海/状态栏 */}
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
@@ -236,7 +248,11 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: "#00AEEC",
|
backgroundColor: "#00AEEC",
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
},
|
},
|
||||||
row: { flexDirection: 'row', paddingHorizontal: 1, justifyContent: "flex-start" },
|
row: {
|
||||||
|
flexDirection: "row",
|
||||||
|
paddingHorizontal: 1,
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
},
|
||||||
leftCol: { marginLeft: 4, marginRight: 2 },
|
leftCol: { marginLeft: 4, marginRight: 2 },
|
||||||
rightCol: { marginLeft: 2, marginRight: 4 },
|
rightCol: { marginLeft: 2, marginRight: 4 },
|
||||||
footer: { height: 48, alignItems: "center", justifyContent: "center" },
|
footer: { height: 48, alignItems: "center", justifyContent: "center" },
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import React, { useRef, useMemo, useEffect } from 'react';
|
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
||||||
import { View, Text, FlatList, TouchableOpacity, StyleSheet } from 'react-native';
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
FlatList,
|
||||||
|
TouchableOpacity,
|
||||||
|
StyleSheet,
|
||||||
|
Animated,
|
||||||
|
NativeSyntheticEvent,
|
||||||
|
NativeScrollEvent,
|
||||||
|
} from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { DanmakuItem } from '../services/types';
|
import { DanmakuItem } from '../services/types';
|
||||||
import { danmakuColorToCss } from '../utils/danmaku';
|
import { danmakuColorToCss } from '../utils/danmaku';
|
||||||
@@ -11,19 +20,163 @@ interface Props {
|
|||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DisplayedDanmaku extends DanmakuItem {
|
||||||
|
_key: number;
|
||||||
|
_fadeAnim: Animated.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_DISPLAYED = 100;
|
||||||
|
const DRIP_INTERVAL = 250;
|
||||||
|
const FAST_DRIP_INTERVAL = 100;
|
||||||
|
const QUEUE_FAST_THRESHOLD = 50;
|
||||||
|
const SEEK_THRESHOLD = 2;
|
||||||
|
|
||||||
|
function formatTimestamp(seconds: number): string {
|
||||||
|
const m = Math.floor(seconds / 60);
|
||||||
|
const s = Math.floor(seconds % 60);
|
||||||
|
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
export default function DanmakuList({ danmakus, currentTime, visible, onToggle }: Props) {
|
export default function DanmakuList({ danmakus, currentTime, visible, onToggle }: Props) {
|
||||||
const flatListRef = useRef<FlatList>(null);
|
const flatListRef = useRef<FlatList>(null);
|
||||||
|
const [displayedItems, setDisplayedItems] = useState<DisplayedDanmaku[]>([]);
|
||||||
|
const [unseenCount, setUnseenCount] = useState(0);
|
||||||
|
|
||||||
const visibleItems = useMemo(
|
const queueRef = useRef<DanmakuItem[]>([]);
|
||||||
() => danmakus.filter(d => d.time <= currentTime),
|
const lastTimeRef = useRef(0);
|
||||||
[danmakus, currentTime]
|
const processedIndexRef = useRef(0);
|
||||||
|
const keyCounterRef = useRef(0);
|
||||||
|
const isAtBottomRef = useRef(true);
|
||||||
|
const danmakusRef = useRef(danmakus);
|
||||||
|
|
||||||
|
// Reset everything when danmakus array reference changes (video switch)
|
||||||
|
useEffect(() => {
|
||||||
|
if (danmakusRef.current !== danmakus) {
|
||||||
|
danmakusRef.current = danmakus;
|
||||||
|
queueRef.current = [];
|
||||||
|
processedIndexRef.current = 0;
|
||||||
|
lastTimeRef.current = 0;
|
||||||
|
setDisplayedItems([]);
|
||||||
|
setUnseenCount(0);
|
||||||
|
isAtBottomRef.current = true;
|
||||||
|
}
|
||||||
|
}, [danmakus]);
|
||||||
|
|
||||||
|
// Watch currentTime, enqueue new danmakus
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible || danmakus.length === 0) return;
|
||||||
|
|
||||||
|
const prevTime = lastTimeRef.current;
|
||||||
|
lastTimeRef.current = currentTime;
|
||||||
|
|
||||||
|
// Seek detection
|
||||||
|
if (Math.abs(currentTime - prevTime) > SEEK_THRESHOLD) {
|
||||||
|
queueRef.current = [];
|
||||||
|
processedIndexRef.current = 0;
|
||||||
|
setDisplayedItems([]);
|
||||||
|
setUnseenCount(0);
|
||||||
|
isAtBottomRef.current = true;
|
||||||
|
|
||||||
|
// Re-enqueue danmakus up to current time
|
||||||
|
const catchUp = danmakus.filter(d => d.time <= currentTime);
|
||||||
|
// Only enqueue recent ones to avoid flooding
|
||||||
|
const tail = catchUp.slice(-20);
|
||||||
|
queueRef.current = tail;
|
||||||
|
processedIndexRef.current = danmakus.findIndex(
|
||||||
|
d => d.time > currentTime
|
||||||
|
);
|
||||||
|
if (processedIndexRef.current === -1) {
|
||||||
|
processedIndexRef.current = danmakus.length;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal progression: enqueue danmakus between prevTime and currentTime
|
||||||
|
const sorted = danmakus; // assumed sorted by time
|
||||||
|
let i = processedIndexRef.current;
|
||||||
|
while (i < sorted.length && sorted[i].time <= currentTime) {
|
||||||
|
queueRef.current.push(sorted[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
processedIndexRef.current = i;
|
||||||
|
}, [currentTime, danmakus, visible]);
|
||||||
|
|
||||||
|
// Drip interval: pop from queue, append to displayed
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) return;
|
||||||
|
|
||||||
|
const id = setInterval(() => {
|
||||||
|
if (queueRef.current.length === 0) return;
|
||||||
|
|
||||||
|
const item = queueRef.current.shift()!;
|
||||||
|
const fadeAnim = new Animated.Value(0);
|
||||||
|
const displayed: DisplayedDanmaku = {
|
||||||
|
...item,
|
||||||
|
_key: keyCounterRef.current++,
|
||||||
|
_fadeAnim: fadeAnim,
|
||||||
|
};
|
||||||
|
|
||||||
|
Animated.timing(fadeAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 200,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}).start();
|
||||||
|
|
||||||
|
setDisplayedItems(prev => {
|
||||||
|
const next = [...prev, displayed];
|
||||||
|
return next.length > MAX_DISPLAYED ? next.slice(-MAX_DISPLAYED) : next;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isAtBottomRef.current) {
|
||||||
|
// Auto-scroll on next frame
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
flatListRef.current?.scrollToEnd({ animated: true });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setUnseenCount(c => c + 1);
|
||||||
|
}
|
||||||
|
}, queueRef.current.length > QUEUE_FAST_THRESHOLD ? FAST_DRIP_INTERVAL : DRIP_INTERVAL);
|
||||||
|
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
const handleScroll = useCallback(
|
||||||
|
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||||
|
const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
|
||||||
|
const distanceFromBottom =
|
||||||
|
contentSize.height - layoutMeasurement.height - contentOffset.y;
|
||||||
|
isAtBottomRef.current = distanceFromBottom < 40;
|
||||||
|
if (isAtBottomRef.current) {
|
||||||
|
setUnseenCount(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleScrollBeginDrag = useCallback(() => {
|
||||||
if (visible && visibleItems.length > 0) {
|
isAtBottomRef.current = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePillPress = useCallback(() => {
|
||||||
flatListRef.current?.scrollToEnd({ animated: true });
|
flatListRef.current?.scrollToEnd({ animated: true });
|
||||||
}
|
setUnseenCount(0);
|
||||||
}, [visibleItems.length, visible]);
|
isAtBottomRef.current = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderItem = useCallback(({ item }: { item: DisplayedDanmaku }) => {
|
||||||
|
const dotColor = danmakuColorToCss(item.color);
|
||||||
|
return (
|
||||||
|
<Animated.View style={[styles.bubble, { opacity: item._fadeAnim }]}>
|
||||||
|
<View style={[styles.colorDot, { backgroundColor: dotColor }]} />
|
||||||
|
<Text style={styles.bubbleText} numberOfLines={3}>
|
||||||
|
{item.text}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.timestamp}>{formatTimestamp(item.time)}</Text>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const keyExtractor = useCallback((item: DisplayedDanmaku) => String(item._key), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
@@ -44,25 +197,34 @@ export default function DanmakuList({ danmakus, currentTime, visible, onToggle }
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{visible && (
|
{visible && (
|
||||||
|
<View style={styles.listWrapper}>
|
||||||
<FlatList
|
<FlatList
|
||||||
ref={flatListRef}
|
ref={flatListRef}
|
||||||
data={visibleItems}
|
data={displayedItems}
|
||||||
keyExtractor={(item, i) => `${item.time}_${item.text}_${i}`}
|
keyExtractor={keyExtractor}
|
||||||
|
renderItem={renderItem}
|
||||||
style={styles.list}
|
style={styles.list}
|
||||||
renderItem={({ item }) => (
|
contentContainerStyle={styles.listContent}
|
||||||
<Text
|
onScroll={handleScroll}
|
||||||
style={[styles.item, { color: danmakuColorToCss(item.color) }]}
|
onScrollBeginDrag={handleScrollBeginDrag}
|
||||||
numberOfLines={1}
|
scrollEventThrottle={16}
|
||||||
>
|
removeClippedSubviews={true}
|
||||||
{item.text}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<Text style={styles.empty}>
|
<Text style={styles.empty}>
|
||||||
{danmakus.length === 0 ? '暂无弹幕' : '弹幕将随视频播放显示'}
|
{danmakus.length === 0 ? '暂无弹幕' : '弹幕将随视频播放显示'}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{unseenCount > 0 && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.pill}
|
||||||
|
onPress={handlePillPress}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<Text style={styles.pillText}>{unseenCount} 条新弹幕</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -87,15 +249,61 @@ const styles = StyleSheet.create({
|
|||||||
color: '#212121',
|
color: '#212121',
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
list: {
|
listWrapper: {
|
||||||
height: 180,
|
height: 200,
|
||||||
paddingHorizontal: 12,
|
position: 'relative',
|
||||||
},
|
},
|
||||||
item: {
|
list: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#fafafa',
|
||||||
|
},
|
||||||
|
listContent: {
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 4,
|
||||||
|
},
|
||||||
|
bubble: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
backgroundColor: '#f8f8f8',
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 6,
|
||||||
|
marginVertical: 2,
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
colorDot: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
borderRadius: 3,
|
||||||
|
marginTop: 6,
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
bubbleText: {
|
||||||
|
flex: 1,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
paddingVertical: 3,
|
color: '#333',
|
||||||
lineHeight: 18,
|
lineHeight: 18,
|
||||||
},
|
},
|
||||||
|
timestamp: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: '#bbb',
|
||||||
|
marginTop: 1,
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
pill: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 8,
|
||||||
|
alignSelf: 'center',
|
||||||
|
backgroundColor: '#00AEEC',
|
||||||
|
borderRadius: 16,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 6,
|
||||||
|
},
|
||||||
|
pillText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
empty: {
|
empty: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#999',
|
color: '#999',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start --port 8082",
|
"start": "expo start",
|
||||||
"android": "expo run:android",
|
"android": "expo run:android",
|
||||||
"ios": "expo run:ios",
|
"ios": "expo run:ios",
|
||||||
"web": "expo start --web",
|
"web": "expo start --web",
|
||||||
|
|||||||
@@ -184,29 +184,29 @@ export async function getDanmaku(cid: number): Promise<DanmakuItem[]> {
|
|||||||
return parseDanmakuXml(res.data);
|
return parseDanmakuXml(res.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Native 策略 1:responseType: 'text',依赖 OkHttp 自动解压
|
// Native:arraybuffer + 逐一尝试解压(服务器强制压缩,无法避免)
|
||||||
const res = await axios.get(`${COMMENT_BASE}/${cid}.xml`, {
|
const res = await axios.get(`${COMMENT_BASE}/${cid}.xml`, {
|
||||||
headers: { Referer: 'https://www.bilibili.com', 'User-Agent': UA },
|
|
||||||
responseType: 'text',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (typeof res.data === 'string' && res.data.includes('<d ')) {
|
|
||||||
return parseDanmakuXml(res.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 策略 2 回退:arraybuffer + pako 手动解压
|
|
||||||
const res2 = await axios.get(`${COMMENT_BASE}/${cid}.xml`, {
|
|
||||||
headers: { Referer: 'https://www.bilibili.com', 'User-Agent': UA },
|
headers: { Referer: 'https://www.bilibili.com', 'User-Agent': UA },
|
||||||
responseType: 'arraybuffer',
|
responseType: 'arraybuffer',
|
||||||
});
|
});
|
||||||
|
|
||||||
const bytes = new Uint8Array(res2.data as ArrayBuffer);
|
const bytes = new Uint8Array(res.data as ArrayBuffer);
|
||||||
let xmlText: string;
|
let xmlText: string | undefined;
|
||||||
if (bytes[0] === 0x1f && bytes[1] === 0x8b) {
|
|
||||||
xmlText = pako.inflate(bytes, { to: 'string' });
|
// 依次尝试:inflate (gzip/zlib) → inflateRaw (raw deflate)
|
||||||
} else {
|
for (const fn of [pako.inflate, pako.inflateRaw] as Array<(input: Uint8Array, opts: pako.InflateOptions) => string>) {
|
||||||
|
try {
|
||||||
|
xmlText = fn(bytes, { to: 'string' });
|
||||||
|
if (xmlText.includes('<d ')) break;
|
||||||
|
xmlText = undefined;
|
||||||
|
} catch { /* 继续尝试下一种 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!xmlText) {
|
||||||
|
// 最后尝试当作明文
|
||||||
xmlText = new TextDecoder('utf-8').decode(bytes);
|
xmlText = new TextDecoder('utf-8').decode(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseDanmakuXml(xmlText);
|
return parseDanmakuXml(xmlText);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('getDanmaku failed:', e);
|
console.warn('getDanmaku failed:', e);
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ import { Platform } from 'react-native';
|
|||||||
* Native 端直接返回原 URL(App 请求头由 axios 拦截器统一设置)。
|
* Native 端直接返回原 URL(App 请求头由 axios 拦截器统一设置)。
|
||||||
*/
|
*/
|
||||||
export function proxyImageUrl(url: string): string {
|
export function proxyImageUrl(url: string): string {
|
||||||
if (Platform.OS !== 'web' || !url) return url;
|
if (!url) return url;
|
||||||
|
if (Platform.OS === 'web') {
|
||||||
return url.replace(
|
return url.replace(
|
||||||
/^https?:\/\/([a-z0-9]+\.hdslb\.com)/,
|
/^https?:\/\/([a-z0-9]+\.hdslb\.com)/,
|
||||||
'http://localhost:3001/bilibili-img/$1',
|
'http://localhost:3001/bilibili-img/$1',
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
// Native: force HTTPS so Release APK doesn't block cleartext HTTP
|
||||||
|
return url.replace(/^http:\/\//, 'https://');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user