mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
fix: Improves video player reliability and performance
Prevents crashes by adding safeguards that check if video is loaded before calling player methods Removes performance monitoring hook that was causing unnecessary overhead during playback Reorganizes code structure by removing excessive comment sections and consolidating related functionality for better maintainability Updates Biome linter to latest version for improved code formatting and analysis
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react-native/no-inline-styles */
|
||||
import {
|
||||
type BaseItemDto,
|
||||
type MediaSourceInfo,
|
||||
@@ -48,32 +49,13 @@ import { storage } from "@/utils/mmkv";
|
||||
import generateDeviceProfile from "@/utils/profiles/native";
|
||||
import { msToTicks, ticksToSeconds } from "@/utils/time";
|
||||
|
||||
/* ---------- helpers ---------- */
|
||||
|
||||
const downloadProvider = !Platform.isTV
|
||||
? require("@/providers/DownloadProvider")
|
||||
: { useDownload: () => null };
|
||||
|
||||
const IGNORE_SAFE_AREAS_KEY = "video_player_ignore_safe_areas";
|
||||
|
||||
/* ---------- performance monitor ---------- */
|
||||
|
||||
const usePerformanceMonitoring = (name: string) => {
|
||||
useEffect(() => {
|
||||
const start = performance.now();
|
||||
return () => {
|
||||
const end = performance.now();
|
||||
const ms = end - start;
|
||||
if (ms > 16.67) {
|
||||
// >1 frame at 60 fps
|
||||
console.warn(`[Perf] ${name} render took ${ms.toFixed(1)} ms`);
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/* ---------- reducer ---------- */
|
||||
|
||||
/* Playback state reducer to consolidate related state */
|
||||
interface VideoState {
|
||||
isPlaying: boolean;
|
||||
isMuted: boolean;
|
||||
@@ -95,6 +77,7 @@ const videoReducer = (state: VideoState, action: VideoAction): VideoState => {
|
||||
case "BUFFERING_CHANGED":
|
||||
return { ...state, isBuffering: action.value };
|
||||
case "VIDEO_LOADED":
|
||||
// Mark video as loaded and buffering false here
|
||||
return { ...state, isVideoLoaded: true, isBuffering: false };
|
||||
case "MUTED_CHANGED":
|
||||
return { ...state, isMuted: action.value };
|
||||
@@ -113,23 +96,16 @@ const initialVideoState: VideoState = {
|
||||
isPipStarted: false,
|
||||
};
|
||||
|
||||
/* ---------- main component ---------- */
|
||||
|
||||
export default function DirectPlayerPage() {
|
||||
usePerformanceMonitoring("DirectPlayerPage");
|
||||
|
||||
/* ---------- refs & atoms ---------- */
|
||||
const videoRef = useRef<VlcPlayerViewRef>(null);
|
||||
const user = useAtomValue(userAtom);
|
||||
const api = useAtomValue(apiAtom);
|
||||
|
||||
const navigation = useNavigation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
/* ---------- consolidated playback state ---------- */
|
||||
/* Consolidated video playback state */
|
||||
const [videoState, dispatch] = useReducer(videoReducer, initialVideoState);
|
||||
|
||||
/* ---------- misc UI state ---------- */
|
||||
const [showControls, _setShowControls] = useState(true);
|
||||
const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(() => {
|
||||
return storage.getBoolean(IGNORE_SAFE_AREAS_KEY) ?? false;
|
||||
@@ -147,7 +123,6 @@ export default function DirectPlayerPage() {
|
||||
? null
|
||||
: require("react-native-volume-manager");
|
||||
|
||||
/* ---------- URL params ---------- */
|
||||
const {
|
||||
itemId,
|
||||
audioIndex: audioIndexStr,
|
||||
@@ -173,7 +148,6 @@ export default function DirectPlayerPage() {
|
||||
? parseInt(bitrateValueStr, 10)
|
||||
: BITRATES[0].value;
|
||||
|
||||
/* ---------- stable callbacks ---------- */
|
||||
const setShowControls = useCallback(
|
||||
(show: boolean) => {
|
||||
_setShowControls(show);
|
||||
@@ -186,9 +160,7 @@ export default function DirectPlayerPage() {
|
||||
storage.set(IGNORE_SAFE_AREAS_KEY, ignoreSafeAreas);
|
||||
}, [ignoreSafeAreas]);
|
||||
|
||||
/* ---------- data fetching ---------- */
|
||||
|
||||
/* item */
|
||||
/* Fetch the item info */
|
||||
const [item, setItem] = useState<BaseItemDto | null>(null);
|
||||
const [itemStatus, setItemStatus] = useState({
|
||||
isLoading: true,
|
||||
@@ -236,7 +208,7 @@ export default function DirectPlayerPage() {
|
||||
return () => controller.abort();
|
||||
}, [itemId, offline, api, user?.Id, getDownloadedItem]);
|
||||
|
||||
/* stream */
|
||||
/* Fetch stream info */
|
||||
interface Stream {
|
||||
mediaSource: MediaSourceInfo;
|
||||
sessionId: string;
|
||||
@@ -306,10 +278,9 @@ export default function DirectPlayerPage() {
|
||||
subtitleIndex,
|
||||
]);
|
||||
|
||||
/* ---------- playback API reporting ---------- */
|
||||
|
||||
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
|
||||
|
||||
/* Memoized playback state info for reporting */
|
||||
const currentPlayStateInfo = useMemo(() => {
|
||||
if (!stream) return null;
|
||||
return {
|
||||
@@ -337,6 +308,7 @@ export default function DirectPlayerPage() {
|
||||
stream,
|
||||
]);
|
||||
|
||||
/* Playback progress reporting */
|
||||
const reportPlaybackProgress = useCallback(async () => {
|
||||
if (!api || offline || !stream || !currentPlayStateInfo) return;
|
||||
await getPlaystateApi(api).reportPlaybackProgress({
|
||||
@@ -344,6 +316,7 @@ export default function DirectPlayerPage() {
|
||||
});
|
||||
}, [api, offline, stream, currentPlayStateInfo]);
|
||||
|
||||
/* Report playback stopped */
|
||||
const reportPlaybackStopped = useCallback(async () => {
|
||||
if (offline || !stream) return;
|
||||
await getPlaystateApi(api!).onPlaybackStopped({
|
||||
@@ -363,8 +336,7 @@ export default function DirectPlayerPage() {
|
||||
revalidateProgressCache,
|
||||
]);
|
||||
|
||||
/* ---------- UI / player actions ---------- */
|
||||
|
||||
/* Toggle play/pause */
|
||||
const togglePlay = useCallback(async () => {
|
||||
lightHapticFeedback();
|
||||
const playing = videoState.isPlaying;
|
||||
@@ -375,9 +347,11 @@ export default function DirectPlayerPage() {
|
||||
reportPlaybackProgress();
|
||||
} else {
|
||||
await videoRef.current?.play();
|
||||
await getPlaystateApi(api!).reportPlaybackStart({
|
||||
playbackStartInfo: currentPlayStateInfo as PlaybackStartInfo,
|
||||
});
|
||||
if (currentPlayStateInfo) {
|
||||
await getPlaystateApi(api!).reportPlaybackStart({
|
||||
playbackStartInfo: currentPlayStateInfo as PlaybackStartInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [
|
||||
videoState.isPlaying,
|
||||
@@ -387,8 +361,7 @@ export default function DirectPlayerPage() {
|
||||
currentPlayStateInfo,
|
||||
]);
|
||||
|
||||
/* ---------- React Navigation cleanup ---------- */
|
||||
|
||||
/* Stop playback and clean up */
|
||||
const stop = useCallback(() => {
|
||||
reportPlaybackStopped();
|
||||
setIsPlaybackStopped(true);
|
||||
@@ -400,18 +373,15 @@ export default function DirectPlayerPage() {
|
||||
return unsubscribe;
|
||||
}, [navigation, stop]);
|
||||
|
||||
/* ---------- VLC init options ---------- */
|
||||
|
||||
/* VLC init options optimized for performance */
|
||||
const optimizedInitOptions = useMemo(() => {
|
||||
const opts = [`--sub-text-scale=${settings.subtitleSize}`];
|
||||
|
||||
// reduce buffering memory
|
||||
// Reduce buffering memory usage
|
||||
opts.push("--network-caching=300", "--file-caching=300");
|
||||
|
||||
if (Platform.OS === "android") opts.push("--aout=opensles");
|
||||
if (Platform.OS === "ios") opts.push("--ios-hw-decoding");
|
||||
|
||||
// pre-select tracks
|
||||
// Pre-selection of audio & subtitle tracks handled here
|
||||
const notTranscoding = !stream?.mediaSource.TranscodingUrl;
|
||||
const allAudio =
|
||||
stream?.mediaSource.MediaStreams?.filter((s) => s.Type === "Audio") ?? [];
|
||||
@@ -444,26 +414,20 @@ export default function DirectPlayerPage() {
|
||||
return opts;
|
||||
}, [settings.subtitleSize, stream?.mediaSource, subtitleIndex, audioIndex]);
|
||||
|
||||
/* ---------- picture-in-picture ---------- */
|
||||
|
||||
/* On Picture-In-Picture started or stopped */
|
||||
const onPipStarted = useCallback((e: PipStartedPayload) => {
|
||||
dispatch({ type: "PIP_CHANGED", value: e.nativeEvent.pipStarted });
|
||||
}, []);
|
||||
|
||||
/* ---------- progress ---------- */
|
||||
|
||||
/* Progress event handler */
|
||||
const onProgress = useCallback(
|
||||
(data: ProgressUpdatePayload) => {
|
||||
if (isSeeking.get() || isPlaybackStopped) return;
|
||||
|
||||
if (videoState.isBuffering)
|
||||
dispatch({ type: "BUFFERING_CHANGED", value: false });
|
||||
|
||||
const { currentTime } = data.nativeEvent;
|
||||
progress.set(currentTime);
|
||||
|
||||
router.setParams({ playbackPosition: msToTicks(currentTime).toString() });
|
||||
|
||||
if (!offline) reportPlaybackProgress();
|
||||
},
|
||||
[
|
||||
@@ -476,12 +440,10 @@ export default function DirectPlayerPage() {
|
||||
],
|
||||
);
|
||||
|
||||
/* ---------- playback state listener ---------- */
|
||||
|
||||
/* Playback state changes */
|
||||
const onPlaybackStateChanged = useCallback(
|
||||
async (e: PlaybackStatePayload) => {
|
||||
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
||||
|
||||
switch (state) {
|
||||
case "Playing":
|
||||
dispatch({ type: "PLAYING_CHANGED", value: true });
|
||||
@@ -494,7 +456,6 @@ export default function DirectPlayerPage() {
|
||||
reportPlaybackProgress();
|
||||
break;
|
||||
default:
|
||||
// fallback
|
||||
dispatch({ type: "BUFFERING_CHANGED", value: !!isBuffering });
|
||||
dispatch({ type: "PLAYING_CHANGED", value: !!isPlaying });
|
||||
}
|
||||
@@ -502,85 +463,18 @@ export default function DirectPlayerPage() {
|
||||
[reportPlaybackProgress],
|
||||
);
|
||||
|
||||
/* ---------- web socket / remote ---------- */
|
||||
|
||||
/* volume handlers */
|
||||
const [previousVolume, setPreviousVolume] = useState<number | null>(null);
|
||||
|
||||
const volumeUpCb = useCallback(async () => {
|
||||
if (Platform.isTV) return;
|
||||
const { volume } = await VolumeManager.getVolume();
|
||||
await VolumeManager.setVolume(Math.min(volume + 0.1, 1));
|
||||
}, []);
|
||||
|
||||
const volumeDownCb = useCallback(async () => {
|
||||
if (Platform.isTV) return;
|
||||
const { volume } = await VolumeManager.getVolume();
|
||||
await VolumeManager.setVolume(Math.max(volume - 0.1, 0));
|
||||
}, []);
|
||||
|
||||
const setVolumeCb = useCallback(async (v: number) => {
|
||||
if (Platform.isTV) return;
|
||||
await VolumeManager.setVolume(Math.max(0, Math.min(v, 100)) / 100);
|
||||
}, []);
|
||||
|
||||
const toggleMuteCb = useCallback(async () => {
|
||||
if (Platform.isTV) return;
|
||||
const { volume } = await VolumeManager.getVolume();
|
||||
const percent = volume * 100;
|
||||
if (percent > 0) {
|
||||
setPreviousVolume(percent);
|
||||
await VolumeManager.setVolume(0);
|
||||
dispatch({ type: "MUTED_CHANGED", value: true });
|
||||
} else {
|
||||
const restore = previousVolume || 50;
|
||||
await VolumeManager.setVolume(restore / 100);
|
||||
setPreviousVolume(null);
|
||||
dispatch({ type: "MUTED_CHANGED", value: false });
|
||||
}
|
||||
}, [previousVolume]);
|
||||
|
||||
useWebSocket({
|
||||
isPlaying: videoState.isPlaying,
|
||||
togglePlay,
|
||||
stopPlayback: stop,
|
||||
offline,
|
||||
toggleMute: toggleMuteCb,
|
||||
volumeUp: volumeUpCb,
|
||||
volumeDown: volumeDownCb,
|
||||
setVolume: setVolumeCb,
|
||||
});
|
||||
|
||||
/* ---------- start position ---------- */
|
||||
|
||||
const startPosition = useMemo(
|
||||
() => (offline ? 0 : ticksToSeconds(getInitialPlaybackTicks())),
|
||||
[offline, getInitialPlaybackTicks],
|
||||
);
|
||||
|
||||
/* ---------- subtitle & audio helpers ---------- */
|
||||
|
||||
const _allAudio =
|
||||
stream?.mediaSource.MediaStreams?.filter((a) => a.Type === "Audio") ?? [];
|
||||
const allSubs =
|
||||
stream?.mediaSource.MediaStreams?.filter(
|
||||
(s) => s.Type === "Subtitle",
|
||||
)?.sort((a, b) => Number(a.IsExternal) - Number(b.IsExternal)) ?? [];
|
||||
|
||||
const externalSubtitles = allSubs
|
||||
.filter((s) => s.DeliveryMethod === "External")
|
||||
.map((s) => ({
|
||||
name: s.DisplayTitle,
|
||||
DeliveryUrl: api?.basePath + s.DeliveryUrl,
|
||||
}));
|
||||
|
||||
/* ---------- player helpers (memoised safe wrappers) ---------- */
|
||||
/* Safe wrapper for player methods that skips calls if video not loaded */
|
||||
const safeMethod =
|
||||
<T extends unknown[]>(
|
||||
fn: ((...args: T) => any) | undefined,
|
||||
name: string,
|
||||
) =>
|
||||
async (...args: T) => {
|
||||
// New safeguard: skip calling if video not loaded yet
|
||||
if (!videoState.isVideoLoaded) {
|
||||
writeToLog("WARN", `${name} skipped - video not loaded yet`);
|
||||
return;
|
||||
}
|
||||
if (!fn) {
|
||||
writeToLog("ERROR", `${name} fn missing`, {
|
||||
isVideoLoaded: videoState.isVideoLoaded,
|
||||
@@ -638,19 +532,56 @@ export default function DirectPlayerPage() {
|
||||
[videoRef],
|
||||
);
|
||||
|
||||
/* ---------- memory / cache cleanup ---------- */
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (!videoState.isPlaying) videoRef.current?.clearCache?.();
|
||||
}, 60000); // every minute
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
videoRef.current?.dispose?.();
|
||||
};
|
||||
}, [videoState.isPlaying]);
|
||||
/* Volume handlers */
|
||||
const [previousVolume, setPreviousVolume] = useState<number | null>(null);
|
||||
const volumeUpCb = useCallback(async () => {
|
||||
if (Platform.isTV) return;
|
||||
const { volume } = await VolumeManager.getVolume();
|
||||
await VolumeManager.setVolume(Math.min(volume + 0.1, 1));
|
||||
}, []);
|
||||
const volumeDownCb = useCallback(async () => {
|
||||
if (Platform.isTV) return;
|
||||
const { volume } = await VolumeManager.getVolume();
|
||||
await VolumeManager.setVolume(Math.max(volume - 0.1, 0));
|
||||
}, []);
|
||||
const setVolumeCb = useCallback(async (v: number) => {
|
||||
if (Platform.isTV) return;
|
||||
await VolumeManager.setVolume(Math.max(0, Math.min(v, 100)) / 100);
|
||||
}, []);
|
||||
const toggleMuteCb = useCallback(async () => {
|
||||
if (Platform.isTV) return;
|
||||
const { volume } = await VolumeManager.getVolume();
|
||||
const percent = volume * 100;
|
||||
if (percent > 0) {
|
||||
setPreviousVolume(percent);
|
||||
await VolumeManager.setVolume(0);
|
||||
dispatch({ type: "MUTED_CHANGED", value: true });
|
||||
} else {
|
||||
const restore = previousVolume || 50;
|
||||
await VolumeManager.setVolume(restore / 100);
|
||||
setPreviousVolume(null);
|
||||
dispatch({ type: "MUTED_CHANGED", value: false });
|
||||
}
|
||||
}, [previousVolume]);
|
||||
|
||||
/* ---------- render guard ---------- */
|
||||
useWebSocket({
|
||||
isPlaying: videoState.isPlaying,
|
||||
togglePlay,
|
||||
stopPlayback: stop,
|
||||
offline,
|
||||
toggleMute: toggleMuteCb,
|
||||
volumeUp: volumeUpCb,
|
||||
volumeDown: volumeDownCb,
|
||||
setVolume: setVolumeCb,
|
||||
});
|
||||
|
||||
/* Calculate start position in seconds */
|
||||
const startPosition = useMemo(
|
||||
() => (offline ? 0 : ticksToSeconds(getInitialPlaybackTicks())),
|
||||
[offline, getInitialPlaybackTicks],
|
||||
);
|
||||
|
||||
/* Conditionally render based on loading and error state */
|
||||
if (itemStatus.isError || streamStatus.isError) {
|
||||
return (
|
||||
<View className='w-screen h-screen items-center justify-center bg-black'>
|
||||
@@ -658,7 +589,6 @@ export default function DirectPlayerPage() {
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (itemStatus.isLoading || streamStatus.isLoading || !item || !stream) {
|
||||
return (
|
||||
<View className='w-screen h-screen items-center justify-center bg-black'>
|
||||
@@ -667,7 +597,16 @@ export default function DirectPlayerPage() {
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- render ---------- */
|
||||
const allSubs =
|
||||
stream?.mediaSource.MediaStreams?.filter((s) => s.Type === "Subtitle") ||
|
||||
[];
|
||||
const externalSubtitles = allSubs
|
||||
.filter((s) => s.DeliveryMethod === "External")
|
||||
.map((s) => ({
|
||||
name: s.DisplayTitle,
|
||||
DeliveryUrl: api?.basePath + s.DeliveryUrl,
|
||||
}));
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: "black" }}>
|
||||
<View
|
||||
@@ -693,6 +632,7 @@ export default function DirectPlayerPage() {
|
||||
progressUpdateInterval={1000}
|
||||
onVideoStateChange={onPlaybackStateChanged}
|
||||
onPipStarted={onPipStarted}
|
||||
// Mark video as loaded on load end to enable player method calls safely
|
||||
onVideoLoadEnd={() => dispatch({ type: "VIDEO_LOADED" })}
|
||||
onVideoError={(e) => {
|
||||
console.error("Video Error:", e.nativeEvent);
|
||||
@@ -726,12 +666,17 @@ export default function DirectPlayerPage() {
|
||||
pause={pause}
|
||||
seek={seek}
|
||||
enableTrickplay
|
||||
getAudioTracks={getAudioTracks}
|
||||
getSubtitleTracks={getSubtitleTracks}
|
||||
// Pass undefined for player methods until the video is loaded to avoid crashes
|
||||
getAudioTracks={videoState.isVideoLoaded ? getAudioTracks : undefined}
|
||||
getSubtitleTracks={
|
||||
videoState.isVideoLoaded ? getSubtitleTracks : undefined
|
||||
}
|
||||
offline={offline}
|
||||
setSubtitleTrack={setSubtitleTrack}
|
||||
setSubtitleURL={setSubtitleURL}
|
||||
setAudioTrack={setAudioTrack}
|
||||
setSubtitleTrack={
|
||||
videoState.isVideoLoaded ? setSubtitleTrack : undefined
|
||||
}
|
||||
setSubtitleURL={videoState.isVideoLoaded ? setSubtitleURL : undefined}
|
||||
setAudioTrack={videoState.isVideoLoaded ? setAudioTrack : undefined}
|
||||
isVlc
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.1.3/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
|
||||
"files": {
|
||||
"includes": [
|
||||
"**/*",
|
||||
|
||||
34
bun.lock
34
bun.lock
@@ -85,7 +85,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@biomejs/biome": "^2.1.3",
|
||||
"@biomejs/biome": "^2.1.4",
|
||||
"@react-native-community/cli": "^19",
|
||||
"@react-native-tvos/config-tv": "^0.1.1",
|
||||
"@types/jest": "^29.5.12",
|
||||
@@ -94,7 +94,7 @@
|
||||
"@types/react-test-renderer": "^19.0.0",
|
||||
"cross-env": "^10.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.2",
|
||||
"lint-staged": "^16.1.5",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"react-test-renderer": "19.1.1",
|
||||
"typescript": "~5.8.3",
|
||||
@@ -297,23 +297,23 @@
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@2.1.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.1.3", "@biomejs/cli-darwin-x64": "2.1.3", "@biomejs/cli-linux-arm64": "2.1.3", "@biomejs/cli-linux-arm64-musl": "2.1.3", "@biomejs/cli-linux-x64": "2.1.3", "@biomejs/cli-linux-x64-musl": "2.1.3", "@biomejs/cli-win32-arm64": "2.1.3", "@biomejs/cli-win32-x64": "2.1.3" }, "bin": { "biome": "bin/biome" } }, "sha512-KE/tegvJIxTkl7gJbGWSgun7G6X/n2M6C35COT6ctYrAy7SiPyNvi6JtoQERVK/VRbttZfgGq96j2bFmhmnH4w=="],
|
||||
"@biomejs/biome": ["@biomejs/biome@2.1.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.1.4", "@biomejs/cli-darwin-x64": "2.1.4", "@biomejs/cli-linux-arm64": "2.1.4", "@biomejs/cli-linux-arm64-musl": "2.1.4", "@biomejs/cli-linux-x64": "2.1.4", "@biomejs/cli-linux-x64-musl": "2.1.4", "@biomejs/cli-win32-arm64": "2.1.4", "@biomejs/cli-win32-x64": "2.1.4" }, "bin": { "biome": "bin/biome" } }, "sha512-QWlrqyxsU0FCebuMnkvBIkxvPqH89afiJzjMl+z67ybutse590jgeaFdDurE9XYtzpjRGTI1tlUZPGWmbKsElA=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.1.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LFLkSWRoSGS1wVUD/BE6Nlt2dSn0ulH3XImzg2O/36BoToJHKXjSxzPEMAqT9QvwVtk7/9AQhZpTneERU9qaXA=="],
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sCrNENE74I9MV090Wq/9Dg7EhPudx3+5OiSoQOkIe3DLPzFARuL1dOwCWhKCpA3I5RHmbrsbNSRfZwCabwd8Qg=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.1.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Q/4OTw8P9No9QeowyxswcWdm0n2MsdCwWcc5NcKQQvzwPjwuPdf8dpPPf4r+x0RWKBtl1FLiAUtJvBlri6DnYw=="],
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-gOEICJbTCy6iruBywBDcG4X5rHMbqCPs3clh3UQ+hRKlgvJTk4NHWQAyHOXvaLe+AxD1/TNX1jbZeffBJzcrOw=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-2hS6LgylRqMFmAZCOFwYrf77QMdUwJp49oe8PX/O8+P2yKZMSpyQTf3Eo5ewnsMFUEmYbPOskafdV1ds1MZMJA=="],
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-juhEkdkKR4nbUi5k/KRp1ocGPNWLgFRD4NrHZSveYrD6i98pyvuzmS9yFYgOZa5JhaVqo0HPnci0+YuzSwT2fw=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-KXouFSBnoxAWZYDQrnNRzZBbt5s9UJkIm40hdvSL9mBxSSoxRFQJbtg1hP3aa8A2SnXyQHxQfpiVeJlczZt76w=="],
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-nYr7H0CyAJPaLupFE2cH16KZmRC5Z9PEftiA2vWxk+CsFkPZQ6dBRdcC6RuS+zJlPc/JOd8xw3uCCt9Pv41WvQ=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-NxlSCBhLvQtWGagEztfAZ4WcE1AkMTntZV65ZvR+J9jp06+EtOYEBPQndA70ZGhHbEDG57bR6uNvqkd1WrEYVA=="],
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Eoy9ycbhpJVYuR+LskV9s3uyaIkp89+qqgqhGQsWnp/I02Uqg2fXFblHJOpGZR8AxdB9ADy87oFVxn9MpFKUrw=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-KaLAxnROouzIWtl6a0Y88r/4hW5oDUJTIqQorOTVQITaKQsKjZX4XCUmHIhdEk8zMnaiLZzRTAwk1yIAl+mIew=="],
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lvwvb2SQQHctHUKvBKptR6PLFCM7JfRjpCCrDaTmvB7EeZ5/dQJPhTYBf36BE/B4CRWR2ZiBLRYhK7hhXBCZAg=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.1.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-V9CUZCtWH4u0YwyCYbQ3W5F4ZGPWp2C2TYcsiWFNNyRfmOW1j/TY/jAurl33SaRjgZPO5UUhGyr9m6BN9t84NQ=="],
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-3WRYte7orvyi6TRfIZkDN9Jzoogbv+gSvR+b9VOXUg1We1XrjBg6WljADeVEaKTvOcpVdH0a90TwyOQ6ue4fGw=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-dxy599q6lgp8ANPpR8sDMscwdp9oOumEsVXuVCVT9N2vAho8uYXlCz53JhxX6LtJOXaE73qzgkGQ7QqvFlMC0g=="],
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-tBc+W7anBPSFXGAoQW+f/+svkpt8/uXfRwDzN1DvnatkRMt16KIYpEi/iw8u9GahJlFv98kgHcIrSsZHZTR0sw=="],
|
||||
|
||||
"@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@0.9.2", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-IZZKllcaqCGsKIgeXmYFGU95IXxbBpXtwKws4Lg2GJw/qqAYYsPFEl0JBvnymSD7G1zkHYEilg5UHuTd0NmX7A=="],
|
||||
|
||||
@@ -1335,9 +1335,9 @@
|
||||
|
||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||
|
||||
"lint-staged": ["lint-staged@16.1.2", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^14.0.0", "debug": "^4.4.1", "lilconfig": "^3.1.3", "listr2": "^8.3.3", "micromatch": "^4.0.8", "nano-spawn": "^1.0.2", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q=="],
|
||||
"lint-staged": ["lint-staged@16.1.5", "", { "dependencies": { "chalk": "^5.5.0", "commander": "^14.0.0", "debug": "^4.4.1", "lilconfig": "^3.1.3", "listr2": "^9.0.1", "micromatch": "^4.0.8", "nano-spawn": "^1.0.2", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-uAeQQwByI6dfV7wpt/gVqg+jAPaSp8WwOA8kKC/dv1qw14oGpnpAisY65ibGHUGDUv0rYaZ8CAJZ/1U8hUvC2A=="],
|
||||
|
||||
"listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="],
|
||||
"listr2": ["listr2@9.0.1", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
@@ -1987,7 +1987,7 @@
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="],
|
||||
"yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
|
||||
|
||||
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
@@ -2081,6 +2081,8 @@
|
||||
|
||||
"@react-native-community/cli-doctor/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"@react-native-community/cli-doctor/yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="],
|
||||
|
||||
"@react-native-community/cli-server-api/pretty-format": ["pretty-format@26.6.2", "", { "dependencies": { "@jest/types": "^26.6.2", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^17.0.1" } }, "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg=="],
|
||||
|
||||
"@react-native-community/cli-tools/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
@@ -2167,7 +2169,7 @@
|
||||
|
||||
"lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"lint-staged/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
||||
"lint-staged/chalk": ["chalk@5.5.0", "", {}, "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg=="],
|
||||
|
||||
"lint-staged/commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="],
|
||||
|
||||
@@ -2201,6 +2203,8 @@
|
||||
|
||||
"postcss-css-variables/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||
|
||||
"postcss-load-config/yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="],
|
||||
|
||||
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@biomejs/biome": "^2.1.3",
|
||||
"@biomejs/biome": "^2.1.4",
|
||||
"@react-native-community/cli": "^19",
|
||||
"@react-native-tvos/config-tv": "^0.1.1",
|
||||
"@types/jest": "^29.5.12",
|
||||
@@ -109,7 +109,7 @@
|
||||
"@types/react-test-renderer": "^19.0.0",
|
||||
"cross-env": "^10.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.2",
|
||||
"lint-staged": "^16.1.5",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"react-test-renderer": "19.1.1",
|
||||
"typescript": "~5.8.3"
|
||||
|
||||
Reference in New Issue
Block a user