This commit is contained in:
Timothy Z. 2026-07-27 12:51:39 +00:00 committed by GitHub
commit 40bc07e2f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 136 additions and 12 deletions

View file

@ -3,9 +3,9 @@
require('spatial-navigation-polyfill');
const React = require('react');
const { useTranslation } = require('react-i18next');
const { useNavigate } = require('react-router');
const { useLocation, useNavigate } = require('react-router');
const { useCore } = require('stremio/core');
const { Routes } = require('stremio-router');
const { Routes, navigateToRoute } = require('stremio-router');
const { Chromecast, ServicesProvider, GamepadProvider } = require('stremio/services');
const { FullscreenProvider, ToastProvider, TooltipProvider, ShortcutsProvider, DiscordProvider, CONSTANTS, useBinaryState, useProfile, withCoreSuspender, onFileDrop, usePlatform } = require('stremio/common');
const ServicesToaster = require('./ServicesToaster');
@ -24,7 +24,10 @@ const App = () => {
const profile = useProfile();
const { i18n } = useTranslation();
const { shell } = usePlatform();
const location = useLocation();
const navigate = useNavigate();
const locationRef = React.useRef(location);
locationRef.current = location;
const [gamepadSupportEnabled, setGamepadSupportEnabled] = React.useState(false);
const services = React.useMemo(() => {
return {
@ -113,7 +116,9 @@ const App = () => {
const transportUrl = `https://${hostname}${pathname}`;
navigate(`/addons?addon=${encodeURIComponent(transportUrl)}`);
} else {
navigate(`${pathname}?${searchParams.toString()}`);
const search = searchParams.toString();
const path = search ? `${pathname}?${search}` : pathname;
navigateToRoute(navigate, locationRef.current, path);
}
}
} catch (e) {
@ -127,7 +132,7 @@ const App = () => {
}
return () => shell.off('open-media', onOpenMedia);
}, [shell.state.initialized]);
}, [shell.state.initialized, navigate]);
React.useEffect(() => {
if (typeof profile.settings?.interfaceLanguage === 'string') {

View file

@ -1,11 +1,12 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const { useNavigate } = require('react-router');
const { useLocation, useNavigate } = require('react-router');
const { withCoreSuspender, useStreamingServer } = require('stremio/common');
const { default: toPath } = require('stremio-router/toPath');
const { navigateToRoute, toPath } = require('stremio-router');
const DeepLinkHandler = () => {
const location = useLocation();
const navigate = useNavigate();
const streamingServer = useStreamingServer();
React.useEffect(() => {
@ -14,11 +15,12 @@ const DeepLinkHandler = () => {
if (type === 'Ready') {
const [, deepLinks] = content;
if (typeof deepLinks.metaDetailsVideos === 'string') {
navigate(toPath(deepLinks.metaDetailsVideos));
const path = toPath(deepLinks.metaDetailsVideos);
navigateToRoute(navigate, location, path);
}
}
}
}, [streamingServer.torrent]);
}, [streamingServer.torrent, location.pathname, location.search, navigate]);
return null;
};

View file

@ -6,6 +6,7 @@ import Modal from './Modal';
import Routes from './Routes';
import Route from './Route';
import toPath from './toPath';
import navigateToRoute from './routePath';
export {
useModalsContainer,
@ -14,4 +15,5 @@ export {
Routes,
Route,
toPath,
navigateToRoute,
};

41
src/router/routePath.ts Normal file
View file

@ -0,0 +1,41 @@
// Copyright (C) 2017-2026 Smart code 203358507
import type { Location, NavigateFunction } from 'react-router-dom';
const decodePathname = (pathname: string): string => {
try {
return decodeURIComponent(pathname);
} catch {
return pathname;
}
};
const normalizeRoutePathname = (pathname: string): string => {
return decodePathname(pathname)
.replace(/^\/metadetails(?=\/|$)/, '/detail');
};
const getPathname = (path: string): string => path.split(/[?#]/)[0];
const getSearch = (path: string): string => {
const searchIndex = path.indexOf('?');
return searchIndex !== -1 ? path.slice(searchIndex) : '';
};
const sameRoutePathname = (location: Location, path: string): boolean => {
return normalizeRoutePathname(location.pathname) === normalizeRoutePathname(getPathname(path));
};
const sameRouteLocation = (location: Location, path: string): boolean => {
return sameRoutePathname(location, path) && location.search === getSearch(path);
};
const navigateToRoute = (navigate: NavigateFunction, location: Location, path: string): void => {
if (!sameRouteLocation(location, path)) {
navigate(path, {
replace: sameRoutePathname(location, path)
});
}
};
export default navigateToRoute;

View file

@ -2,6 +2,7 @@
const React = require('react');
const { useParams, useLocation, useNavigate } = require('react-router');
const { useSearchParams } = require('react-router-dom');
const { useTranslation } = require('react-i18next');
const classnames = require('classnames');
const { useCore } = require('stremio/core');
@ -14,6 +15,7 @@ const VideosList = require('./VideosList');
const useMetaDetails = require('./useMetaDetails');
const useSeason = require('./useSeason');
const useMetaExtensionTabs = require('./useMetaExtensionTabs');
const { default: useExternalPlayerCallback } = require('./useExternalPlayerCallback');
const styles = require('./styles');
const GAMEPAD_HANDLER_ID = 'metadetails';
@ -22,6 +24,7 @@ const MetaDetails = () => {
const { type, id, videoId } = useParams();
const location = useLocation();
const navigate = useNavigate();
const [queryParams] = useSearchParams();
const { getStoredOrigin } = useNavigateWithOrigin();
const contentRef = React.useRef(null);
const { t } = useTranslation();
@ -32,6 +35,7 @@ const MetaDetails = () => {
videoId
}), [type, id, videoId]);
const metaDetails = useMetaDetails(urlParams);
useExternalPlayerCallback(urlParams, queryParams, metaDetails);
const [season, setSeason] = useSeason(urlParams);
const [tabs, metaExtension, clearMetaExtension] = useMetaExtensionTabs(metaDetails.metaExtensions);
const [metaPath, streamPath] = React.useMemo(() => {
@ -52,6 +56,11 @@ const MetaDetails = () => {
:
null;
}, [metaDetails.metaItem, streamPath]);
const externalPlayerCallbackCanMarkWatched = React.useMemo(() => {
return typeof video?.id === 'string' &&
metaDetails.libraryItem?.state?.video_id === video.id &&
metaDetails.libraryItem?.state?.duration > 0;
}, [metaDetails.libraryItem, video]);
const addToLibrary = React.useCallback(() => {
if (metaDetails.metaItem === null || metaDetails.metaItem.content.type !== 'Ready') {
return;
@ -215,6 +224,7 @@ const MetaDetails = () => {
streams={metaDetails.streams}
video={video}
type={streamPath.type}
externalPlayerCallbackCanMarkWatched={externalPlayerCallbackCanMarkWatched}
onEpisodeSearch={handleEpisodeSearch}
/>
:

View file

@ -12,7 +12,7 @@ const { default: useRouteFocused } = require('stremio/common/useRouteFocused');
const StreamPlaceholder = require('./StreamPlaceholder');
const styles = require('./styles');
const Stream = ({ className, videoId, videoReleased, addonName, name, description, thumbnail, progress, deepLinks, ...props }) => {
const Stream = ({ className, videoId, videoReleased, addonName, name, description, thumbnail, progress, deepLinks, externalPlayerCallbackCanMarkWatched, ...props }) => {
const profile = useProfile();
const toast = useToast();
const platform = usePlatform();
@ -118,7 +118,9 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
}
if (profile.settings.playerType !== null) {
markVideoAsWatched();
if (profile.settings.playerType !== 'infuse' || !externalPlayerCallbackCanMarkWatched) {
markVideoAsWatched();
}
toast.show({
type: 'success',
title: 'Stream opened in external player',
@ -129,7 +131,7 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
if (typeof props.onClick === 'function') {
props.onClick(event);
}
}, [props.onClick, profile.settings, markVideoAsWatched]);
}, [props.onClick, profile.settings.playerType, externalPlayerCallbackCanMarkWatched, markVideoAsWatched]);
const copyMagnetLink = React.useCallback((event) => {
event.preventDefault();
@ -321,6 +323,7 @@ Stream.propTypes = {
})
})
}),
externalPlayerCallbackCanMarkWatched: PropTypes.bool,
onClick: PropTypes.func
};

View file

@ -16,7 +16,7 @@ const { default: SeasonEpisodePicker } = require('../EpisodePicker');
const ALL_ADDONS_KEY = 'ALL';
const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
const StreamsList = ({ className, video, type, externalPlayerCallbackCanMarkWatched, onEpisodeSearch, ...props }) => {
const { t } = useTranslation();
const core = useCore();
const platform = usePlatform();
@ -184,6 +184,7 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
thumbnail={stream.thumbnail}
progress={stream.progress}
deepLinks={stream.deepLinks}
externalPlayerCallbackCanMarkWatched={externalPlayerCallbackCanMarkWatched}
onClick={stream.onClick}
/>
))}
@ -219,6 +220,7 @@ StreamsList.propTypes = {
streams: PropTypes.arrayOf(PropTypes.object).isRequired,
video: PropTypes.object,
type: PropTypes.string,
externalPlayerCallbackCanMarkWatched: PropTypes.bool,
onEpisodeSearch: PropTypes.func
};

View file

@ -0,0 +1,59 @@
// Copyright (C) 2017-2026 Smart code 203358507
import React from 'react';
import { useCore } from 'stremio/core';
type UrlParams = {
type?: string,
id?: string,
videoId?: string,
};
const useExternalPlayerCallback = (
urlParams: UrlParams,
searchParams: URLSearchParams,
metaDetails: MetaDetails,
) => {
const core = useCore();
const callbackRef = React.useRef<string | null>(null);
React.useEffect(() => {
const position = searchParams.get('position');
if (position === null || position === '') {
return;
}
const positionNumber = Number(position);
const time = Math.round(positionNumber * 1000);
const streamPath = metaDetails.selected?.streamPath ?? null;
const callbackKey = [
urlParams.type,
urlParams.id,
urlParams.videoId,
time,
searchParams.get('lastPlayedUrl')
].join(':');
if (
!Number.isFinite(positionNumber) ||
positionNumber < 0 ||
streamPath === null ||
metaDetails.libraryItem === null ||
callbackRef.current === callbackKey
) {
return;
}
callbackRef.current = callbackKey;
core.transport.dispatch({
action: 'MetaDetails',
args: {
action: 'ExternalPlayerProgressChanged',
args: {
time
}
}
});
}, [urlParams, searchParams, metaDetails]);
};
export default useExternalPlayerCallback;