From 3b730a2bd816b8f0984cd6b2c7237d1fd8285920 Mon Sep 17 00:00:00 2001
From: Neeraj TK <64883030+GaryGosh@users.noreply.github.com>
Date: Sun, 11 Aug 2024 04:32:23 +0530
Subject: [PATCH 001/154] added shortkey to toggle caption. UX improvement.
---
src/routes/Player/Player.js | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index c7d94d82c..5a4b1cf71 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -515,6 +515,18 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
+ case 'KeyC': {
+ if (!menusOpen && !nextVideoPopupOpen) {
+ if (video.state.selectedSubtitlesTrackId !== null) {
+ onSubtitlesTrackSelected(null);
+ } else if (video.state.subtitlesTracks.length > 0) {
+ onSubtitlesTrackSelected(video.state.subtitlesTracks[0].id);
+ } else if (video.state.extraSubtitlesTracks.length > 0) {
+ onExtraSubtitlesTrackSelected(video.state.extraSubtitlesTracks[0].id);
+ }
+ }
+ break;
+ }
case 'KeyI': {
closeMenus();
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
From d2db62f33a4e3be6fdb1b4b9f8cab2c9fd18bad6 Mon Sep 17 00:00:00 2001
From: Neeraj TK <64883030+GaryGosh@users.noreply.github.com>
Date: Mon, 23 Sep 2024 20:05:36 +0530
Subject: [PATCH 002/154] retain last selected subtitle upon toggling
---
src/routes/Player/Player.js | 1796 +++++++++++++++++++----------------
1 file changed, 999 insertions(+), 797 deletions(-)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index 5a4b1cf71..894a9b466 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -1,812 +1,1014 @@
// Copyright (C) 2017-2023 Smart code 203358507
-const React = require('react');
-const PropTypes = require('prop-types');
-const classnames = require('classnames');
-const debounce = require('lodash.debounce');
-const langs = require('langs');
-const { useTranslation } = require('react-i18next');
-const { useRouteFocused } = require('stremio-router');
-const { useServices } = require('stremio/services');
-const { HorizontalNavBar, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common');
-const BufferingLoader = require('./BufferingLoader');
-const VolumeChangeIndicator = require('./VolumeChangeIndicator');
-const Error = require('./Error');
-const ControlBar = require('./ControlBar');
-const NextVideoPopup = require('./NextVideoPopup');
-const StatisticsMenu = require('./StatisticsMenu');
-const InfoMenu = require('./InfoMenu');
-const OptionsMenu = require('./OptionsMenu');
-const VideosMenu = require('./VideosMenu');
-const SubtitlesMenu = require('./SubtitlesMenu');
-const SpeedMenu = require('./SpeedMenu');
-const usePlayer = require('./usePlayer');
-const useSettings = require('./useSettings');
-const useStatistics = require('./useStatistics');
-const useVideo = require('./useVideo');
-const styles = require('./styles');
-const Video = require('./Video');
+const React = require("react");
+const PropTypes = require("prop-types");
+const classnames = require("classnames");
+const debounce = require("lodash.debounce");
+const langs = require("langs");
+const { useTranslation } = require("react-i18next");
+const { useRouteFocused } = require("stremio-router");
+const { useServices } = require("stremio/services");
+const {
+ HorizontalNavBar,
+ useFullscreen,
+ useBinaryState,
+ useToast,
+ useStreamingServer,
+ withCoreSuspender,
+} = require("stremio/common");
+const BufferingLoader = require("./BufferingLoader");
+const VolumeChangeIndicator = require("./VolumeChangeIndicator");
+const Error = require("./Error");
+const ControlBar = require("./ControlBar");
+const NextVideoPopup = require("./NextVideoPopup");
+const StatisticsMenu = require("./StatisticsMenu");
+const InfoMenu = require("./InfoMenu");
+const OptionsMenu = require("./OptionsMenu");
+const VideosMenu = require("./VideosMenu");
+const SubtitlesMenu = require("./SubtitlesMenu");
+const SpeedMenu = require("./SpeedMenu");
+const usePlayer = require("./usePlayer");
+const useSettings = require("./useSettings");
+const useStatistics = require("./useStatistics");
+const useVideo = require("./useVideo");
+const styles = require("./styles");
+const Video = require("./Video");
const Player = ({ urlParams, queryParams }) => {
- const { t } = useTranslation();
- const { chromecast, shell, core } = useServices();
- const forceTranscoding = React.useMemo(() => {
- return queryParams.has('forceTranscoding');
- }, [queryParams]);
-
- const [player, videoParamsChanged, timeChanged, pausedChanged, ended, nextVideo] = usePlayer(urlParams);
- const [settings, updateSettings] = useSettings();
- const streamingServer = useStreamingServer();
- const statistics = useStatistics(player, streamingServer);
- const video = useVideo();
- const routeFocused = useRouteFocused();
- const toast = useToast();
-
- const [casting, setCasting] = React.useState(() => {
- return chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED;
- });
-
- const [immersed, setImmersed] = React.useState(true);
- const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
- const [, , , toggleFullscreen] = useFullscreen();
-
- const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false);
- const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
- const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
- const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
- const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false);
- const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] = useBinaryState(false);
- const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
-
- const menusOpen = React.useMemo(() => {
- return optionsMenuOpen || subtitlesMenuOpen || infoMenuOpen || speedMenuOpen || videosMenuOpen || statisticsMenuOpen;
- }, [optionsMenuOpen, subtitlesMenuOpen, infoMenuOpen, speedMenuOpen, videosMenuOpen, statisticsMenuOpen]);
-
- const closeMenus = React.useCallback(() => {
- closeOptionsMenu();
- closeSubtitlesMenu();
- closeInfoMenu();
- closeSpeedMenu();
- closeVideosMenu();
- closeStatisticsMenu();
- }, []);
-
- const overlayHidden = React.useMemo(() => {
- return immersed && !casting && video.state.paused !== null && !video.state.paused && !menusOpen && !nextVideoPopupOpen;
- }, [immersed, casting, video.state.paused, menusOpen, nextVideoPopupOpen]);
-
- const nextVideoPopupDismissed = React.useRef(false);
- const defaultSubtitlesSelected = React.useRef(false);
- const defaultAudioTrackSelected = React.useRef(false);
- const [error, setError] = React.useState(null);
-
- const onImplementationChanged = React.useCallback(() => {
- video.setProp('subtitlesSize', settings.subtitlesSize);
- video.setProp('subtitlesOffset', settings.subtitlesOffset);
- video.setProp('subtitlesTextColor', settings.subtitlesTextColor);
- video.setProp('subtitlesBackgroundColor', settings.subtitlesBackgroundColor);
- video.setProp('subtitlesOutlineColor', settings.subtitlesOutlineColor);
- video.setProp('extraSubtitlesSize', settings.subtitlesSize);
- video.setProp('extraSubtitlesOffset', settings.subtitlesOffset);
- video.setProp('extraSubtitlesTextColor', settings.subtitlesTextColor);
- video.setProp('extraSubtitlesBackgroundColor', settings.subtitlesBackgroundColor);
- video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor);
- }, [settings.subtitlesSize, settings.subtitlesOffset, settings.subtitlesTextColor, settings.subtitlesBackgroundColor, settings.subtitlesOutlineColor]);
-
- const onEnded = React.useCallback(() => {
- ended();
- if (player.nextVideo !== null) {
- onNextVideoRequested();
- } else {
- window.history.back();
- }
- }, [player.nextVideo, onNextVideoRequested]);
-
- const onError = React.useCallback((error) => {
- console.error('Player', error);
- if (error.critical) {
- setError(error);
- } else {
- toast.show({
- type: 'error',
- title: t('ERROR'),
- message: error.message,
- timeout: 3000
- });
- }
- }, []);
-
- const onSubtitlesTrackLoaded = React.useCallback(() => {
- toast.show({
- type: 'success',
- title: t('PLAYER_SUBTITLES_LOADED'),
- message: t('PLAYER_SUBTITLES_LOADED_EMBEDDED'),
- timeout: 3000
- });
- }, []);
-
- const onExtraSubtitlesTrackLoaded = React.useCallback((track) => {
- toast.show({
- type: 'success',
- title: t('PLAYER_SUBTITLES_LOADED'),
- message: track.exclusive ? t('PLAYER_SUBTITLES_LOADED_EXCLUSIVE') : t('PLAYER_SUBTITLES_LOADED_ORIGIN', { origin: track.origin }),
- timeout: 3000
- });
- }, []);
-
- const onPlayRequested = React.useCallback(() => {
- video.setProp('paused', false);
- }, []);
-
- const onPlayRequestedDebounced = React.useCallback(debounce(onPlayRequested, 200), []);
-
- const onPauseRequested = React.useCallback(() => {
- video.setProp('paused', true);
- }, []);
-
- const onPauseRequestedDebounced = React.useCallback(debounce(onPauseRequested, 200), []);
- const onMuteRequested = React.useCallback(() => {
- video.setProp('muted', true);
- }, []);
-
- const onUnmuteRequested = React.useCallback(() => {
- video.setProp('muted', false);
- }, []);
-
- const onVolumeChangeRequested = React.useCallback((volume) => {
- video.setProp('volume', volume);
- }, []);
-
- const onSeekRequested = React.useCallback((time) => {
- video.setProp('time', time);
- }, []);
-
- const onPlaybackSpeedChanged = React.useCallback((rate) => {
- video.setProp('playbackSpeed', rate);
- }, []);
-
- const onSubtitlesTrackSelected = React.useCallback((id) => {
- video.setProp('selectedSubtitlesTrackId', id);
- video.setProp('selectedExtraSubtitlesTrackId', null);
- }, []);
-
- const onExtraSubtitlesTrackSelected = React.useCallback((id) => {
- video.setProp('selectedSubtitlesTrackId', null);
- video.setProp('selectedExtraSubtitlesTrackId', id);
- }, []);
-
- const onAudioTrackSelected = React.useCallback((id) => {
- video.setProp('selectedAudioTrackId', id);
- }, []);
-
- const onExtraSubtitlesDelayChanged = React.useCallback((delay) => {
- video.setProp('extraSubtitlesDelay', delay);
- }, []);
-
- const onSubtitlesSizeChanged = React.useCallback((size) => {
- updateSettings({ subtitlesSize: size });
- }, [updateSettings]);
-
- const onSubtitlesOffsetChanged = React.useCallback((offset) => {
- updateSettings({ subtitlesOffset: offset });
- }, [updateSettings]);
-
- const onDismissNextVideoPopup = React.useCallback(() => {
- closeNextVideoPopup();
- nextVideoPopupDismissed.current = true;
- }, []);
-
- const onNextVideoRequested = React.useCallback(() => {
- if (player.nextVideo !== null) {
- nextVideo();
-
- const deepLinks = player.nextVideo.deepLinks;
- if (deepLinks.metaDetailsStreams && deepLinks.player) {
- window.location.replace(deepLinks.metaDetailsStreams);
- window.location.href = deepLinks.player;
- } else {
- window.location.replace(deepLinks.player ?? deepLinks.metaDetailsStreams);
- }
- }
- }, [player.nextVideo]);
-
- const onVideoClick = React.useCallback(() => {
- if (video.state.paused !== null) {
- if (video.state.paused) {
- onPlayRequestedDebounced();
- } else {
- onPauseRequestedDebounced();
- }
- }
- }, [video.state.paused]);
-
- const onVideoDoubleClick = React.useCallback(() => {
- onPlayRequestedDebounced.cancel();
- onPauseRequestedDebounced.cancel();
- toggleFullscreen();
- }, [toggleFullscreen]);
-
- const onContainerMouseDown = React.useCallback((event) => {
- if (!event.nativeEvent.optionsMenuClosePrevented) {
- closeOptionsMenu();
- }
- if (!event.nativeEvent.subtitlesMenuClosePrevented) {
- closeSubtitlesMenu();
- }
- if (!event.nativeEvent.infoMenuClosePrevented) {
- closeInfoMenu();
- }
- if (!event.nativeEvent.speedMenuClosePrevented) {
- closeSpeedMenu();
- }
- if (!event.nativeEvent.videosMenuClosePrevented) {
- closeVideosMenu();
- }
- if (!event.nativeEvent.statisticsMenuClosePrevented) {
- closeStatisticsMenu();
- }
- }, []);
-
- const onContainerMouseMove = React.useCallback((event) => {
- setImmersed(false);
- if (!event.nativeEvent.immersePrevented) {
- setImmersedDebounced(true);
- } else {
- setImmersedDebounced.cancel();
- }
- }, []);
-
- const onContainerMouseLeave = React.useCallback(() => {
- setImmersedDebounced.cancel();
- setImmersed(true);
- }, []);
-
- const onBarMouseMove = React.useCallback((event) => {
- event.nativeEvent.immersePrevented = true;
- }, []);
-
- React.useEffect(() => {
- setError(null);
- if (player.selected === null) {
- video.unload();
- } else if (streamingServer.settings !== null && streamingServer.settings.type !== 'Loading' &&
- (player.selected.metaRequest === null || (player.metaItem !== null && player.metaItem.type !== 'Loading'))) {
- video.load({
- stream: {
- ...player.selected.stream,
- subtitles: Array.isArray(player.selected.stream.subtitles) ?
- player.selected.stream.subtitles.map((subtitles) => ({
- ...subtitles,
- label: subtitles.url
- }))
- :
- []
- },
- autoplay: true,
- time: player.libraryItem !== null &&
- player.selected.streamRequest !== null &&
- player.selected.streamRequest.path !== null &&
- player.libraryItem.state.video_id === player.selected.streamRequest.path.id ?
- player.libraryItem.state.timeOffset
- :
- 0,
- forceTranscoding: forceTranscoding || casting,
- maxAudioChannels: settings.surroundSound ? 32 : 2,
- streamingServerURL: streamingServer.baseUrl ?
- casting ?
- streamingServer.baseUrl
- :
- streamingServer.selected.transportUrl
- :
- null,
- seriesInfo: player.seriesInfo
- }, {
- chromecastTransport: chromecast.active ? chromecast.transport : null,
- shellTransport: shell.active ? shell.transport : null,
- });
- }
- }, [streamingServer.baseUrl, player.selected, player.metaItem, forceTranscoding, casting]);
- React.useEffect(() => {
- if (video.state.stream !== null) {
- const tracks = player.subtitles.map((subtitles) => ({
- ...subtitles,
- label: subtitles.url
- }));
- video.addExtraSubtitlesTracks(tracks);
- }
- }, [player.subtitles, video.state.stream]);
-
- React.useEffect(() => {
- video.setProp('subtitlesSize', settings.subtitlesSize);
- video.setProp('extraSubtitlesSize', settings.subtitlesSize);
- }, [settings.subtitlesSize]);
-
- React.useEffect(() => {
- video.setProp('subtitlesOffset', settings.subtitlesOffset);
- video.setProp('extraSubtitlesOffset', settings.subtitlesOffset);
- }, [settings.subtitlesOffset]);
-
- React.useEffect(() => {
- video.setProp('subtitlesTextColor', settings.subtitlesTextColor);
- video.setProp('extraSubtitlesTextColor', settings.subtitlesTextColor);
- }, [settings.subtitlesTextColor]);
-
- React.useEffect(() => {
- video.setProp('subtitlesBackgroundColor', settings.subtitlesBackgroundColor);
- video.setProp('extraSubtitlesBackgroundColor', settings.subtitlesBackgroundColor);
- }, [settings.subtitlesBackgroundColor]);
-
- React.useEffect(() => {
- video.setProp('subtitlesOutlineColor', settings.subtitlesOutlineColor);
- video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor);
- }, [settings.subtitlesOutlineColor]);
-
- React.useEffect(() => {
- if (video.state.time !== null && !isNaN(video.state.time) &&
- video.state.duration !== null && !isNaN(video.state.duration) &&
- video.state.manifest !== null && typeof video.state.manifest.name === 'string') {
- timeChanged(video.state.time, video.state.duration, video.state.manifest.name);
- }
- }, [video.state.time, video.state.duration, video.state.manifest]);
-
- React.useEffect(() => {
- if (video.state.paused !== null) {
- pausedChanged(video.state.paused);
- }
- }, [video.state.paused]);
-
- React.useEffect(() => {
- videoParamsChanged(video.state.videoParams);
- }, [video.state.videoParams]);
-
- React.useEffect(() => {
- if (!!settings.bingeWatching && player.nextVideo !== null && !nextVideoPopupDismissed.current) {
- if (video.state.time !== null && video.state.duration !== null && video.state.time < video.state.duration && (video.state.duration - video.state.time) <= settings.nextVideoNotificationDuration) {
- openNextVideoPopup();
- } else {
- closeNextVideoPopup();
- }
- }
- }, [player.nextVideo, video.state.time, video.state.duration]);
-
- React.useEffect(() => {
- if (!defaultSubtitlesSelected.current) {
- const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
-
- const subtitlesTrack = findTrackByLang(video.state.subtitlesTracks, settings.subtitlesLanguage);
- const extraSubtitlesTrack = findTrackByLang(video.state.extraSubtitlesTracks, settings.subtitlesLanguage);
-
- if (subtitlesTrack && subtitlesTrack.id) {
- onSubtitlesTrackSelected(subtitlesTrack.id);
- defaultSubtitlesSelected.current = true;
- } else if (extraSubtitlesTrack && extraSubtitlesTrack.id) {
- onExtraSubtitlesTrackSelected(extraSubtitlesTrack.id);
- defaultSubtitlesSelected.current = true;
- }
- }
- }, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
-
- React.useEffect(() => {
- if (!defaultAudioTrackSelected.current) {
- const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
- const audioTrack = findTrackByLang(video.state.audioTracks, settings.audioLanguage);
-
- if (audioTrack && audioTrack.id) {
- onAudioTrackSelected(audioTrack.id);
- defaultAudioTrackSelected.current = true;
- }
- }
- }, [video.state.audioTracks]);
-
- React.useEffect(() => {
- defaultSubtitlesSelected.current = false;
- defaultAudioTrackSelected.current = false;
- nextVideoPopupDismissed.current = false;
- }, [video.state.stream]);
-
- React.useEffect(() => {
- if ((!Array.isArray(video.state.subtitlesTracks) || video.state.subtitlesTracks.length === 0) &&
- (!Array.isArray(video.state.extraSubtitlesTracks) || video.state.extraSubtitlesTracks.length === 0) &&
- (!Array.isArray(video.state.audioTracks) || video.state.audioTracks.length === 0)) {
- closeSubtitlesMenu();
- }
- }, [video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
-
- React.useEffect(() => {
- if (player.metaItem === null || player.metaItem.type !== 'Ready') {
- closeInfoMenu();
- closeVideosMenu();
- }
- }, [player.metaItem]);
-
- React.useEffect(() => {
- if (video.state.playbackSpeed === null) {
- closeSpeedMenu();
- }
- }, [video.state.playbackSpeed]);
-
- React.useEffect(() => {
- const toastFilter = (item) => item?.dataset?.type === 'CoreEvent';
- toast.addFilter(toastFilter);
- const onCastStateChange = () => {
- setCasting(chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED);
- };
- const onChromecastServiceStateChange = () => {
- onCastStateChange();
- if (chromecast.active) {
- chromecast.transport.on(
- cast.framework.CastContextEventType.CAST_STATE_CHANGED,
- onCastStateChange
- );
- }
- };
- const onCoreEvent = ({ event }) => {
- if (event === 'PlayingOnDevice') {
- onPauseRequested();
- }
- };
- chromecast.on('stateChanged', onChromecastServiceStateChange);
- core.transport.on('CoreEvent', onCoreEvent);
- onChromecastServiceStateChange();
- return () => {
- toast.removeFilter(toastFilter);
- chromecast.off('stateChanged', onChromecastServiceStateChange);
- core.transport.off('CoreEvent', onCoreEvent);
- if (chromecast.active) {
- chromecast.transport.off(
- cast.framework.CastContextEventType.CAST_STATE_CHANGED,
- onCastStateChange
- );
- }
- };
- }, []);
-
- React.useLayoutEffect(() => {
- const onKeyDown = (event) => {
- switch (event.code) {
- case 'Space': {
- if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) {
- if (video.state.paused) {
- onPlayRequested();
- } else {
- onPauseRequested();
- }
- }
-
- break;
- }
- case 'ArrowRight': {
- if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
- const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
- onSeekRequested(video.state.time + seekDuration);
- }
-
- break;
- }
- case 'ArrowLeft': {
- if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
- const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
- onSeekRequested(video.state.time - seekDuration);
- }
-
- break;
- }
- case 'ArrowUp': {
- if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
- onVolumeChangeRequested(video.state.volume + 5);
- }
-
- break;
- }
- case 'ArrowDown': {
- if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
- onVolumeChangeRequested(video.state.volume - 5);
- }
-
- break;
- }
- case 'KeyS': {
- closeMenus();
- if ((Array.isArray(video.state.subtitlesTracks) && video.state.subtitlesTracks.length > 0) ||
- (Array.isArray(video.state.extraSubtitlesTracks) && video.state.extraSubtitlesTracks.length > 0) ||
- (Array.isArray(video.state.audioTracks) && video.state.audioTracks.length > 0)) {
- toggleSubtitlesMenu();
- }
-
- break;
- }
- case 'KeyC': {
- if (!menusOpen && !nextVideoPopupOpen) {
- if (video.state.selectedSubtitlesTrackId !== null) {
- onSubtitlesTrackSelected(null);
- } else if (video.state.subtitlesTracks.length > 0) {
- onSubtitlesTrackSelected(video.state.subtitlesTracks[0].id);
- } else if (video.state.extraSubtitlesTracks.length > 0) {
- onExtraSubtitlesTrackSelected(video.state.extraSubtitlesTracks[0].id);
- }
- }
- break;
- }
- case 'KeyI': {
- closeMenus();
- if (player.metaItem !== null && player.metaItem.type === 'Ready') {
- toggleInfoMenu();
- }
-
- break;
- }
- case 'KeyR': {
- closeMenus();
- if (video.state.playbackSpeed !== null) {
- toggleSpeedMenu();
- }
-
- break;
- }
- case 'KeyV': {
- closeMenus();
- if (player.metaItem !== null && player.metaItem.type === 'Ready' && player.metaItem?.content?.videos?.length > 0) {
- toggleVideosMenu();
- }
-
- break;
- }
- case 'KeyD': {
- closeMenus();
- if (streamingServer.statistics !== null && streamingServer.statistics.type !== 'Err' && player.selected && typeof player.selected.stream.infoHash === 'string' && typeof player.selected.stream.fileIdx === 'number') {
- toggleStatisticsMenu();
- }
-
- break;
- }
- case 'Escape': {
- closeMenus();
- break;
- }
- }
- };
- const onWheel = ({ deltaY }) => {
- if (deltaY > 0) {
- if (!menusOpen && video.state.volume !== null) {
- onVolumeChangeRequested(video.state.volume - 5);
- }
- } else {
- if (!menusOpen && video.state.volume !== null) {
- onVolumeChangeRequested(video.state.volume + 5);
- }
- }
- };
- if (routeFocused) {
- window.addEventListener('keydown', onKeyDown);
- window.addEventListener('wheel', onWheel);
- }
- return () => {
- window.removeEventListener('keydown', onKeyDown);
- window.removeEventListener('wheel', onWheel);
- };
- }, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, settings.seekShortTimeDuration, routeFocused, menusOpen, nextVideoPopupOpen, video.state.paused, video.state.time, video.state.volume, video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, video.state.playbackSpeed, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu, toggleStatisticsMenu]);
-
- React.useEffect(() => {
- video.events.on('error', onError);
- video.events.on('ended', onEnded);
- video.events.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
- video.events.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
- video.events.on('implementationChanged', onImplementationChanged);
-
- return () => {
- video.events.off('error', onError);
- video.events.off('ended', onEnded);
- video.events.off('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
- video.events.off('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
- video.events.off('implementationChanged', onImplementationChanged);
- };
- }, []);
-
- React.useLayoutEffect(() => {
- return () => {
- setImmersedDebounced.cancel();
- onPlayRequestedDebounced.cancel();
- onPauseRequestedDebounced.cancel();
- };
- }, []);
-
+ const { t } = useTranslation();
+ const { chromecast, shell, core } = useServices();
+ const forceTranscoding = React.useMemo(() => {
+ return queryParams.has("forceTranscoding");
+ }, [queryParams]);
+
+ const [
+ player,
+ videoParamsChanged,
+ timeChanged,
+ pausedChanged,
+ ended,
+ nextVideo,
+ ] = usePlayer(urlParams);
+ const [settings, updateSettings] = useSettings();
+ const streamingServer = useStreamingServer();
+ const statistics = useStatistics(player, streamingServer);
+ const video = useVideo();
+ const routeFocused = useRouteFocused();
+ const toast = useToast();
+
+ const [casting, setCasting] = React.useState(() => {
return (
-
-
- {
- !video.state.loaded ?
-
-

-
- :
- null
- }
- {
- (video.state.buffering || !video.state.loaded) && !error ?
-
- :
- null
- }
- {
- error !== null ?
-
- :
- null
- }
- {
- menusOpen ?
-
- :
- null
- }
- {
- video.state.volume !== null && overlayHidden ?
-
- :
- null
- }
-
-
- {
- nextVideoPopupOpen ?
-
- :
- null
- }
- {
- statisticsMenuOpen ?
-
- :
- null
- }
- {
- subtitlesMenuOpen ?
-
- :
- null
- }
- {
- infoMenuOpen ?
-
- :
- null
- }
- {
- speedMenuOpen ?
-
- :
- null
- }
- {
- videosMenuOpen ?
-
- :
- null
- }
- {
- optionsMenuOpen ?
-
- :
- null
- }
-
+ chromecast.active &&
+ chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED
);
+ });
+
+ const [immersed, setImmersed] = React.useState(true);
+ const setImmersedDebounced = React.useCallback(
+ debounce(setImmersed, 3000),
+ []
+ );
+ const [, , , toggleFullscreen] = useFullscreen();
+
+ const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] =
+ useBinaryState(false);
+ const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] =
+ useBinaryState(false);
+ const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
+ const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] =
+ useBinaryState(false);
+ const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] =
+ useBinaryState(false);
+ const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] =
+ useBinaryState(false);
+ const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] =
+ useBinaryState(false);
+
+ const menusOpen = React.useMemo(() => {
+ return (
+ optionsMenuOpen ||
+ subtitlesMenuOpen ||
+ infoMenuOpen ||
+ speedMenuOpen ||
+ videosMenuOpen ||
+ statisticsMenuOpen
+ );
+ }, [
+ optionsMenuOpen,
+ subtitlesMenuOpen,
+ infoMenuOpen,
+ speedMenuOpen,
+ videosMenuOpen,
+ statisticsMenuOpen,
+ ]);
+
+ const closeMenus = React.useCallback(() => {
+ closeOptionsMenu();
+ closeSubtitlesMenu();
+ closeInfoMenu();
+ closeSpeedMenu();
+ closeVideosMenu();
+ closeStatisticsMenu();
+ }, []);
+
+ const overlayHidden = React.useMemo(() => {
+ return (
+ immersed &&
+ !casting &&
+ video.state.paused !== null &&
+ !video.state.paused &&
+ !menusOpen &&
+ !nextVideoPopupOpen
+ );
+ }, [immersed, casting, video.state.paused, menusOpen, nextVideoPopupOpen]);
+
+ const nextVideoPopupDismissed = React.useRef(false);
+ const defaultSubtitlesSelected = React.useRef(false);
+ const defaultAudioTrackSelected = React.useRef(false);
+ const [error, setError] = React.useState(null);
+ const [previousSubtitlesTrackId, setPreviousSubtitlesTrackId] =
+ React.useState(null);
+
+ const onImplementationChanged = React.useCallback(() => {
+ video.setProp("subtitlesSize", settings.subtitlesSize);
+ video.setProp("subtitlesOffset", settings.subtitlesOffset);
+ video.setProp("subtitlesTextColor", settings.subtitlesTextColor);
+ video.setProp(
+ "subtitlesBackgroundColor",
+ settings.subtitlesBackgroundColor
+ );
+ video.setProp("subtitlesOutlineColor", settings.subtitlesOutlineColor);
+ video.setProp("extraSubtitlesSize", settings.subtitlesSize);
+ video.setProp("extraSubtitlesOffset", settings.subtitlesOffset);
+ video.setProp("extraSubtitlesTextColor", settings.subtitlesTextColor);
+ video.setProp(
+ "extraSubtitlesBackgroundColor",
+ settings.subtitlesBackgroundColor
+ );
+ video.setProp("extraSubtitlesOutlineColor", settings.subtitlesOutlineColor);
+ }, [
+ settings.subtitlesSize,
+ settings.subtitlesOffset,
+ settings.subtitlesTextColor,
+ settings.subtitlesBackgroundColor,
+ settings.subtitlesOutlineColor,
+ ]);
+
+ const onEnded = React.useCallback(() => {
+ ended();
+ if (player.nextVideo !== null) {
+ onNextVideoRequested();
+ } else {
+ window.history.back();
+ }
+ }, [player.nextVideo, onNextVideoRequested]);
+
+ const onError = React.useCallback((error) => {
+ console.error("Player", error);
+ if (error.critical) {
+ setError(error);
+ } else {
+ toast.show({
+ type: "error",
+ title: t("ERROR"),
+ message: error.message,
+ timeout: 3000,
+ });
+ }
+ }, []);
+
+ const onSubtitlesTrackLoaded = React.useCallback(() => {
+ toast.show({
+ type: "success",
+ title: t("PLAYER_SUBTITLES_LOADED"),
+ message: t("PLAYER_SUBTITLES_LOADED_EMBEDDED"),
+ timeout: 3000,
+ });
+ }, []);
+
+ const onExtraSubtitlesTrackLoaded = React.useCallback((track) => {
+ toast.show({
+ type: "success",
+ title: t("PLAYER_SUBTITLES_LOADED"),
+ message: track.exclusive
+ ? t("PLAYER_SUBTITLES_LOADED_EXCLUSIVE")
+ : t("PLAYER_SUBTITLES_LOADED_ORIGIN", { origin: track.origin }),
+ timeout: 3000,
+ });
+ }, []);
+
+ const onPlayRequested = React.useCallback(() => {
+ video.setProp("paused", false);
+ }, []);
+
+ const onPlayRequestedDebounced = React.useCallback(
+ debounce(onPlayRequested, 200),
+ []
+ );
+
+ const onPauseRequested = React.useCallback(() => {
+ video.setProp("paused", true);
+ }, []);
+
+ const onPauseRequestedDebounced = React.useCallback(
+ debounce(onPauseRequested, 200),
+ []
+ );
+ const onMuteRequested = React.useCallback(() => {
+ video.setProp("muted", true);
+ }, []);
+
+ const onUnmuteRequested = React.useCallback(() => {
+ video.setProp("muted", false);
+ }, []);
+
+ const onVolumeChangeRequested = React.useCallback((volume) => {
+ video.setProp("volume", volume);
+ }, []);
+
+ const onSeekRequested = React.useCallback((time) => {
+ video.setProp("time", time);
+ }, []);
+
+ const onPlaybackSpeedChanged = React.useCallback((rate) => {
+ video.setProp("playbackSpeed", rate);
+ }, []);
+
+ const onSubtitlesTrackSelected = React.useCallback((id) => {
+ video.setProp("selectedSubtitlesTrackId", id);
+ video.setProp("selectedExtraSubtitlesTrackId", null);
+ }, []);
+
+ const onExtraSubtitlesTrackSelected = React.useCallback((id) => {
+ video.setProp("selectedSubtitlesTrackId", null);
+ video.setProp("selectedExtraSubtitlesTrackId", id);
+ }, []);
+
+ const onAudioTrackSelected = React.useCallback((id) => {
+ video.setProp("selectedAudioTrackId", id);
+ }, []);
+
+ const onExtraSubtitlesDelayChanged = React.useCallback((delay) => {
+ video.setProp("extraSubtitlesDelay", delay);
+ }, []);
+
+ const onSubtitlesTrackSelected = React.useCallback((id) => {
+ video.setProp("selectedSubtitlesTrackId", id);
+ video.setProp("selectedExtraSubtitlesTrackId", null);
+ setPreviousSubtitlesTrackId(id);
+ }, []);
+
+ const onSubtitlesSizeChanged = React.useCallback(
+ (size) => {
+ updateSettings({ subtitlesSize: size });
+ },
+ [updateSettings]
+ );
+
+ const onSubtitlesOffsetChanged = React.useCallback(
+ (offset) => {
+ updateSettings({ subtitlesOffset: offset });
+ },
+ [updateSettings]
+ );
+
+ const onDismissNextVideoPopup = React.useCallback(() => {
+ closeNextVideoPopup();
+ nextVideoPopupDismissed.current = true;
+ }, []);
+
+ const onNextVideoRequested = React.useCallback(() => {
+ if (player.nextVideo !== null) {
+ nextVideo();
+
+ const deepLinks = player.nextVideo.deepLinks;
+ if (deepLinks.metaDetailsStreams && deepLinks.player) {
+ window.location.replace(deepLinks.metaDetailsStreams);
+ window.location.href = deepLinks.player;
+ } else {
+ window.location.replace(
+ deepLinks.player ?? deepLinks.metaDetailsStreams
+ );
+ }
+ }
+ }, [player.nextVideo]);
+
+ const onVideoClick = React.useCallback(() => {
+ if (video.state.paused !== null) {
+ if (video.state.paused) {
+ onPlayRequestedDebounced();
+ } else {
+ onPauseRequestedDebounced();
+ }
+ }
+ }, [video.state.paused]);
+
+ const onVideoDoubleClick = React.useCallback(() => {
+ onPlayRequestedDebounced.cancel();
+ onPauseRequestedDebounced.cancel();
+ toggleFullscreen();
+ }, [toggleFullscreen]);
+
+ const onContainerMouseDown = React.useCallback((event) => {
+ if (!event.nativeEvent.optionsMenuClosePrevented) {
+ closeOptionsMenu();
+ }
+ if (!event.nativeEvent.subtitlesMenuClosePrevented) {
+ closeSubtitlesMenu();
+ }
+ if (!event.nativeEvent.infoMenuClosePrevented) {
+ closeInfoMenu();
+ }
+ if (!event.nativeEvent.speedMenuClosePrevented) {
+ closeSpeedMenu();
+ }
+ if (!event.nativeEvent.videosMenuClosePrevented) {
+ closeVideosMenu();
+ }
+ if (!event.nativeEvent.statisticsMenuClosePrevented) {
+ closeStatisticsMenu();
+ }
+ }, []);
+
+ const onContainerMouseMove = React.useCallback((event) => {
+ setImmersed(false);
+ if (!event.nativeEvent.immersePrevented) {
+ setImmersedDebounced(true);
+ } else {
+ setImmersedDebounced.cancel();
+ }
+ }, []);
+
+ const onContainerMouseLeave = React.useCallback(() => {
+ setImmersedDebounced.cancel();
+ setImmersed(true);
+ }, []);
+
+ const onBarMouseMove = React.useCallback((event) => {
+ event.nativeEvent.immersePrevented = true;
+ }, []);
+
+ React.useEffect(() => {
+ setError(null);
+ if (player.selected === null) {
+ video.unload();
+ } else if (
+ streamingServer.settings !== null &&
+ streamingServer.settings.type !== "Loading" &&
+ (player.selected.metaRequest === null ||
+ (player.metaItem !== null && player.metaItem.type !== "Loading"))
+ ) {
+ video.load(
+ {
+ stream: {
+ ...player.selected.stream,
+ subtitles: Array.isArray(player.selected.stream.subtitles)
+ ? player.selected.stream.subtitles.map((subtitles) => ({
+ ...subtitles,
+ label: subtitles.url,
+ }))
+ : [],
+ },
+ autoplay: true,
+ time:
+ player.libraryItem !== null &&
+ player.selected.streamRequest !== null &&
+ player.selected.streamRequest.path !== null &&
+ player.libraryItem.state.video_id ===
+ player.selected.streamRequest.path.id
+ ? player.libraryItem.state.timeOffset
+ : 0,
+ forceTranscoding: forceTranscoding || casting,
+ maxAudioChannels: settings.surroundSound ? 32 : 2,
+ streamingServerURL: streamingServer.baseUrl
+ ? casting
+ ? streamingServer.baseUrl
+ : streamingServer.selected.transportUrl
+ : null,
+ seriesInfo: player.seriesInfo,
+ },
+ {
+ chromecastTransport: chromecast.active ? chromecast.transport : null,
+ shellTransport: shell.active ? shell.transport : null,
+ }
+ );
+ }
+ }, [
+ streamingServer.baseUrl,
+ player.selected,
+ player.metaItem,
+ forceTranscoding,
+ casting,
+ ]);
+ React.useEffect(() => {
+ if (video.state.stream !== null) {
+ const tracks = player.subtitles.map((subtitles) => ({
+ ...subtitles,
+ label: subtitles.url,
+ }));
+ video.addExtraSubtitlesTracks(tracks);
+ }
+ }, [player.subtitles, video.state.stream]);
+
+ React.useEffect(() => {
+ video.setProp("subtitlesSize", settings.subtitlesSize);
+ video.setProp("extraSubtitlesSize", settings.subtitlesSize);
+ }, [settings.subtitlesSize]);
+
+ React.useEffect(() => {
+ video.setProp("subtitlesOffset", settings.subtitlesOffset);
+ video.setProp("extraSubtitlesOffset", settings.subtitlesOffset);
+ }, [settings.subtitlesOffset]);
+
+ React.useEffect(() => {
+ video.setProp("subtitlesTextColor", settings.subtitlesTextColor);
+ video.setProp("extraSubtitlesTextColor", settings.subtitlesTextColor);
+ }, [settings.subtitlesTextColor]);
+
+ React.useEffect(() => {
+ video.setProp(
+ "subtitlesBackgroundColor",
+ settings.subtitlesBackgroundColor
+ );
+ video.setProp(
+ "extraSubtitlesBackgroundColor",
+ settings.subtitlesBackgroundColor
+ );
+ }, [settings.subtitlesBackgroundColor]);
+
+ React.useEffect(() => {
+ video.setProp("subtitlesOutlineColor", settings.subtitlesOutlineColor);
+ video.setProp("extraSubtitlesOutlineColor", settings.subtitlesOutlineColor);
+ }, [settings.subtitlesOutlineColor]);
+
+ React.useEffect(() => {
+ if (
+ video.state.time !== null &&
+ !isNaN(video.state.time) &&
+ video.state.duration !== null &&
+ !isNaN(video.state.duration) &&
+ video.state.manifest !== null &&
+ typeof video.state.manifest.name === "string"
+ ) {
+ timeChanged(
+ video.state.time,
+ video.state.duration,
+ video.state.manifest.name
+ );
+ }
+ }, [video.state.time, video.state.duration, video.state.manifest]);
+
+ React.useEffect(() => {
+ if (video.state.paused !== null) {
+ pausedChanged(video.state.paused);
+ }
+ }, [video.state.paused]);
+
+ React.useEffect(() => {
+ videoParamsChanged(video.state.videoParams);
+ }, [video.state.videoParams]);
+
+ React.useEffect(() => {
+ if (
+ !!settings.bingeWatching &&
+ player.nextVideo !== null &&
+ !nextVideoPopupDismissed.current
+ ) {
+ if (
+ video.state.time !== null &&
+ video.state.duration !== null &&
+ video.state.time < video.state.duration &&
+ video.state.duration - video.state.time <=
+ settings.nextVideoNotificationDuration
+ ) {
+ openNextVideoPopup();
+ } else {
+ closeNextVideoPopup();
+ }
+ }
+ }, [player.nextVideo, video.state.time, video.state.duration]);
+
+ React.useEffect(() => {
+ if (!defaultSubtitlesSelected.current) {
+ const findTrackByLang = (tracks, lang) =>
+ tracks.find(
+ (track) =>
+ track.lang === lang || langs.where("1", track.lang)?.[2] === lang
+ );
+
+ const subtitlesTrack = findTrackByLang(
+ video.state.subtitlesTracks,
+ settings.subtitlesLanguage
+ );
+ const extraSubtitlesTrack = findTrackByLang(
+ video.state.extraSubtitlesTracks,
+ settings.subtitlesLanguage
+ );
+
+ if (subtitlesTrack && subtitlesTrack.id) {
+ onSubtitlesTrackSelected(subtitlesTrack.id);
+ defaultSubtitlesSelected.current = true;
+ } else if (extraSubtitlesTrack && extraSubtitlesTrack.id) {
+ onExtraSubtitlesTrackSelected(extraSubtitlesTrack.id);
+ defaultSubtitlesSelected.current = true;
+ }
+ }
+ }, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
+
+ React.useEffect(() => {
+ if (!defaultAudioTrackSelected.current) {
+ const findTrackByLang = (tracks, lang) =>
+ tracks.find(
+ (track) =>
+ track.lang === lang || langs.where("1", track.lang)?.[2] === lang
+ );
+ const audioTrack = findTrackByLang(
+ video.state.audioTracks,
+ settings.audioLanguage
+ );
+
+ if (audioTrack && audioTrack.id) {
+ onAudioTrackSelected(audioTrack.id);
+ defaultAudioTrackSelected.current = true;
+ }
+ }
+ }, [video.state.audioTracks]);
+
+ React.useEffect(() => {
+ defaultSubtitlesSelected.current = false;
+ defaultAudioTrackSelected.current = false;
+ nextVideoPopupDismissed.current = false;
+ }, [video.state.stream]);
+
+ React.useEffect(() => {
+ if (
+ (!Array.isArray(video.state.subtitlesTracks) ||
+ video.state.subtitlesTracks.length === 0) &&
+ (!Array.isArray(video.state.extraSubtitlesTracks) ||
+ video.state.extraSubtitlesTracks.length === 0) &&
+ (!Array.isArray(video.state.audioTracks) ||
+ video.state.audioTracks.length === 0)
+ ) {
+ closeSubtitlesMenu();
+ }
+ }, [
+ video.state.audioTracks,
+ video.state.subtitlesTracks,
+ video.state.extraSubtitlesTracks,
+ ]);
+
+ React.useEffect(() => {
+ if (player.metaItem === null || player.metaItem.type !== "Ready") {
+ closeInfoMenu();
+ closeVideosMenu();
+ }
+ }, [player.metaItem]);
+
+ React.useEffect(() => {
+ if (video.state.playbackSpeed === null) {
+ closeSpeedMenu();
+ }
+ }, [video.state.playbackSpeed]);
+
+ React.useEffect(() => {
+ const toastFilter = (item) => item?.dataset?.type === "CoreEvent";
+ toast.addFilter(toastFilter);
+ const onCastStateChange = () => {
+ setCasting(
+ chromecast.active &&
+ chromecast.transport.getCastState() ===
+ cast.framework.CastState.CONNECTED
+ );
+ };
+ const onChromecastServiceStateChange = () => {
+ onCastStateChange();
+ if (chromecast.active) {
+ chromecast.transport.on(
+ cast.framework.CastContextEventType.CAST_STATE_CHANGED,
+ onCastStateChange
+ );
+ }
+ };
+ const onCoreEvent = ({ event }) => {
+ if (event === "PlayingOnDevice") {
+ onPauseRequested();
+ }
+ };
+ chromecast.on("stateChanged", onChromecastServiceStateChange);
+ core.transport.on("CoreEvent", onCoreEvent);
+ onChromecastServiceStateChange();
+ return () => {
+ toast.removeFilter(toastFilter);
+ chromecast.off("stateChanged", onChromecastServiceStateChange);
+ core.transport.off("CoreEvent", onCoreEvent);
+ if (chromecast.active) {
+ chromecast.transport.off(
+ cast.framework.CastContextEventType.CAST_STATE_CHANGED,
+ onCastStateChange
+ );
+ }
+ };
+ }, []);
+
+ React.useLayoutEffect(() => {
+ const onKeyDown = (event) => {
+ switch (event.code) {
+ case "Space": {
+ if (
+ !menusOpen &&
+ !nextVideoPopupOpen &&
+ video.state.paused !== null
+ ) {
+ if (video.state.paused) {
+ onPlayRequested();
+ } else {
+ onPauseRequested();
+ }
+ }
+
+ break;
+ }
+ case "ArrowRight": {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
+ const seekDuration = event.shiftKey
+ ? settings.seekShortTimeDuration
+ : settings.seekTimeDuration;
+ onSeekRequested(video.state.time + seekDuration);
+ }
+
+ break;
+ }
+ case "ArrowLeft": {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
+ const seekDuration = event.shiftKey
+ ? settings.seekShortTimeDuration
+ : settings.seekTimeDuration;
+ onSeekRequested(video.state.time - seekDuration);
+ }
+
+ break;
+ }
+ case "ArrowUp": {
+ if (
+ !menusOpen &&
+ !nextVideoPopupOpen &&
+ video.state.volume !== null
+ ) {
+ onVolumeChangeRequested(video.state.volume + 5);
+ }
+
+ break;
+ }
+ case "ArrowDown": {
+ if (
+ !menusOpen &&
+ !nextVideoPopupOpen &&
+ video.state.volume !== null
+ ) {
+ onVolumeChangeRequested(video.state.volume - 5);
+ }
+
+ break;
+ }
+ case "KeyS": {
+ closeMenus();
+ if (
+ (Array.isArray(video.state.subtitlesTracks) &&
+ video.state.subtitlesTracks.length > 0) ||
+ (Array.isArray(video.state.extraSubtitlesTracks) &&
+ video.state.extraSubtitlesTracks.length > 0) ||
+ (Array.isArray(video.state.audioTracks) &&
+ video.state.audioTracks.length > 0)
+ ) {
+ toggleSubtitlesMenu();
+ }
+
+ break;
+ }
+ case "KeyC": {
+ if (!menusOpen && !nextVideoPopupOpen) {
+ if (video.state.selectedSubtitlesTrackId !== null) {
+ onSubtitlesTrackSelected(null);
+ } else if (previousSubtitlesTrackId !== null) {
+ onSubtitlesTrackSelected(previousSubtitlesTrackId);
+ } else if (video.state.subtitlesTracks.length > 0) {
+ onSubtitlesTrackSelected(video.state.subtitlesTracks[0].id);
+ } else if (video.state.extraSubtitlesTracks.length > 0) {
+ onExtraSubtitlesTrackSelected(
+ video.state.extraSubtitlesTracks[0].id
+ );
+ }
+ }
+ break;
+ }
+ case "KeyI": {
+ closeMenus();
+ if (player.metaItem !== null && player.metaItem.type === "Ready") {
+ toggleInfoMenu();
+ }
+
+ break;
+ }
+ case "KeyR": {
+ closeMenus();
+ if (video.state.playbackSpeed !== null) {
+ toggleSpeedMenu();
+ }
+
+ break;
+ }
+ case "KeyV": {
+ closeMenus();
+ if (
+ player.metaItem !== null &&
+ player.metaItem.type === "Ready" &&
+ player.metaItem?.content?.videos?.length > 0
+ ) {
+ toggleVideosMenu();
+ }
+
+ break;
+ }
+ case "KeyD": {
+ closeMenus();
+ if (
+ streamingServer.statistics !== null &&
+ streamingServer.statistics.type !== "Err" &&
+ player.selected &&
+ typeof player.selected.stream.infoHash === "string" &&
+ typeof player.selected.stream.fileIdx === "number"
+ ) {
+ toggleStatisticsMenu();
+ }
+
+ break;
+ }
+ case "Escape": {
+ closeMenus();
+ break;
+ }
+ }
+ };
+ const onWheel = ({ deltaY }) => {
+ if (deltaY > 0) {
+ if (!menusOpen && video.state.volume !== null) {
+ onVolumeChangeRequested(video.state.volume - 5);
+ }
+ } else {
+ if (!menusOpen && video.state.volume !== null) {
+ onVolumeChangeRequested(video.state.volume + 5);
+ }
+ }
+ };
+ if (routeFocused) {
+ window.addEventListener("keydown", onKeyDown);
+ window.addEventListener("wheel", onWheel);
+ }
+ return () => {
+ window.removeEventListener("keydown", onKeyDown);
+ window.removeEventListener("wheel", onWheel);
+ };
+ }, [
+ player.metaItem,
+ player.selected,
+ streamingServer.statistics,
+ settings.seekTimeDuration,
+ settings.seekShortTimeDuration,
+ routeFocused,
+ menusOpen,
+ nextVideoPopupOpen,
+ video.state.paused,
+ video.state.time,
+ video.state.volume,
+ video.state.audioTracks,
+ video.state.subtitlesTracks,
+ video.state.extraSubtitlesTracks,
+ video.state.playbackSpeed,
+ toggleSubtitlesMenu,
+ toggleInfoMenu,
+ toggleVideosMenu,
+ toggleStatisticsMenu,
+ ]);
+
+ React.useEffect(() => {
+ video.events.on("error", onError);
+ video.events.on("ended", onEnded);
+ video.events.on("subtitlesTrackLoaded", onSubtitlesTrackLoaded);
+ video.events.on("extraSubtitlesTrackLoaded", onExtraSubtitlesTrackLoaded);
+ video.events.on("implementationChanged", onImplementationChanged);
+
+ return () => {
+ video.events.off("error", onError);
+ video.events.off("ended", onEnded);
+ video.events.off("subtitlesTrackLoaded", onSubtitlesTrackLoaded);
+ video.events.off(
+ "extraSubtitlesTrackLoaded",
+ onExtraSubtitlesTrackLoaded
+ );
+ video.events.off("implementationChanged", onImplementationChanged);
+ };
+ }, []);
+
+ React.useLayoutEffect(() => {
+ return () => {
+ setImmersedDebounced.cancel();
+ onPlayRequestedDebounced.cancel();
+ onPauseRequestedDebounced.cancel();
+ };
+ }, []);
+
+ return (
+
+
+ {!video.state.loaded ? (
+
+

+
+ ) : null}
+ {(video.state.buffering || !video.state.loaded) && !error ? (
+
+ ) : null}
+ {error !== null ? (
+
+ ) : null}
+ {menusOpen ?
: null}
+ {video.state.volume !== null && overlayHidden ? (
+
+ ) : null}
+
+
+ {nextVideoPopupOpen ? (
+
+ ) : null}
+ {statisticsMenuOpen ? (
+
+ ) : null}
+ {subtitlesMenuOpen ? (
+
+ ) : null}
+ {infoMenuOpen ? (
+
+ ) : null}
+ {speedMenuOpen ? (
+
+ ) : null}
+ {videosMenuOpen ? (
+
+ ) : null}
+ {optionsMenuOpen ? (
+
+ ) : null}
+
+ );
};
Player.propTypes = {
- urlParams: PropTypes.shape({
- stream: PropTypes.string,
- streamTransportUrl: PropTypes.string,
- metaTransportUrl: PropTypes.string,
- type: PropTypes.string,
- id: PropTypes.string,
- videoId: PropTypes.string
- }),
- queryParams: PropTypes.instanceOf(URLSearchParams)
+ urlParams: PropTypes.shape({
+ stream: PropTypes.string,
+ streamTransportUrl: PropTypes.string,
+ metaTransportUrl: PropTypes.string,
+ type: PropTypes.string,
+ id: PropTypes.string,
+ videoId: PropTypes.string,
+ }),
+ queryParams: PropTypes.instanceOf(URLSearchParams),
};
const PlayerFallback = () => (
-
+
);
module.exports = withCoreSuspender(Player, PlayerFallback);
From 881c8080035d0deaabab67602d6ce9f4cf44257d Mon Sep 17 00:00:00 2001
From: Neeraj TK <64883030+GaryGosh@users.noreply.github.com>
Date: Mon, 23 Sep 2024 20:07:11 +0530
Subject: [PATCH 003/154] formatting revert
---
src/routes/Player/Player.js | 1722 ++++++++++++++++-------------------
1 file changed, 765 insertions(+), 957 deletions(-)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index 894a9b466..00071e02d 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -1,1014 +1,822 @@
// Copyright (C) 2017-2023 Smart code 203358507
-const React = require("react");
-const PropTypes = require("prop-types");
-const classnames = require("classnames");
-const debounce = require("lodash.debounce");
-const langs = require("langs");
-const { useTranslation } = require("react-i18next");
-const { useRouteFocused } = require("stremio-router");
-const { useServices } = require("stremio/services");
-const {
- HorizontalNavBar,
- useFullscreen,
- useBinaryState,
- useToast,
- useStreamingServer,
- withCoreSuspender,
-} = require("stremio/common");
-const BufferingLoader = require("./BufferingLoader");
-const VolumeChangeIndicator = require("./VolumeChangeIndicator");
-const Error = require("./Error");
-const ControlBar = require("./ControlBar");
-const NextVideoPopup = require("./NextVideoPopup");
-const StatisticsMenu = require("./StatisticsMenu");
-const InfoMenu = require("./InfoMenu");
-const OptionsMenu = require("./OptionsMenu");
-const VideosMenu = require("./VideosMenu");
-const SubtitlesMenu = require("./SubtitlesMenu");
-const SpeedMenu = require("./SpeedMenu");
-const usePlayer = require("./usePlayer");
-const useSettings = require("./useSettings");
-const useStatistics = require("./useStatistics");
-const useVideo = require("./useVideo");
-const styles = require("./styles");
-const Video = require("./Video");
+const React = require('react');
+const PropTypes = require('prop-types');
+const classnames = require('classnames');
+const debounce = require('lodash.debounce');
+const langs = require('langs');
+const { useTranslation } = require('react-i18next');
+const { useRouteFocused } = require('stremio-router');
+const { useServices } = require('stremio/services');
+const { HorizontalNavBar, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common');
+const BufferingLoader = require('./BufferingLoader');
+const VolumeChangeIndicator = require('./VolumeChangeIndicator');
+const Error = require('./Error');
+const ControlBar = require('./ControlBar');
+const NextVideoPopup = require('./NextVideoPopup');
+const StatisticsMenu = require('./StatisticsMenu');
+const InfoMenu = require('./InfoMenu');
+const OptionsMenu = require('./OptionsMenu');
+const VideosMenu = require('./VideosMenu');
+const SubtitlesMenu = require('./SubtitlesMenu');
+const SpeedMenu = require('./SpeedMenu');
+const usePlayer = require('./usePlayer');
+const useSettings = require('./useSettings');
+const useStatistics = require('./useStatistics');
+const useVideo = require('./useVideo');
+const styles = require('./styles');
+const Video = require('./Video');
const Player = ({ urlParams, queryParams }) => {
- const { t } = useTranslation();
- const { chromecast, shell, core } = useServices();
- const forceTranscoding = React.useMemo(() => {
- return queryParams.has("forceTranscoding");
- }, [queryParams]);
+ const { t } = useTranslation();
+ const { chromecast, shell, core } = useServices();
+ const forceTranscoding = React.useMemo(() => {
+ return queryParams.has('forceTranscoding');
+ }, [queryParams]);
- const [
- player,
- videoParamsChanged,
- timeChanged,
- pausedChanged,
- ended,
- nextVideo,
- ] = usePlayer(urlParams);
- const [settings, updateSettings] = useSettings();
- const streamingServer = useStreamingServer();
- const statistics = useStatistics(player, streamingServer);
- const video = useVideo();
- const routeFocused = useRouteFocused();
- const toast = useToast();
+ const [player, videoParamsChanged, timeChanged, pausedChanged, ended, nextVideo] = usePlayer(urlParams);
+ const [settings, updateSettings] = useSettings();
+ const streamingServer = useStreamingServer();
+ const statistics = useStatistics(player, streamingServer);
+ const video = useVideo();
+ const routeFocused = useRouteFocused();
+ const toast = useToast();
- const [casting, setCasting] = React.useState(() => {
- return (
- chromecast.active &&
- chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED
- );
- });
-
- const [immersed, setImmersed] = React.useState(true);
- const setImmersedDebounced = React.useCallback(
- debounce(setImmersed, 3000),
- []
- );
- const [, , , toggleFullscreen] = useFullscreen();
-
- const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] =
- useBinaryState(false);
- const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] =
- useBinaryState(false);
- const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
- const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] =
- useBinaryState(false);
- const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] =
- useBinaryState(false);
- const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] =
- useBinaryState(false);
- const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] =
- useBinaryState(false);
-
- const menusOpen = React.useMemo(() => {
- return (
- optionsMenuOpen ||
- subtitlesMenuOpen ||
- infoMenuOpen ||
- speedMenuOpen ||
- videosMenuOpen ||
- statisticsMenuOpen
- );
- }, [
- optionsMenuOpen,
- subtitlesMenuOpen,
- infoMenuOpen,
- speedMenuOpen,
- videosMenuOpen,
- statisticsMenuOpen,
- ]);
-
- const closeMenus = React.useCallback(() => {
- closeOptionsMenu();
- closeSubtitlesMenu();
- closeInfoMenu();
- closeSpeedMenu();
- closeVideosMenu();
- closeStatisticsMenu();
- }, []);
-
- const overlayHidden = React.useMemo(() => {
- return (
- immersed &&
- !casting &&
- video.state.paused !== null &&
- !video.state.paused &&
- !menusOpen &&
- !nextVideoPopupOpen
- );
- }, [immersed, casting, video.state.paused, menusOpen, nextVideoPopupOpen]);
-
- const nextVideoPopupDismissed = React.useRef(false);
- const defaultSubtitlesSelected = React.useRef(false);
- const defaultAudioTrackSelected = React.useRef(false);
- const [error, setError] = React.useState(null);
- const [previousSubtitlesTrackId, setPreviousSubtitlesTrackId] =
- React.useState(null);
-
- const onImplementationChanged = React.useCallback(() => {
- video.setProp("subtitlesSize", settings.subtitlesSize);
- video.setProp("subtitlesOffset", settings.subtitlesOffset);
- video.setProp("subtitlesTextColor", settings.subtitlesTextColor);
- video.setProp(
- "subtitlesBackgroundColor",
- settings.subtitlesBackgroundColor
- );
- video.setProp("subtitlesOutlineColor", settings.subtitlesOutlineColor);
- video.setProp("extraSubtitlesSize", settings.subtitlesSize);
- video.setProp("extraSubtitlesOffset", settings.subtitlesOffset);
- video.setProp("extraSubtitlesTextColor", settings.subtitlesTextColor);
- video.setProp(
- "extraSubtitlesBackgroundColor",
- settings.subtitlesBackgroundColor
- );
- video.setProp("extraSubtitlesOutlineColor", settings.subtitlesOutlineColor);
- }, [
- settings.subtitlesSize,
- settings.subtitlesOffset,
- settings.subtitlesTextColor,
- settings.subtitlesBackgroundColor,
- settings.subtitlesOutlineColor,
- ]);
-
- const onEnded = React.useCallback(() => {
- ended();
- if (player.nextVideo !== null) {
- onNextVideoRequested();
- } else {
- window.history.back();
- }
- }, [player.nextVideo, onNextVideoRequested]);
-
- const onError = React.useCallback((error) => {
- console.error("Player", error);
- if (error.critical) {
- setError(error);
- } else {
- toast.show({
- type: "error",
- title: t("ERROR"),
- message: error.message,
- timeout: 3000,
- });
- }
- }, []);
-
- const onSubtitlesTrackLoaded = React.useCallback(() => {
- toast.show({
- type: "success",
- title: t("PLAYER_SUBTITLES_LOADED"),
- message: t("PLAYER_SUBTITLES_LOADED_EMBEDDED"),
- timeout: 3000,
+ const [casting, setCasting] = React.useState(() => {
+ return chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED;
});
- }, []);
- const onExtraSubtitlesTrackLoaded = React.useCallback((track) => {
- toast.show({
- type: "success",
- title: t("PLAYER_SUBTITLES_LOADED"),
- message: track.exclusive
- ? t("PLAYER_SUBTITLES_LOADED_EXCLUSIVE")
- : t("PLAYER_SUBTITLES_LOADED_ORIGIN", { origin: track.origin }),
- timeout: 3000,
- });
- }, []);
+ const [immersed, setImmersed] = React.useState(true);
+ const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
+ const [, , , toggleFullscreen] = useFullscreen();
- const onPlayRequested = React.useCallback(() => {
- video.setProp("paused", false);
- }, []);
+ const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false);
+ const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
+ const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
+ const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
+ const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false);
+ const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] = useBinaryState(false);
+ const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
- const onPlayRequestedDebounced = React.useCallback(
- debounce(onPlayRequested, 200),
- []
- );
+ const menusOpen = React.useMemo(() => {
+ return optionsMenuOpen || subtitlesMenuOpen || infoMenuOpen || speedMenuOpen || videosMenuOpen || statisticsMenuOpen;
+ }, [optionsMenuOpen, subtitlesMenuOpen, infoMenuOpen, speedMenuOpen, videosMenuOpen, statisticsMenuOpen]);
- const onPauseRequested = React.useCallback(() => {
- video.setProp("paused", true);
- }, []);
+ const closeMenus = React.useCallback(() => {
+ closeOptionsMenu();
+ closeSubtitlesMenu();
+ closeInfoMenu();
+ closeSpeedMenu();
+ closeVideosMenu();
+ closeStatisticsMenu();
+ }, []);
- const onPauseRequestedDebounced = React.useCallback(
- debounce(onPauseRequested, 200),
- []
- );
- const onMuteRequested = React.useCallback(() => {
- video.setProp("muted", true);
- }, []);
+ const overlayHidden = React.useMemo(() => {
+ return immersed && !casting && video.state.paused !== null && !video.state.paused && !menusOpen && !nextVideoPopupOpen;
+ }, [immersed, casting, video.state.paused, menusOpen, nextVideoPopupOpen]);
- const onUnmuteRequested = React.useCallback(() => {
- video.setProp("muted", false);
- }, []);
+ const nextVideoPopupDismissed = React.useRef(false);
+ const defaultSubtitlesSelected = React.useRef(false);
+ const defaultAudioTrackSelected = React.useRef(false);
+ const [error, setError] = React.useState(null);
+ const [previousSubtitlesTrackId, setPreviousSubtitlesTrackId] = React.useState(null);
- const onVolumeChangeRequested = React.useCallback((volume) => {
- video.setProp("volume", volume);
- }, []);
- const onSeekRequested = React.useCallback((time) => {
- video.setProp("time", time);
- }, []);
+ const onImplementationChanged = React.useCallback(() => {
+ video.setProp('subtitlesSize', settings.subtitlesSize);
+ video.setProp('subtitlesOffset', settings.subtitlesOffset);
+ video.setProp('subtitlesTextColor', settings.subtitlesTextColor);
+ video.setProp('subtitlesBackgroundColor', settings.subtitlesBackgroundColor);
+ video.setProp('subtitlesOutlineColor', settings.subtitlesOutlineColor);
+ video.setProp('extraSubtitlesSize', settings.subtitlesSize);
+ video.setProp('extraSubtitlesOffset', settings.subtitlesOffset);
+ video.setProp('extraSubtitlesTextColor', settings.subtitlesTextColor);
+ video.setProp('extraSubtitlesBackgroundColor', settings.subtitlesBackgroundColor);
+ video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor);
+ }, [settings.subtitlesSize, settings.subtitlesOffset, settings.subtitlesTextColor, settings.subtitlesBackgroundColor, settings.subtitlesOutlineColor]);
- const onPlaybackSpeedChanged = React.useCallback((rate) => {
- video.setProp("playbackSpeed", rate);
- }, []);
-
- const onSubtitlesTrackSelected = React.useCallback((id) => {
- video.setProp("selectedSubtitlesTrackId", id);
- video.setProp("selectedExtraSubtitlesTrackId", null);
- }, []);
-
- const onExtraSubtitlesTrackSelected = React.useCallback((id) => {
- video.setProp("selectedSubtitlesTrackId", null);
- video.setProp("selectedExtraSubtitlesTrackId", id);
- }, []);
-
- const onAudioTrackSelected = React.useCallback((id) => {
- video.setProp("selectedAudioTrackId", id);
- }, []);
-
- const onExtraSubtitlesDelayChanged = React.useCallback((delay) => {
- video.setProp("extraSubtitlesDelay", delay);
- }, []);
-
- const onSubtitlesTrackSelected = React.useCallback((id) => {
- video.setProp("selectedSubtitlesTrackId", id);
- video.setProp("selectedExtraSubtitlesTrackId", null);
- setPreviousSubtitlesTrackId(id);
- }, []);
-
- const onSubtitlesSizeChanged = React.useCallback(
- (size) => {
- updateSettings({ subtitlesSize: size });
- },
- [updateSettings]
- );
-
- const onSubtitlesOffsetChanged = React.useCallback(
- (offset) => {
- updateSettings({ subtitlesOffset: offset });
- },
- [updateSettings]
- );
-
- const onDismissNextVideoPopup = React.useCallback(() => {
- closeNextVideoPopup();
- nextVideoPopupDismissed.current = true;
- }, []);
-
- const onNextVideoRequested = React.useCallback(() => {
- if (player.nextVideo !== null) {
- nextVideo();
-
- const deepLinks = player.nextVideo.deepLinks;
- if (deepLinks.metaDetailsStreams && deepLinks.player) {
- window.location.replace(deepLinks.metaDetailsStreams);
- window.location.href = deepLinks.player;
- } else {
- window.location.replace(
- deepLinks.player ?? deepLinks.metaDetailsStreams
- );
- }
- }
- }, [player.nextVideo]);
-
- const onVideoClick = React.useCallback(() => {
- if (video.state.paused !== null) {
- if (video.state.paused) {
- onPlayRequestedDebounced();
- } else {
- onPauseRequestedDebounced();
- }
- }
- }, [video.state.paused]);
-
- const onVideoDoubleClick = React.useCallback(() => {
- onPlayRequestedDebounced.cancel();
- onPauseRequestedDebounced.cancel();
- toggleFullscreen();
- }, [toggleFullscreen]);
-
- const onContainerMouseDown = React.useCallback((event) => {
- if (!event.nativeEvent.optionsMenuClosePrevented) {
- closeOptionsMenu();
- }
- if (!event.nativeEvent.subtitlesMenuClosePrevented) {
- closeSubtitlesMenu();
- }
- if (!event.nativeEvent.infoMenuClosePrevented) {
- closeInfoMenu();
- }
- if (!event.nativeEvent.speedMenuClosePrevented) {
- closeSpeedMenu();
- }
- if (!event.nativeEvent.videosMenuClosePrevented) {
- closeVideosMenu();
- }
- if (!event.nativeEvent.statisticsMenuClosePrevented) {
- closeStatisticsMenu();
- }
- }, []);
-
- const onContainerMouseMove = React.useCallback((event) => {
- setImmersed(false);
- if (!event.nativeEvent.immersePrevented) {
- setImmersedDebounced(true);
- } else {
- setImmersedDebounced.cancel();
- }
- }, []);
-
- const onContainerMouseLeave = React.useCallback(() => {
- setImmersedDebounced.cancel();
- setImmersed(true);
- }, []);
-
- const onBarMouseMove = React.useCallback((event) => {
- event.nativeEvent.immersePrevented = true;
- }, []);
-
- React.useEffect(() => {
- setError(null);
- if (player.selected === null) {
- video.unload();
- } else if (
- streamingServer.settings !== null &&
- streamingServer.settings.type !== "Loading" &&
- (player.selected.metaRequest === null ||
- (player.metaItem !== null && player.metaItem.type !== "Loading"))
- ) {
- video.load(
- {
- stream: {
- ...player.selected.stream,
- subtitles: Array.isArray(player.selected.stream.subtitles)
- ? player.selected.stream.subtitles.map((subtitles) => ({
- ...subtitles,
- label: subtitles.url,
- }))
- : [],
- },
- autoplay: true,
- time:
- player.libraryItem !== null &&
- player.selected.streamRequest !== null &&
- player.selected.streamRequest.path !== null &&
- player.libraryItem.state.video_id ===
- player.selected.streamRequest.path.id
- ? player.libraryItem.state.timeOffset
- : 0,
- forceTranscoding: forceTranscoding || casting,
- maxAudioChannels: settings.surroundSound ? 32 : 2,
- streamingServerURL: streamingServer.baseUrl
- ? casting
- ? streamingServer.baseUrl
- : streamingServer.selected.transportUrl
- : null,
- seriesInfo: player.seriesInfo,
- },
- {
- chromecastTransport: chromecast.active ? chromecast.transport : null,
- shellTransport: shell.active ? shell.transport : null,
+ const onEnded = React.useCallback(() => {
+ ended();
+ if (player.nextVideo !== null) {
+ onNextVideoRequested();
+ } else {
+ window.history.back();
}
- );
- }
- }, [
- streamingServer.baseUrl,
- player.selected,
- player.metaItem,
- forceTranscoding,
- casting,
- ]);
- React.useEffect(() => {
- if (video.state.stream !== null) {
- const tracks = player.subtitles.map((subtitles) => ({
- ...subtitles,
- label: subtitles.url,
- }));
- video.addExtraSubtitlesTracks(tracks);
- }
- }, [player.subtitles, video.state.stream]);
+ }, [player.nextVideo, onNextVideoRequested]);
- React.useEffect(() => {
- video.setProp("subtitlesSize", settings.subtitlesSize);
- video.setProp("extraSubtitlesSize", settings.subtitlesSize);
- }, [settings.subtitlesSize]);
+ const onError = React.useCallback((error) => {
+ console.error('Player', error);
+ if (error.critical) {
+ setError(error);
+ } else {
+ toast.show({
+ type: 'error',
+ title: t('ERROR'),
+ message: error.message,
+ timeout: 3000
+ });
+ }
+ }, []);
- React.useEffect(() => {
- video.setProp("subtitlesOffset", settings.subtitlesOffset);
- video.setProp("extraSubtitlesOffset", settings.subtitlesOffset);
- }, [settings.subtitlesOffset]);
+ const onSubtitlesTrackLoaded = React.useCallback(() => {
+ toast.show({
+ type: 'success',
+ title: t('PLAYER_SUBTITLES_LOADED'),
+ message: t('PLAYER_SUBTITLES_LOADED_EMBEDDED'),
+ timeout: 3000
+ });
+ }, []);
- React.useEffect(() => {
- video.setProp("subtitlesTextColor", settings.subtitlesTextColor);
- video.setProp("extraSubtitlesTextColor", settings.subtitlesTextColor);
- }, [settings.subtitlesTextColor]);
+ const onExtraSubtitlesTrackLoaded = React.useCallback((track) => {
+ toast.show({
+ type: 'success',
+ title: t('PLAYER_SUBTITLES_LOADED'),
+ message: track.exclusive ? t('PLAYER_SUBTITLES_LOADED_EXCLUSIVE') : t('PLAYER_SUBTITLES_LOADED_ORIGIN', { origin: track.origin }),
+ timeout: 3000
+ });
+ }, []);
- React.useEffect(() => {
- video.setProp(
- "subtitlesBackgroundColor",
- settings.subtitlesBackgroundColor
- );
- video.setProp(
- "extraSubtitlesBackgroundColor",
- settings.subtitlesBackgroundColor
- );
- }, [settings.subtitlesBackgroundColor]);
+ const onPlayRequested = React.useCallback(() => {
+ video.setProp('paused', false);
+ }, []);
- React.useEffect(() => {
- video.setProp("subtitlesOutlineColor", settings.subtitlesOutlineColor);
- video.setProp("extraSubtitlesOutlineColor", settings.subtitlesOutlineColor);
- }, [settings.subtitlesOutlineColor]);
+ const onPlayRequestedDebounced = React.useCallback(debounce(onPlayRequested, 200), []);
- React.useEffect(() => {
- if (
- video.state.time !== null &&
- !isNaN(video.state.time) &&
- video.state.duration !== null &&
- !isNaN(video.state.duration) &&
- video.state.manifest !== null &&
- typeof video.state.manifest.name === "string"
- ) {
- timeChanged(
- video.state.time,
- video.state.duration,
- video.state.manifest.name
- );
- }
- }, [video.state.time, video.state.duration, video.state.manifest]);
+ const onPauseRequested = React.useCallback(() => {
+ video.setProp('paused', true);
+ }, []);
- React.useEffect(() => {
- if (video.state.paused !== null) {
- pausedChanged(video.state.paused);
- }
- }, [video.state.paused]);
+ const onPauseRequestedDebounced = React.useCallback(debounce(onPauseRequested, 200), []);
+ const onMuteRequested = React.useCallback(() => {
+ video.setProp('muted', true);
+ }, []);
- React.useEffect(() => {
- videoParamsChanged(video.state.videoParams);
- }, [video.state.videoParams]);
+ const onUnmuteRequested = React.useCallback(() => {
+ video.setProp('muted', false);
+ }, []);
- React.useEffect(() => {
- if (
- !!settings.bingeWatching &&
- player.nextVideo !== null &&
- !nextVideoPopupDismissed.current
- ) {
- if (
- video.state.time !== null &&
- video.state.duration !== null &&
- video.state.time < video.state.duration &&
- video.state.duration - video.state.time <=
- settings.nextVideoNotificationDuration
- ) {
- openNextVideoPopup();
- } else {
+ const onVolumeChangeRequested = React.useCallback((volume) => {
+ video.setProp('volume', volume);
+ }, []);
+
+ const onSeekRequested = React.useCallback((time) => {
+ video.setProp('time', time);
+ }, []);
+
+ const onPlaybackSpeedChanged = React.useCallback((rate) => {
+ video.setProp('playbackSpeed', rate);
+ }, []);
+
+ const onSubtitlesTrackSelected = React.useCallback((id) => {
+ video.setProp('selectedSubtitlesTrackId', id);
+ video.setProp('selectedExtraSubtitlesTrackId', null);
+ }, []);
+
+ const onExtraSubtitlesTrackSelected = React.useCallback((id) => {
+ video.setProp('selectedSubtitlesTrackId', null);
+ video.setProp('selectedExtraSubtitlesTrackId', id);
+ }, []);
+
+ const onAudioTrackSelected = React.useCallback((id) => {
+ video.setProp('selectedAudioTrackId', id);
+ }, []);
+
+ const onExtraSubtitlesDelayChanged = React.useCallback((delay) => {
+ video.setProp('extraSubtitlesDelay', delay);
+ }, []);
+
+ const onSubtitlesTrackSelected = React.useCallback((id) => {
+ video.setProp('selectedSubtitlesTrackId', id);
+ video.setProp('selectedExtraSubtitlesTrackId', null);
+ setPreviousSubtitlesTrackId(id);
+ }, []);
+
+ const onSubtitlesSizeChanged = React.useCallback((size) => {
+ updateSettings({ subtitlesSize: size });
+ }, [updateSettings]);
+
+ const onSubtitlesOffsetChanged = React.useCallback((offset) => {
+ updateSettings({ subtitlesOffset: offset });
+ }, [updateSettings]);
+
+ const onDismissNextVideoPopup = React.useCallback(() => {
closeNextVideoPopup();
- }
- }
- }, [player.nextVideo, video.state.time, video.state.duration]);
+ nextVideoPopupDismissed.current = true;
+ }, []);
- React.useEffect(() => {
- if (!defaultSubtitlesSelected.current) {
- const findTrackByLang = (tracks, lang) =>
- tracks.find(
- (track) =>
- track.lang === lang || langs.where("1", track.lang)?.[2] === lang
- );
+ const onNextVideoRequested = React.useCallback(() => {
+ if (player.nextVideo !== null) {
+ nextVideo();
- const subtitlesTrack = findTrackByLang(
- video.state.subtitlesTracks,
- settings.subtitlesLanguage
- );
- const extraSubtitlesTrack = findTrackByLang(
- video.state.extraSubtitlesTracks,
- settings.subtitlesLanguage
- );
-
- if (subtitlesTrack && subtitlesTrack.id) {
- onSubtitlesTrackSelected(subtitlesTrack.id);
- defaultSubtitlesSelected.current = true;
- } else if (extraSubtitlesTrack && extraSubtitlesTrack.id) {
- onExtraSubtitlesTrackSelected(extraSubtitlesTrack.id);
- defaultSubtitlesSelected.current = true;
- }
- }
- }, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
-
- React.useEffect(() => {
- if (!defaultAudioTrackSelected.current) {
- const findTrackByLang = (tracks, lang) =>
- tracks.find(
- (track) =>
- track.lang === lang || langs.where("1", track.lang)?.[2] === lang
- );
- const audioTrack = findTrackByLang(
- video.state.audioTracks,
- settings.audioLanguage
- );
-
- if (audioTrack && audioTrack.id) {
- onAudioTrackSelected(audioTrack.id);
- defaultAudioTrackSelected.current = true;
- }
- }
- }, [video.state.audioTracks]);
-
- React.useEffect(() => {
- defaultSubtitlesSelected.current = false;
- defaultAudioTrackSelected.current = false;
- nextVideoPopupDismissed.current = false;
- }, [video.state.stream]);
-
- React.useEffect(() => {
- if (
- (!Array.isArray(video.state.subtitlesTracks) ||
- video.state.subtitlesTracks.length === 0) &&
- (!Array.isArray(video.state.extraSubtitlesTracks) ||
- video.state.extraSubtitlesTracks.length === 0) &&
- (!Array.isArray(video.state.audioTracks) ||
- video.state.audioTracks.length === 0)
- ) {
- closeSubtitlesMenu();
- }
- }, [
- video.state.audioTracks,
- video.state.subtitlesTracks,
- video.state.extraSubtitlesTracks,
- ]);
-
- React.useEffect(() => {
- if (player.metaItem === null || player.metaItem.type !== "Ready") {
- closeInfoMenu();
- closeVideosMenu();
- }
- }, [player.metaItem]);
-
- React.useEffect(() => {
- if (video.state.playbackSpeed === null) {
- closeSpeedMenu();
- }
- }, [video.state.playbackSpeed]);
-
- React.useEffect(() => {
- const toastFilter = (item) => item?.dataset?.type === "CoreEvent";
- toast.addFilter(toastFilter);
- const onCastStateChange = () => {
- setCasting(
- chromecast.active &&
- chromecast.transport.getCastState() ===
- cast.framework.CastState.CONNECTED
- );
- };
- const onChromecastServiceStateChange = () => {
- onCastStateChange();
- if (chromecast.active) {
- chromecast.transport.on(
- cast.framework.CastContextEventType.CAST_STATE_CHANGED,
- onCastStateChange
- );
- }
- };
- const onCoreEvent = ({ event }) => {
- if (event === "PlayingOnDevice") {
- onPauseRequested();
- }
- };
- chromecast.on("stateChanged", onChromecastServiceStateChange);
- core.transport.on("CoreEvent", onCoreEvent);
- onChromecastServiceStateChange();
- return () => {
- toast.removeFilter(toastFilter);
- chromecast.off("stateChanged", onChromecastServiceStateChange);
- core.transport.off("CoreEvent", onCoreEvent);
- if (chromecast.active) {
- chromecast.transport.off(
- cast.framework.CastContextEventType.CAST_STATE_CHANGED,
- onCastStateChange
- );
- }
- };
- }, []);
-
- React.useLayoutEffect(() => {
- const onKeyDown = (event) => {
- switch (event.code) {
- case "Space": {
- if (
- !menusOpen &&
- !nextVideoPopupOpen &&
- video.state.paused !== null
- ) {
- if (video.state.paused) {
- onPlayRequested();
+ const deepLinks = player.nextVideo.deepLinks;
+ if (deepLinks.metaDetailsStreams && deepLinks.player) {
+ window.location.replace(deepLinks.metaDetailsStreams);
+ window.location.href = deepLinks.player;
} else {
- onPauseRequested();
+ window.location.replace(deepLinks.player ?? deepLinks.metaDetailsStreams);
}
- }
-
- break;
}
- case "ArrowRight": {
- if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
- const seekDuration = event.shiftKey
- ? settings.seekShortTimeDuration
- : settings.seekTimeDuration;
- onSeekRequested(video.state.time + seekDuration);
- }
+ }, [player.nextVideo]);
- break;
- }
- case "ArrowLeft": {
- if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
- const seekDuration = event.shiftKey
- ? settings.seekShortTimeDuration
- : settings.seekTimeDuration;
- onSeekRequested(video.state.time - seekDuration);
- }
-
- break;
- }
- case "ArrowUp": {
- if (
- !menusOpen &&
- !nextVideoPopupOpen &&
- video.state.volume !== null
- ) {
- onVolumeChangeRequested(video.state.volume + 5);
- }
-
- break;
- }
- case "ArrowDown": {
- if (
- !menusOpen &&
- !nextVideoPopupOpen &&
- video.state.volume !== null
- ) {
- onVolumeChangeRequested(video.state.volume - 5);
- }
-
- break;
- }
- case "KeyS": {
- closeMenus();
- if (
- (Array.isArray(video.state.subtitlesTracks) &&
- video.state.subtitlesTracks.length > 0) ||
- (Array.isArray(video.state.extraSubtitlesTracks) &&
- video.state.extraSubtitlesTracks.length > 0) ||
- (Array.isArray(video.state.audioTracks) &&
- video.state.audioTracks.length > 0)
- ) {
- toggleSubtitlesMenu();
- }
-
- break;
- }
- case "KeyC": {
- if (!menusOpen && !nextVideoPopupOpen) {
- if (video.state.selectedSubtitlesTrackId !== null) {
- onSubtitlesTrackSelected(null);
- } else if (previousSubtitlesTrackId !== null) {
- onSubtitlesTrackSelected(previousSubtitlesTrackId);
- } else if (video.state.subtitlesTracks.length > 0) {
- onSubtitlesTrackSelected(video.state.subtitlesTracks[0].id);
- } else if (video.state.extraSubtitlesTracks.length > 0) {
- onExtraSubtitlesTrackSelected(
- video.state.extraSubtitlesTracks[0].id
- );
+ const onVideoClick = React.useCallback(() => {
+ if (video.state.paused !== null) {
+ if (video.state.paused) {
+ onPlayRequestedDebounced();
+ } else {
+ onPauseRequestedDebounced();
}
- }
- break;
}
- case "KeyI": {
- closeMenus();
- if (player.metaItem !== null && player.metaItem.type === "Ready") {
- toggleInfoMenu();
- }
+ }, [video.state.paused]);
- break;
+ const onVideoDoubleClick = React.useCallback(() => {
+ onPlayRequestedDebounced.cancel();
+ onPauseRequestedDebounced.cancel();
+ toggleFullscreen();
+ }, [toggleFullscreen]);
+
+ const onContainerMouseDown = React.useCallback((event) => {
+ if (!event.nativeEvent.optionsMenuClosePrevented) {
+ closeOptionsMenu();
}
- case "KeyR": {
- closeMenus();
- if (video.state.playbackSpeed !== null) {
- toggleSpeedMenu();
- }
-
- break;
+ if (!event.nativeEvent.subtitlesMenuClosePrevented) {
+ closeSubtitlesMenu();
}
- case "KeyV": {
- closeMenus();
- if (
- player.metaItem !== null &&
- player.metaItem.type === "Ready" &&
- player.metaItem?.content?.videos?.length > 0
- ) {
- toggleVideosMenu();
- }
-
- break;
+ if (!event.nativeEvent.infoMenuClosePrevented) {
+ closeInfoMenu();
}
- case "KeyD": {
- closeMenus();
- if (
- streamingServer.statistics !== null &&
- streamingServer.statistics.type !== "Err" &&
- player.selected &&
- typeof player.selected.stream.infoHash === "string" &&
- typeof player.selected.stream.fileIdx === "number"
- ) {
- toggleStatisticsMenu();
- }
-
- break;
+ if (!event.nativeEvent.speedMenuClosePrevented) {
+ closeSpeedMenu();
}
- case "Escape": {
- closeMenus();
- break;
+ if (!event.nativeEvent.videosMenuClosePrevented) {
+ closeVideosMenu();
}
- }
- };
- const onWheel = ({ deltaY }) => {
- if (deltaY > 0) {
- if (!menusOpen && video.state.volume !== null) {
- onVolumeChangeRequested(video.state.volume - 5);
+ if (!event.nativeEvent.statisticsMenuClosePrevented) {
+ closeStatisticsMenu();
}
- } else {
- if (!menusOpen && video.state.volume !== null) {
- onVolumeChangeRequested(video.state.volume + 5);
+ }, []);
+
+ const onContainerMouseMove = React.useCallback((event) => {
+ setImmersed(false);
+ if (!event.nativeEvent.immersePrevented) {
+ setImmersedDebounced(true);
+ } else {
+ setImmersedDebounced.cancel();
}
- }
- };
- if (routeFocused) {
- window.addEventListener("keydown", onKeyDown);
- window.addEventListener("wheel", onWheel);
- }
- return () => {
- window.removeEventListener("keydown", onKeyDown);
- window.removeEventListener("wheel", onWheel);
- };
- }, [
- player.metaItem,
- player.selected,
- streamingServer.statistics,
- settings.seekTimeDuration,
- settings.seekShortTimeDuration,
- routeFocused,
- menusOpen,
- nextVideoPopupOpen,
- video.state.paused,
- video.state.time,
- video.state.volume,
- video.state.audioTracks,
- video.state.subtitlesTracks,
- video.state.extraSubtitlesTracks,
- video.state.playbackSpeed,
- toggleSubtitlesMenu,
- toggleInfoMenu,
- toggleVideosMenu,
- toggleStatisticsMenu,
- ]);
+ }, []);
- React.useEffect(() => {
- video.events.on("error", onError);
- video.events.on("ended", onEnded);
- video.events.on("subtitlesTrackLoaded", onSubtitlesTrackLoaded);
- video.events.on("extraSubtitlesTrackLoaded", onExtraSubtitlesTrackLoaded);
- video.events.on("implementationChanged", onImplementationChanged);
+ const onContainerMouseLeave = React.useCallback(() => {
+ setImmersedDebounced.cancel();
+ setImmersed(true);
+ }, []);
- return () => {
- video.events.off("error", onError);
- video.events.off("ended", onEnded);
- video.events.off("subtitlesTrackLoaded", onSubtitlesTrackLoaded);
- video.events.off(
- "extraSubtitlesTrackLoaded",
- onExtraSubtitlesTrackLoaded
- );
- video.events.off("implementationChanged", onImplementationChanged);
- };
- }, []);
+ const onBarMouseMove = React.useCallback((event) => {
+ event.nativeEvent.immersePrevented = true;
+ }, []);
- React.useLayoutEffect(() => {
- return () => {
- setImmersedDebounced.cancel();
- onPlayRequestedDebounced.cancel();
- onPauseRequestedDebounced.cancel();
- };
- }, []);
+ React.useEffect(() => {
+ setError(null);
+ if (player.selected === null) {
+ video.unload();
+ } else if (streamingServer.settings !== null && streamingServer.settings.type !== 'Loading' &&
+ (player.selected.metaRequest === null || (player.metaItem !== null && player.metaItem.type !== 'Loading'))) {
+ video.load({
+ stream: {
+ ...player.selected.stream,
+ subtitles: Array.isArray(player.selected.stream.subtitles) ?
+ player.selected.stream.subtitles.map((subtitles) => ({
+ ...subtitles,
+ label: subtitles.url
+ }))
+ :
+ []
+ },
+ autoplay: true,
+ time: player.libraryItem !== null &&
+ player.selected.streamRequest !== null &&
+ player.selected.streamRequest.path !== null &&
+ player.libraryItem.state.video_id === player.selected.streamRequest.path.id ?
+ player.libraryItem.state.timeOffset
+ :
+ 0,
+ forceTranscoding: forceTranscoding || casting,
+ maxAudioChannels: settings.surroundSound ? 32 : 2,
+ streamingServerURL: streamingServer.baseUrl ?
+ casting ?
+ streamingServer.baseUrl
+ :
+ streamingServer.selected.transportUrl
+ :
+ null,
+ seriesInfo: player.seriesInfo
+ }, {
+ chromecastTransport: chromecast.active ? chromecast.transport : null,
+ shellTransport: shell.active ? shell.transport : null,
+ });
+ }
+ }, [streamingServer.baseUrl, player.selected, player.metaItem, forceTranscoding, casting]);
+ React.useEffect(() => {
+ if (video.state.stream !== null) {
+ const tracks = player.subtitles.map((subtitles) => ({
+ ...subtitles,
+ label: subtitles.url
+ }));
+ video.addExtraSubtitlesTracks(tracks);
+ }
+ }, [player.subtitles, video.state.stream]);
- return (
-
-
- {!video.state.loaded ? (
-
-

+ React.useEffect(() => {
+ video.setProp('subtitlesSize', settings.subtitlesSize);
+ video.setProp('extraSubtitlesSize', settings.subtitlesSize);
+ }, [settings.subtitlesSize]);
+
+ React.useEffect(() => {
+ video.setProp('subtitlesOffset', settings.subtitlesOffset);
+ video.setProp('extraSubtitlesOffset', settings.subtitlesOffset);
+ }, [settings.subtitlesOffset]);
+
+ React.useEffect(() => {
+ video.setProp('subtitlesTextColor', settings.subtitlesTextColor);
+ video.setProp('extraSubtitlesTextColor', settings.subtitlesTextColor);
+ }, [settings.subtitlesTextColor]);
+
+ React.useEffect(() => {
+ video.setProp('subtitlesBackgroundColor', settings.subtitlesBackgroundColor);
+ video.setProp('extraSubtitlesBackgroundColor', settings.subtitlesBackgroundColor);
+ }, [settings.subtitlesBackgroundColor]);
+
+ React.useEffect(() => {
+ video.setProp('subtitlesOutlineColor', settings.subtitlesOutlineColor);
+ video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor);
+ }, [settings.subtitlesOutlineColor]);
+
+ React.useEffect(() => {
+ if (video.state.time !== null && !isNaN(video.state.time) &&
+ video.state.duration !== null && !isNaN(video.state.duration) &&
+ video.state.manifest !== null && typeof video.state.manifest.name === 'string') {
+ timeChanged(video.state.time, video.state.duration, video.state.manifest.name);
+ }
+ }, [video.state.time, video.state.duration, video.state.manifest]);
+
+ React.useEffect(() => {
+ if (video.state.paused !== null) {
+ pausedChanged(video.state.paused);
+ }
+ }, [video.state.paused]);
+
+ React.useEffect(() => {
+ videoParamsChanged(video.state.videoParams);
+ }, [video.state.videoParams]);
+
+ React.useEffect(() => {
+ if (!!settings.bingeWatching && player.nextVideo !== null && !nextVideoPopupDismissed.current) {
+ if (video.state.time !== null && video.state.duration !== null && video.state.time < video.state.duration && (video.state.duration - video.state.time) <= settings.nextVideoNotificationDuration) {
+ openNextVideoPopup();
+ } else {
+ closeNextVideoPopup();
+ }
+ }
+ }, [player.nextVideo, video.state.time, video.state.duration]);
+
+ React.useEffect(() => {
+ if (!defaultSubtitlesSelected.current) {
+ const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
+
+ const subtitlesTrack = findTrackByLang(video.state.subtitlesTracks, settings.subtitlesLanguage);
+ const extraSubtitlesTrack = findTrackByLang(video.state.extraSubtitlesTracks, settings.subtitlesLanguage);
+
+ if (subtitlesTrack && subtitlesTrack.id) {
+ onSubtitlesTrackSelected(subtitlesTrack.id);
+ defaultSubtitlesSelected.current = true;
+ } else if (extraSubtitlesTrack && extraSubtitlesTrack.id) {
+ onExtraSubtitlesTrackSelected(extraSubtitlesTrack.id);
+ defaultSubtitlesSelected.current = true;
+ }
+ }
+ }, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
+
+ React.useEffect(() => {
+ if (!defaultAudioTrackSelected.current) {
+ const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
+ const audioTrack = findTrackByLang(video.state.audioTracks, settings.audioLanguage);
+
+ if (audioTrack && audioTrack.id) {
+ onAudioTrackSelected(audioTrack.id);
+ defaultAudioTrackSelected.current = true;
+ }
+ }
+ }, [video.state.audioTracks]);
+
+ React.useEffect(() => {
+ defaultSubtitlesSelected.current = false;
+ defaultAudioTrackSelected.current = false;
+ nextVideoPopupDismissed.current = false;
+ }, [video.state.stream]);
+
+ React.useEffect(() => {
+ if ((!Array.isArray(video.state.subtitlesTracks) || video.state.subtitlesTracks.length === 0) &&
+ (!Array.isArray(video.state.extraSubtitlesTracks) || video.state.extraSubtitlesTracks.length === 0) &&
+ (!Array.isArray(video.state.audioTracks) || video.state.audioTracks.length === 0)) {
+ closeSubtitlesMenu();
+ }
+ }, [video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
+
+ React.useEffect(() => {
+ if (player.metaItem === null || player.metaItem.type !== 'Ready') {
+ closeInfoMenu();
+ closeVideosMenu();
+ }
+ }, [player.metaItem]);
+
+ React.useEffect(() => {
+ if (video.state.playbackSpeed === null) {
+ closeSpeedMenu();
+ }
+ }, [video.state.playbackSpeed]);
+
+ React.useEffect(() => {
+ const toastFilter = (item) => item?.dataset?.type === 'CoreEvent';
+ toast.addFilter(toastFilter);
+ const onCastStateChange = () => {
+ setCasting(chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED);
+ };
+ const onChromecastServiceStateChange = () => {
+ onCastStateChange();
+ if (chromecast.active) {
+ chromecast.transport.on(
+ cast.framework.CastContextEventType.CAST_STATE_CHANGED,
+ onCastStateChange
+ );
+ }
+ };
+ const onCoreEvent = ({ event }) => {
+ if (event === 'PlayingOnDevice') {
+ onPauseRequested();
+ }
+ };
+ chromecast.on('stateChanged', onChromecastServiceStateChange);
+ core.transport.on('CoreEvent', onCoreEvent);
+ onChromecastServiceStateChange();
+ return () => {
+ toast.removeFilter(toastFilter);
+ chromecast.off('stateChanged', onChromecastServiceStateChange);
+ core.transport.off('CoreEvent', onCoreEvent);
+ if (chromecast.active) {
+ chromecast.transport.off(
+ cast.framework.CastContextEventType.CAST_STATE_CHANGED,
+ onCastStateChange
+ );
+ }
+ };
+ }, []);
+
+ React.useLayoutEffect(() => {
+ const onKeyDown = (event) => {
+ switch (event.code) {
+ case 'Space': {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) {
+ if (video.state.paused) {
+ onPlayRequested();
+ } else {
+ onPauseRequested();
+ }
+ }
+
+ break;
+ }
+ case 'ArrowRight': {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
+ const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
+ onSeekRequested(video.state.time + seekDuration);
+ }
+
+ break;
+ }
+ case 'ArrowLeft': {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
+ const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
+ onSeekRequested(video.state.time - seekDuration);
+ }
+
+ break;
+ }
+ case 'ArrowUp': {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
+ onVolumeChangeRequested(video.state.volume + 5);
+ }
+
+ break;
+ }
+ case 'ArrowDown': {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
+ onVolumeChangeRequested(video.state.volume - 5);
+ }
+
+ break;
+ }
+ case 'KeyS': {
+ closeMenus();
+ if ((Array.isArray(video.state.subtitlesTracks) && video.state.subtitlesTracks.length > 0) ||
+ (Array.isArray(video.state.extraSubtitlesTracks) && video.state.extraSubtitlesTracks.length > 0) ||
+ (Array.isArray(video.state.audioTracks) && video.state.audioTracks.length > 0)) {
+ toggleSubtitlesMenu();
+ }
+
+ break;
+ }
+ case 'KeyC': {
+ if (!menusOpen && !nextVideoPopupOpen) {
+ if (video.state.selectedSubtitlesTrackId !== null) {
+ onSubtitlesTrackSelected(null);
+ } else if (previousSubtitlesTrackId !== null) {
+ onSubtitlesTrackSelected(previousSubtitlesTrackId);
+ } else if (video.state.subtitlesTracks.length > 0) {
+ onSubtitlesTrackSelected(video.state.subtitlesTracks[0].id);
+ } else if (video.state.extraSubtitlesTracks.length > 0) {
+ onExtraSubtitlesTrackSelected(video.state.extraSubtitlesTracks[0].id);
+ }
+ }
+ break;
+ }
+ case 'KeyI': {
+ closeMenus();
+ if (player.metaItem !== null && player.metaItem.type === 'Ready') {
+ toggleInfoMenu();
+ }
+
+ break;
+ }
+ case 'KeyR': {
+ closeMenus();
+ if (video.state.playbackSpeed !== null) {
+ toggleSpeedMenu();
+ }
+
+ break;
+ }
+ case 'KeyV': {
+ closeMenus();
+ if (player.metaItem !== null && player.metaItem.type === 'Ready' && player.metaItem?.content?.videos?.length > 0) {
+ toggleVideosMenu();
+ }
+
+ break;
+ }
+ case 'KeyD': {
+ closeMenus();
+ if (streamingServer.statistics !== null && streamingServer.statistics.type !== 'Err' && player.selected && typeof player.selected.stream.infoHash === 'string' && typeof player.selected.stream.fileIdx === 'number') {
+ toggleStatisticsMenu();
+ }
+
+ break;
+ }
+ case 'Escape': {
+ closeMenus();
+ break;
+ }
+ }
+ };
+ const onWheel = ({ deltaY }) => {
+ if (deltaY > 0) {
+ if (!menusOpen && video.state.volume !== null) {
+ onVolumeChangeRequested(video.state.volume - 5);
+ }
+ } else {
+ if (!menusOpen && video.state.volume !== null) {
+ onVolumeChangeRequested(video.state.volume + 5);
+ }
+ }
+ };
+ if (routeFocused) {
+ window.addEventListener('keydown', onKeyDown);
+ window.addEventListener('wheel', onWheel);
+ }
+ return () => {
+ window.removeEventListener('keydown', onKeyDown);
+ window.removeEventListener('wheel', onWheel);
+ };
+ }, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, settings.seekShortTimeDuration, routeFocused, menusOpen, nextVideoPopupOpen, video.state.paused, video.state.time, video.state.volume, video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, video.state.playbackSpeed, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu, toggleStatisticsMenu]);
+
+ React.useEffect(() => {
+ video.events.on('error', onError);
+ video.events.on('ended', onEnded);
+ video.events.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
+ video.events.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
+ video.events.on('implementationChanged', onImplementationChanged);
+
+ return () => {
+ video.events.off('error', onError);
+ video.events.off('ended', onEnded);
+ video.events.off('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
+ video.events.off('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
+ video.events.off('implementationChanged', onImplementationChanged);
+ };
+ }, []);
+
+ React.useLayoutEffect(() => {
+ return () => {
+ setImmersedDebounced.cancel();
+ onPlayRequestedDebounced.cancel();
+ onPauseRequestedDebounced.cancel();
+ };
+ }, []);
+
+ return (
+
+
+ {
+ !video.state.loaded ?
+
+

+
+ :
+ null
+ }
+ {
+ (video.state.buffering || !video.state.loaded) && !error ?
+
+ :
+ null
+ }
+ {
+ error !== null ?
+
+ :
+ null
+ }
+ {
+ menusOpen ?
+
+ :
+ null
+ }
+ {
+ video.state.volume !== null && overlayHidden ?
+
+ :
+ null
+ }
+
+
+ {
+ nextVideoPopupOpen ?
+
+ :
+ null
+ }
+ {
+ statisticsMenuOpen ?
+
+ :
+ null
+ }
+ {
+ subtitlesMenuOpen ?
+
+ :
+ null
+ }
+ {
+ infoMenuOpen ?
+
+ :
+ null
+ }
+ {
+ speedMenuOpen ?
+
+ :
+ null
+ }
+ {
+ videosMenuOpen ?
+
+ :
+ null
+ }
+ {
+ optionsMenuOpen ?
+
+ :
+ null
+ }
- ) : null}
- {(video.state.buffering || !video.state.loaded) && !error ? (
-
- ) : null}
- {error !== null ? (
-
- ) : null}
- {menusOpen ?
: null}
- {video.state.volume !== null && overlayHidden ? (
-
- ) : null}
-
-
- {nextVideoPopupOpen ? (
-
- ) : null}
- {statisticsMenuOpen ? (
-
- ) : null}
- {subtitlesMenuOpen ? (
-
- ) : null}
- {infoMenuOpen ? (
-
- ) : null}
- {speedMenuOpen ? (
-
- ) : null}
- {videosMenuOpen ? (
-
- ) : null}
- {optionsMenuOpen ? (
-
- ) : null}
-
- );
+ );
};
Player.propTypes = {
- urlParams: PropTypes.shape({
- stream: PropTypes.string,
- streamTransportUrl: PropTypes.string,
- metaTransportUrl: PropTypes.string,
- type: PropTypes.string,
- id: PropTypes.string,
- videoId: PropTypes.string,
- }),
- queryParams: PropTypes.instanceOf(URLSearchParams),
+ urlParams: PropTypes.shape({
+ stream: PropTypes.string,
+ streamTransportUrl: PropTypes.string,
+ metaTransportUrl: PropTypes.string,
+ type: PropTypes.string,
+ id: PropTypes.string,
+ videoId: PropTypes.string
+ }),
+ queryParams: PropTypes.instanceOf(URLSearchParams)
};
const PlayerFallback = () => (
-
+
);
module.exports = withCoreSuspender(Player, PlayerFallback);
From 5ab324f12541c9803f49148ae71dd49f7defccae Mon Sep 17 00:00:00 2001
From: ArtificialSloth
Date: Mon, 30 Jun 2025 00:58:33 -0400
Subject: [PATCH 004/154] resolve upstream conflicts
---
.../ContinueWatchingItem.js | 19 ++-----------------
src/components/LibItem/LibItem.js | 10 +++++++++-
src/components/MetaItem/MetaItem.js | 12 ++++++------
3 files changed, 17 insertions(+), 24 deletions(-)
diff --git a/src/components/ContinueWatchingItem/ContinueWatchingItem.js b/src/components/ContinueWatchingItem/ContinueWatchingItem.js
index 8a0143619..8e56179df 100644
--- a/src/components/ContinueWatchingItem/ContinueWatchingItem.js
+++ b/src/components/ContinueWatchingItem/ContinueWatchingItem.js
@@ -5,24 +5,11 @@ const PropTypes = require('prop-types');
const { useServices } = require('stremio/services');
const LibItem = require('stremio/components/LibItem');
-const ContinueWatchingItem = ({ _id, notifications, deepLinks, ...props }) => {
+const ContinueWatchingItem = ({ _id, notifications, ...props }) => {
const { core } = useServices();
- const onClick = React.useCallback(() => {
- if (deepLinks?.metaDetailsVideos ?? deepLinks?.metaDetailsStreams) {
- window.location = deepLinks?.metaDetailsVideos ?? deepLinks?.metaDetailsStreams;
- }
- }, [deepLinks]);
-
- const onPlayClick = React.useCallback((event) => {
- event.stopPropagation();
- if (deepLinks?.player ?? deepLinks?.metaDetailsStreams ?? deepLinks?.metaDetailsVideos) {
- window.location = deepLinks?.player ?? deepLinks?.metaDetailsStreams ?? deepLinks?.metaDetailsVideos;
- }
- }, [deepLinks]);
-
const onDismissClick = React.useCallback((event) => {
- event.stopPropagation();
+ event.preventDefault();
if (typeof _id === 'string') {
core.transport.dispatch({
action: 'Ctx',
@@ -47,8 +34,6 @@ const ContinueWatchingItem = ({ _id, notifications, deepLinks, ...props }) => {
_id={_id}
posterChangeCursor={true}
notifications={notifications}
- onClick={onClick}
- onPlayClick={onPlayClick}
onDismissClick={onDismissClick}
/>
);
diff --git a/src/components/LibItem/LibItem.js b/src/components/LibItem/LibItem.js
index a42def27f..7157d913a 100644
--- a/src/components/LibItem/LibItem.js
+++ b/src/components/LibItem/LibItem.js
@@ -29,7 +29,7 @@ const LibItem = ({ _id, removable, notifications, watched, ...props }) => {
case 'details':
return props.deepLinks && (typeof props.deepLinks.metaDetailsVideos === 'string' || typeof props.deepLinks.metaDetailsStreams === 'string');
case 'watched':
- return props.deepLinks && (typeof props.deepLinks.metaDetailsVideos === 'string' || typeof props.deepLinks.metaDetailsStreams === 'string');
+ return typeof watched !== 'undefined' && props.deepLinks && (typeof props.deepLinks.metaDetailsVideos === 'string' || typeof props.deepLinks.metaDetailsStreams === 'string');
case 'dismiss':
return typeof _id === 'string' && props.progress !== null && !isNaN(props.progress) && props.progress > 0;
case 'remove':
@@ -119,6 +119,13 @@ const LibItem = ({ _id, removable, notifications, watched, ...props }) => {
}
}, [_id, props.deepLinks, props.optionOnSelect]);
+ const onPlayClick = React.useCallback((event) => {
+ if (props.deepLinks && typeof props.deepLinks.player === 'string') {
+ event.preventDefault();
+ window.location = props.deepLinks.player;
+ }
+ }, [props.deepLinks]);
+
return (
{
newVideos={newVideos}
options={options}
optionOnSelect={optionOnSelect}
+ onPlayClick={props.deepLinks && typeof props.deepLinks.player === 'string' ? onPlayClick : null}
/>
);
};
diff --git a/src/components/MetaItem/MetaItem.js b/src/components/MetaItem/MetaItem.js
index 1e19054c1..0e66c6a9e 100644
--- a/src/components/MetaItem/MetaItem.js
+++ b/src/components/MetaItem/MetaItem.js
@@ -18,14 +18,14 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, poste
const [menuOpen, onMenuOpen, onMenuClose] = useBinaryState(false);
const href = React.useMemo(() => {
return deepLinks ?
- typeof deepLinks.player === 'string' ?
- deepLinks.player
+ typeof deepLinks.metaDetailsStreams === 'string' ?
+ deepLinks.metaDetailsStreams
:
- typeof deepLinks.metaDetailsStreams === 'string' ?
- deepLinks.metaDetailsStreams
+ typeof deepLinks.metaDetailsVideos === 'string' ?
+ deepLinks.metaDetailsVideos
:
- typeof deepLinks.metaDetailsVideos === 'string' ?
- deepLinks.metaDetailsVideos
+ typeof deepLinks.player === 'string' ?
+ deepLinks.player
:
null
:
From ea69521912fa8d7f51c0460ad40937aa82e61cc1 Mon Sep 17 00:00:00 2001
From: Lachezar Lechev
Date: Fri, 17 Oct 2025 12:46:46 +0300
Subject: [PATCH 005/154] fix: metaDetails redirect when route ends with /
Signed-off-by: Lachezar Lechev
---
src/routes/MetaDetails/MetaDetails.js | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/routes/MetaDetails/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js
index d806ffed5..7d6f21d69 100644
--- a/src/routes/MetaDetails/MetaDetails.js
+++ b/src/routes/MetaDetails/MetaDetails.js
@@ -81,7 +81,11 @@ const MetaDetails = ({ urlParams, queryParams }) => {
const handleEpisodeSearch = React.useCallback((season, episode) => {
const searchVideoHash = encodeURIComponent(`${urlParams.id}:${season}:${episode}`);
const url = window.location.hash;
- const searchVideoPath = url.replace(encodeURIComponent(urlParams.videoId), searchVideoHash);
+
+ const searchVideoPath = (urlParams.videoId === undefined || urlParams.videoId === null || urlParams.videoId === '') ?
+ url + (!url.endsWith('/') ? '/' : '') + searchVideoHash
+ : url.replace(encodeURIComponent(urlParams.videoId), searchVideoHash);
+
window.location = searchVideoPath;
}, [urlParams, window.location]);
From 2de2e89446fac251c62d3d2b9f40a7b28ec58f15 Mon Sep 17 00:00:00 2001
From: Lachezar Lechev
Date: Fri, 17 Oct 2025 14:09:27 +0300
Subject: [PATCH 006/154] fix: meta details - don't set streamPath if videoId
is empty string - fix season selection path inconsistencies
Signed-off-by: Lachezar Lechev
---
src/routes/MetaDetails/EpisodePicker/EpisodePicker.tsx | 4 ++++
src/routes/MetaDetails/useMetaDetails.js | 2 +-
src/routes/MetaDetails/useSeason.js | 7 ++++++-
3 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/src/routes/MetaDetails/EpisodePicker/EpisodePicker.tsx b/src/routes/MetaDetails/EpisodePicker/EpisodePicker.tsx
index 256c827a9..d00429638 100644
--- a/src/routes/MetaDetails/EpisodePicker/EpisodePicker.tsx
+++ b/src/routes/MetaDetails/EpisodePicker/EpisodePicker.tsx
@@ -16,6 +16,10 @@ const EpisodePicker = ({ className, onSubmit }: Props) => {
const { initialSeason, initialEpisode } = useMemo(() => {
const splitPath = window.location.hash.split('/');
+ if (splitPath[splitPath.length - 1] === '') {
+ // remove the empty element
+ splitPath.pop();
+ }
const videoId = decodeURIComponent(splitPath[splitPath.length - 1]);
const [, pathSeason, pathEpisode] = videoId ? videoId.split(':') : [];
return {
diff --git a/src/routes/MetaDetails/useMetaDetails.js b/src/routes/MetaDetails/useMetaDetails.js
index c3790fddb..f86259345 100644
--- a/src/routes/MetaDetails/useMetaDetails.js
+++ b/src/routes/MetaDetails/useMetaDetails.js
@@ -48,7 +48,7 @@ const useMetaDetails = (urlParams) => {
id: urlParams.id,
extra: []
},
- streamPath: typeof urlParams.videoId === 'string' ?
+ streamPath: typeof urlParams.videoId === 'string' && urlParams.videoId !== '' ?
{
resource: 'stream',
type: urlParams.type,
diff --git a/src/routes/MetaDetails/useSeason.js b/src/routes/MetaDetails/useSeason.js
index 9d958a5cf..ec310c656 100644
--- a/src/routes/MetaDetails/useSeason.js
+++ b/src/routes/MetaDetails/useSeason.js
@@ -12,7 +12,12 @@ const useSeason = (urlParams, queryParams) => {
const setSeason = React.useCallback((season) => {
const nextQueryParams = new URLSearchParams(queryParams);
nextQueryParams.set('season', season);
- window.location.replace(`#${urlParams.path}?${nextQueryParams}`);
+ const path = urlParams.path.endsWith('/') ?
+ // remove the trailing /
+ urlParams.path.slice(0, -1):
+ urlParams.path;
+
+ window.location.replace(`#${path}?${nextQueryParams}`);
}, [urlParams, queryParams]);
return [season, setSeason];
};
From 6833bb719d3e894e6c511bec96a309cfd2f364c9 Mon Sep 17 00:00:00 2001
From: higorgoulart
Date: Fri, 31 Oct 2025 19:52:30 -0300
Subject: [PATCH 007/154] feat: watched on discover & details
---
src/components/DiscItem/DiscItem.js | 74 +++++++++++++++++++++++
src/components/DiscItem/index.js | 5 ++
src/components/MetaPreview/MetaPreview.js | 17 +++++-
src/components/index.ts | 2 +
src/routes/Discover/Discover.js | 46 +++++++++-----
src/routes/MetaDetails/MetaDetails.js | 28 +++++++++
6 files changed, 156 insertions(+), 16 deletions(-)
create mode 100644 src/components/DiscItem/DiscItem.js
create mode 100644 src/components/DiscItem/index.js
diff --git a/src/components/DiscItem/DiscItem.js b/src/components/DiscItem/DiscItem.js
new file mode 100644
index 000000000..45f6499a6
--- /dev/null
+++ b/src/components/DiscItem/DiscItem.js
@@ -0,0 +1,74 @@
+// Copyright (C) 2017-2023 Smart code 203358507
+
+const React = require('react');
+const { useServices } = require('stremio/services');
+const PropTypes = require('prop-types');
+const classnames = require('classnames');
+const MetaItem = require('stremio/components/MetaItem');
+const { t } = require('i18next');
+
+const DiscItem = ({ id, watched, selected, toggleWatched, ...props }) => {
+
+ const { core } = useServices();
+
+ const options = React.useMemo(() => {
+ return [
+ { label: watched ? 'CTX_MARK_UNWATCHED' : 'CTX_MARK_WATCHED', value: 'watched' },
+ ].filter(({ value }) => {
+ switch (value) {
+ case 'watched':
+ return props.deepLinks && (typeof props.deepLinks.metaDetailsVideos === 'string' || typeof props.deepLinks.metaDetailsStreams === 'string');
+ }
+ }).map((option) => ({
+ ...option,
+ label: t(option.label)
+ }));
+ }, [id, props.deepLinks, watched]);
+
+ const optionOnSelect = React.useCallback((event) => {
+ if (typeof props.optionOnSelect === 'function') {
+ props.optionOnSelect(event);
+ }
+
+ if (!event.nativeEvent.optionSelectPrevented) {
+ switch (event.value) {
+ case 'watched': {
+ if (typeof id === 'string') {
+ if (typeof toggleWatched === 'function') {
+ toggleWatched();
+ }
+ }
+
+ break;
+ }
+ }
+ }
+ }, [id, props.deepLinks, props.optionOnSelect]);
+
+ return (
+
+ );
+};
+
+DiscItem.propTypes = {
+ id: PropTypes.string,
+ removable: PropTypes.bool,
+ watched: PropTypes.bool,
+ selected: PropTypes.bool,
+ deepLinks: PropTypes.shape({
+ metaDetailsVideos: PropTypes.string,
+ metaDetailsStreams: PropTypes.string,
+ player: PropTypes.string
+ }),
+ toggleWatched: PropTypes.func,
+ optionOnSelect: PropTypes.func
+};
+
+module.exports = DiscItem;
diff --git a/src/components/DiscItem/index.js b/src/components/DiscItem/index.js
new file mode 100644
index 000000000..f0fe335c2
--- /dev/null
+++ b/src/components/DiscItem/index.js
@@ -0,0 +1,5 @@
+// Copyright (C) 2017-2023 Smart code 203358507
+
+const DiscItem = require('./DiscItem');
+
+module.exports = DiscItem;
diff --git a/src/components/MetaPreview/MetaPreview.js b/src/components/MetaPreview/MetaPreview.js
index 13717919a..e4ec82ded 100644
--- a/src/components/MetaPreview/MetaPreview.js
+++ b/src/components/MetaPreview/MetaPreview.js
@@ -25,7 +25,7 @@ const ALLOWED_LINK_REDIRECTS = [
routesRegexp.metadetails.regexp
];
-const MetaPreview = React.forwardRef(({ className, compact, name, logo, background, runtime, releaseInfo, released, description, deepLinks, links, trailerStreams, inLibrary, toggleInLibrary, ratingInfo }, ref) => {
+const MetaPreview = React.forwardRef(({ className, compact, name, logo, background, runtime, releaseInfo, released, description, deepLinks, links, trailerStreams, inLibrary, toggleInLibrary, watched, toggleWatched, ratingInfo }, ref) => {
const { t } = useTranslation();
const [shareModalOpen, openShareModal, closeShareModal] = useBinaryState(false);
const linksGroups = React.useMemo(() => {
@@ -221,6 +221,19 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
:
null
}
+ {
+ typeof toggleWatched === 'function' ?
+
+ :
+ null
+ }
{
typeof showHref === 'string' && compact ?
{
}
});
}, [selectedMetaItem]);
+ const toggleWatched = React.useCallback(() => {
+ if (selectedMetaItem === null) {
+ return;
+ }
+
+ if (!selectedMetaItem.inLibrary) {
+ core.transport.dispatch({
+ action: 'Ctx',
+ args: {
+ action: 'AddToLibrary',
+ args: selectedMetaItem
+ }
+ });
+ }
+
+ core.transport.dispatch({
+ action: 'Ctx',
+ args: {
+ action: 'LibraryItemMarkAsWatched',
+ args: {
+ id: selectedMetaItem.id,
+ is_watched: !selectedMetaItem.watched
+ }
+ }
+ });
+ }, [selectedMetaItem]);
const metaItemsOnFocusCapture = React.useCallback((event) => {
if (event.target.dataset.index !== null && !isNaN(event.target.dataset.index)) {
setSelectedMetaItemIndex(parseInt(event.target.dataset.index, 10));
@@ -157,20 +183,8 @@ const Discover = ({ urlParams, queryParams }) => {
:
- {discover.catalog.content.content.map((metaItem, index) => (
-
+ {discover.catalog.content.content.map((discItem, index) => (
+
))}
}
@@ -193,6 +207,8 @@ const Discover = ({ urlParams, queryParams }) => {
trailerStreams={selectedMetaItem.trailerStreams}
inLibrary={selectedMetaItem.inLibrary}
toggleInLibrary={selectedMetaItem.inLibrary ? removeFromLibrary : addToLibrary}
+ watched={selectedMetaItem.watched}
+ toggleWatched={toggleWatched}
metaId={selectedMetaItem.id}
like={selectedMetaItem.like}
/>
diff --git a/src/routes/MetaDetails/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js
index fd27478b5..478e59bee 100644
--- a/src/routes/MetaDetails/MetaDetails.js
+++ b/src/routes/MetaDetails/MetaDetails.js
@@ -64,6 +64,32 @@ const MetaDetails = ({ urlParams, queryParams }) => {
}
});
}, [metaDetails]);
+ const toggleWatched = React.useCallback(() => {
+ if (metaDetails.metaItem.content.content === null || metaDetails.metaItem.content.type !== 'Ready') {
+ return;
+ }
+
+ if (!metaDetails.metaItem.content.content.inLibrary) {
+ core.transport.dispatch({
+ action: 'Ctx',
+ args: {
+ action: 'AddToLibrary',
+ args: metaDetails.metaItem.content.content
+ }
+ });
+ }
+
+ core.transport.dispatch({
+ action: 'Ctx',
+ args: {
+ action: 'LibraryItemMarkAsWatched',
+ args: {
+ id: metaDetails.metaItem.content.content.id,
+ is_watched: !metaDetails.metaItem.content.content.watched
+ }
+ }
+ });
+ }, [metaDetails]);
const toggleNotifications = React.useCallback(() => {
if (metaDetails.libraryItem) {
core.transport.dispatch({
@@ -168,6 +194,8 @@ const MetaDetails = ({ urlParams, queryParams }) => {
trailerStreams={metaDetails.metaItem.content.content.trailerStreams}
inLibrary={metaDetails.metaItem.content.content.inLibrary}
toggleInLibrary={metaDetails.metaItem.content.content.inLibrary ? removeFromLibrary : addToLibrary}
+ watched={metaDetails.metaItem.content.content.watched}
+ toggleWatched={toggleWatched}
metaId={metaDetails.metaItem.content.content.id}
ratingInfo={metaDetails.ratingInfo}
/>
From 852f478f1ee400436967090b01205fbe8747f850 Mon Sep 17 00:00:00 2001
From: higorgoulart
Date: Tue, 4 Nov 2025 17:52:09 -0300
Subject: [PATCH 008/154] feat: change trailer order & fix discover mark as
watched
---
src/components/DiscItem/DiscItem.js | 5 +----
src/components/MetaPreview/MetaPreview.js | 26 +++++++++++------------
src/routes/Discover/Discover.js | 18 ++++++++--------
3 files changed, 23 insertions(+), 26 deletions(-)
diff --git a/src/components/DiscItem/DiscItem.js b/src/components/DiscItem/DiscItem.js
index 45f6499a6..cc1cf5315 100644
--- a/src/components/DiscItem/DiscItem.js
+++ b/src/components/DiscItem/DiscItem.js
@@ -1,15 +1,12 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
-const { useServices } = require('stremio/services');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const MetaItem = require('stremio/components/MetaItem');
const { t } = require('i18next');
-const DiscItem = ({ id, watched, selected, toggleWatched, ...props }) => {
-
- const { core } = useServices();
+const DiscItem = ({ id, watched, selected, toggleWatched, select, ...props }) => {
const options = React.useMemo(() => {
return [
diff --git a/src/components/MetaPreview/MetaPreview.js b/src/components/MetaPreview/MetaPreview.js
index e4ec82ded..829903cfa 100644
--- a/src/components/MetaPreview/MetaPreview.js
+++ b/src/components/MetaPreview/MetaPreview.js
@@ -195,19 +195,6 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
}
- {
- typeof toggleInLibrary === 'function' ?
-
- :
- null
- }
{
typeof trailerHref === 'string' ?
+ :
+ null
+ }
{
typeof toggleWatched === 'function' ?
{
}
});
}, [selectedMetaItem]);
- const toggleWatched = React.useCallback(() => {
- if (selectedMetaItem === null) {
+ const toggleWatched = React.useCallback((item) => {
+ if (item === null) {
return;
}
- if (!selectedMetaItem.inLibrary) {
+ if (!item.inLibrary) {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'AddToLibrary',
- args: selectedMetaItem
+ args: item
}
});
}
@@ -94,12 +94,12 @@ const Discover = ({ urlParams, queryParams }) => {
args: {
action: 'LibraryItemMarkAsWatched',
args: {
- id: selectedMetaItem.id,
- is_watched: !selectedMetaItem.watched
+ id: item.id,
+ is_watched: !item.watched
}
}
});
- }, [selectedMetaItem]);
+ }, []);
const metaItemsOnFocusCapture = React.useCallback((event) => {
if (event.target.dataset.index !== null && !isNaN(event.target.dataset.index)) {
setSelectedMetaItemIndex(parseInt(event.target.dataset.index, 10));
@@ -184,7 +184,7 @@ const Discover = ({ urlParams, queryParams }) => {
:
{discover.catalog.content.content.map((discItem, index) => (
-
+ toggleWatched(discItem)} selected={selectedMetaItemIndex === index} key={index} data-index={index} onClick={metaItemOnClick} />
))}
}
@@ -208,7 +208,7 @@ const Discover = ({ urlParams, queryParams }) => {
inLibrary={selectedMetaItem.inLibrary}
toggleInLibrary={selectedMetaItem.inLibrary ? removeFromLibrary : addToLibrary}
watched={selectedMetaItem.watched}
- toggleWatched={toggleWatched}
+ toggleWatched={() => toggleWatched(selectedMetaItem)}
metaId={selectedMetaItem.id}
like={selectedMetaItem.like}
/>
From 3b2d1f365c89bb146f6c9c852a4e6750a6cbdf49 Mon Sep 17 00:00:00 2001
From: higorgoulart
Date: Tue, 4 Nov 2025 19:23:39 -0300
Subject: [PATCH 009/154] feat: icons group component
---
src/components/DiscItem/DiscItem.js | 2 +-
.../IconsGroup.less} | 6 ++-
src/components/IconsGroup/IconsGroup.tsx | 36 ++++++++++++++
src/components/IconsGroup/index.ts | 6 +++
src/components/MetaPreview/MetaPreview.js | 48 +++++++------------
.../MetaPreview/Ratings/Ratings.tsx | 27 ++++++-----
src/components/MetaPreview/styles.less | 5 --
7 files changed, 78 insertions(+), 52 deletions(-)
rename src/components/{MetaPreview/Ratings/Ratings.less => IconsGroup/IconsGroup.less} (92%)
create mode 100644 src/components/IconsGroup/IconsGroup.tsx
create mode 100644 src/components/IconsGroup/index.ts
diff --git a/src/components/DiscItem/DiscItem.js b/src/components/DiscItem/DiscItem.js
index cc1cf5315..d09e35380 100644
--- a/src/components/DiscItem/DiscItem.js
+++ b/src/components/DiscItem/DiscItem.js
@@ -6,7 +6,7 @@ const classnames = require('classnames');
const MetaItem = require('stremio/components/MetaItem');
const { t } = require('i18next');
-const DiscItem = ({ id, watched, selected, toggleWatched, select, ...props }) => {
+const DiscItem = ({ id, watched, selected, toggleWatched, ...props }) => {
const options = React.useMemo(() => {
return [
diff --git a/src/components/MetaPreview/Ratings/Ratings.less b/src/components/IconsGroup/IconsGroup.less
similarity index 92%
rename from src/components/MetaPreview/Ratings/Ratings.less
rename to src/components/IconsGroup/IconsGroup.less
index afe7b3637..8e09c9f93 100644
--- a/src/components/MetaPreview/Ratings/Ratings.less
+++ b/src/components/IconsGroup/IconsGroup.less
@@ -8,7 +8,7 @@
@width-mobile: 3rem;
-.ratings-container {
+.group-container {
display: flex;
flex-direction: row;
align-items: center;
@@ -17,6 +17,8 @@
border-radius: 2rem;
height: @height;
width: fit-content;
+ margin-bottom: 1rem;
+ margin-right: 1rem;
.icon-container {
display: flex;
@@ -45,7 +47,7 @@
}
@media @phone-landscape {
- .ratings-container {
+ .group-container {
height: @height-mobile;
.icon-container {
diff --git a/src/components/IconsGroup/IconsGroup.tsx b/src/components/IconsGroup/IconsGroup.tsx
new file mode 100644
index 000000000..a667592de
--- /dev/null
+++ b/src/components/IconsGroup/IconsGroup.tsx
@@ -0,0 +1,36 @@
+// Copyright (C) 2017-2023 Smart code 203358507
+
+import classNames from 'classnames';
+import React from 'react';
+import Icon from '@stremio/stremio-icons/react';
+import styles from './IconsGroup.less';
+
+type GroupItem = {
+ icon: string;
+ filled?: string;
+ disabled?: boolean;
+ className?: string;
+ onClick?: () => void;
+};
+
+type Props = {
+ items: GroupItem[];
+ className?: string;
+};
+
+const IconsGroup = ({ items, className }: Props) => {
+ return (
+
+ {items.map((item, index) => (
+
+
+
+ ))}
+
+ );
+};
+
+export default IconsGroup;
diff --git a/src/components/IconsGroup/index.ts b/src/components/IconsGroup/index.ts
new file mode 100644
index 000000000..4407a8c6f
--- /dev/null
+++ b/src/components/IconsGroup/index.ts
@@ -0,0 +1,6 @@
+// Copyright (C) 2017-2023 Smart code 203358507
+
+import IconsGroup from './IconsGroup';
+
+export { IconsGroup };
+
diff --git a/src/components/MetaPreview/MetaPreview.js b/src/components/MetaPreview/MetaPreview.js
index 829903cfa..5cedaa55e 100644
--- a/src/components/MetaPreview/MetaPreview.js
+++ b/src/components/MetaPreview/MetaPreview.js
@@ -8,6 +8,7 @@ const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { default: Button } = require('stremio/components/Button');
const { default: Image } = require('stremio/components/Image');
+const { IconsGroup } = require('stremio/components/IconsGroup');
const ModalDialog = require('stremio/components/ModalDialog');
const SharePrompt = require('stremio/components/SharePrompt');
const CONSTANTS = require('stremio/common/CONSTANTS');
@@ -98,6 +99,16 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
const renderLogoFallback = React.useCallback(() => (
{name}
), [name]);
+ const libAndWatchedGroup = React.useMemo(() => [
+ {
+ icon: inLibrary ? 'remove-from-library' : 'add-to-library',
+ onClick: typeof toggleInLibrary === 'function' ? toggleInLibrary : null,
+ },
+ {
+ icon: watched ? 'eye-off' : 'eye',
+ onClick: typeof toggleWatched === 'function' ? toggleWatched : undefined,
+ },
+ ], [inLibrary, watched, toggleInLibrary, toggleWatched]);
return (
{
@@ -209,30 +220,9 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
null
}
{
- typeof toggleInLibrary === 'function' ?
-
- :
- null
- }
- {
- typeof toggleWatched === 'function' ?
-
- :
- null
+ typeof toggleInLibrary === 'function' && typeof toggleWatched === 'function'
+ ?
+ : null
}
{
typeof showHref === 'string' && compact ?
@@ -247,13 +237,9 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
null
}
{
- !compact && ratingInfo !== null ?
-
- :
- null
+ !compact && ratingInfo !== null
+ ?
+ : null
}
{
linksGroups.has(CONSTANTS.SHARE_LINK_CATEGORY) && !compact ?
diff --git a/src/components/MetaPreview/Ratings/Ratings.tsx b/src/components/MetaPreview/Ratings/Ratings.tsx
index 6bef0cc6d..49ed77ba1 100644
--- a/src/components/MetaPreview/Ratings/Ratings.tsx
+++ b/src/components/MetaPreview/Ratings/Ratings.tsx
@@ -2,9 +2,7 @@
import React, { useMemo } from 'react';
import useRating from './useRating';
-import styles from './Ratings.less';
-import Icon from '@stremio/stremio-icons/react';
-import classNames from 'classnames';
+import { IconsGroup } from 'stremio/components/IconsGroup';
type Props = {
metaId?: string;
@@ -15,17 +13,20 @@ type Props = {
const Ratings = ({ ratingInfo, className }: Props) => {
const { onLiked, onLoved, liked, loved } = useRating(ratingInfo);
const disabled = useMemo(() => ratingInfo?.type !== 'Ready', [ratingInfo]);
+ const items = useMemo(() => [
+ {
+ icon: liked ? 'thumbs-up' : 'thumbs-up-outline',
+ disabled,
+ onClick: onLiked,
+ },
+ {
+ icon: loved ? 'heart' : 'heart-outline',
+ disabled,
+ onClick: onLoved,
+ },
+ ], [liked, loved, disabled, onLiked, onLoved]);
- return (
-
- );
+ return
;
};
export default Ratings;
diff --git a/src/components/MetaPreview/styles.less b/src/components/MetaPreview/styles.less
index 3fea95a5f..9347552a0 100644
--- a/src/components/MetaPreview/styles.less
+++ b/src/components/MetaPreview/styles.less
@@ -208,11 +208,6 @@
}
}
}
-
- .ratings {
- margin-bottom: 1rem;
- margin-right: 1rem;
- }
}
.share-prompt {
From ff08e377fc3e60869d8a1509eb04cec45c69e1b8 Mon Sep 17 00:00:00 2001
From: higorgoulart
Date: Fri, 7 Nov 2025 17:57:06 -0300
Subject: [PATCH 010/154] feat: remove unused component & fix spacing
---
src/components/DiscItem/DiscItem.js | 71 -----------------------
src/components/DiscItem/index.js | 5 --
src/components/IconsGroup/IconsGroup.less | 2 -
src/components/IconsGroup/IconsGroup.tsx | 3 +
src/components/MetaPreview/MetaPreview.js | 6 +-
src/components/MetaPreview/styles.less | 18 +++++-
src/components/index.ts | 2 -
src/routes/Discover/Discover.js | 18 +++++-
8 files changed, 38 insertions(+), 87 deletions(-)
delete mode 100644 src/components/DiscItem/DiscItem.js
delete mode 100644 src/components/DiscItem/index.js
diff --git a/src/components/DiscItem/DiscItem.js b/src/components/DiscItem/DiscItem.js
deleted file mode 100644
index d09e35380..000000000
--- a/src/components/DiscItem/DiscItem.js
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (C) 2017-2023 Smart code 203358507
-
-const React = require('react');
-const PropTypes = require('prop-types');
-const classnames = require('classnames');
-const MetaItem = require('stremio/components/MetaItem');
-const { t } = require('i18next');
-
-const DiscItem = ({ id, watched, selected, toggleWatched, ...props }) => {
-
- const options = React.useMemo(() => {
- return [
- { label: watched ? 'CTX_MARK_UNWATCHED' : 'CTX_MARK_WATCHED', value: 'watched' },
- ].filter(({ value }) => {
- switch (value) {
- case 'watched':
- return props.deepLinks && (typeof props.deepLinks.metaDetailsVideos === 'string' || typeof props.deepLinks.metaDetailsStreams === 'string');
- }
- }).map((option) => ({
- ...option,
- label: t(option.label)
- }));
- }, [id, props.deepLinks, watched]);
-
- const optionOnSelect = React.useCallback((event) => {
- if (typeof props.optionOnSelect === 'function') {
- props.optionOnSelect(event);
- }
-
- if (!event.nativeEvent.optionSelectPrevented) {
- switch (event.value) {
- case 'watched': {
- if (typeof id === 'string') {
- if (typeof toggleWatched === 'function') {
- toggleWatched();
- }
- }
-
- break;
- }
- }
- }
- }, [id, props.deepLinks, props.optionOnSelect]);
-
- return (
-
- );
-};
-
-DiscItem.propTypes = {
- id: PropTypes.string,
- removable: PropTypes.bool,
- watched: PropTypes.bool,
- selected: PropTypes.bool,
- deepLinks: PropTypes.shape({
- metaDetailsVideos: PropTypes.string,
- metaDetailsStreams: PropTypes.string,
- player: PropTypes.string
- }),
- toggleWatched: PropTypes.func,
- optionOnSelect: PropTypes.func
-};
-
-module.exports = DiscItem;
diff --git a/src/components/DiscItem/index.js b/src/components/DiscItem/index.js
deleted file mode 100644
index f0fe335c2..000000000
--- a/src/components/DiscItem/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright (C) 2017-2023 Smart code 203358507
-
-const DiscItem = require('./DiscItem');
-
-module.exports = DiscItem;
diff --git a/src/components/IconsGroup/IconsGroup.less b/src/components/IconsGroup/IconsGroup.less
index 8e09c9f93..25e1b90c0 100644
--- a/src/components/IconsGroup/IconsGroup.less
+++ b/src/components/IconsGroup/IconsGroup.less
@@ -17,8 +17,6 @@
border-radius: 2rem;
height: @height;
width: fit-content;
- margin-bottom: 1rem;
- margin-right: 1rem;
.icon-container {
display: flex;
diff --git a/src/components/IconsGroup/IconsGroup.tsx b/src/components/IconsGroup/IconsGroup.tsx
index a667592de..733560e43 100644
--- a/src/components/IconsGroup/IconsGroup.tsx
+++ b/src/components/IconsGroup/IconsGroup.tsx
@@ -4,9 +4,11 @@ import classNames from 'classnames';
import React from 'react';
import Icon from '@stremio/stremio-icons/react';
import styles from './IconsGroup.less';
+import { Tooltip } from 'stremio/common/Tooltips';
type GroupItem = {
icon: string;
+ label?: string;
filled?: string;
disabled?: boolean;
className?: string;
@@ -26,6 +28,7 @@ const IconsGroup = ({ items, className }: Props) => {
className={classNames(styles['icon-container'], item.className, { [styles['disabled']]: item.disabled })}
onClick={item.onClick}
>
+ {item.label && }
))}
diff --git a/src/components/MetaPreview/MetaPreview.js b/src/components/MetaPreview/MetaPreview.js
index 5cedaa55e..e4bc6448b 100644
--- a/src/components/MetaPreview/MetaPreview.js
+++ b/src/components/MetaPreview/MetaPreview.js
@@ -102,10 +102,12 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
const libAndWatchedGroup = React.useMemo(() => [
{
icon: inLibrary ? 'remove-from-library' : 'add-to-library',
+ label: inLibrary ? t('REMOVE_FROM_LIB') : t('ADD_TO_LIB'),
onClick: typeof toggleInLibrary === 'function' ? toggleInLibrary : null,
},
{
icon: watched ? 'eye-off' : 'eye',
+ label: watched ? t('CTX_MARK_UNWATCHED') : t('CTX_MARK_WATCHED'),
onClick: typeof toggleWatched === 'function' ? toggleWatched : undefined,
},
], [inLibrary, watched, toggleInLibrary, toggleWatched]);
@@ -221,7 +223,7 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
}
{
typeof toggleInLibrary === 'function' && typeof toggleWatched === 'function'
- ?
+ ?
: null
}
{
@@ -238,7 +240,7 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
}
{
!compact && ratingInfo !== null
- ?
+ ?
: null
}
{
diff --git a/src/components/MetaPreview/styles.less b/src/components/MetaPreview/styles.less
index 9347552a0..81883abbe 100644
--- a/src/components/MetaPreview/styles.less
+++ b/src/components/MetaPreview/styles.less
@@ -32,7 +32,7 @@
.action-buttons-container {
justify-content: space-between;
- .action-button:not(:last-child) {
+ .action-button:not(:last-child), .group-container:not(:last-child) {
margin-right: 0;
}
}
@@ -207,6 +207,20 @@
}
}
}
+
+ .group-container {
+ margin-bottom: 1rem;
+
+ &:global(.wide) {
+ width: auto;
+ padding: 0 2rem;
+ border-radius: 4rem;
+ }
+
+ &:not(:last-child) {
+ margin-right: 1rem;
+ }
+ }
}
}
@@ -228,7 +242,7 @@
padding-top: 1.5rem;
gap: 0.5rem;
- .action-button {
+ .action-button, .group-container {
padding: 0 1.5rem !important;
margin-right: 0rem !important;
height: 3rem;
diff --git a/src/components/index.ts b/src/components/index.ts
index 373faf1de..a47c2c709 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -11,7 +11,6 @@ import EventModal from './EventModal';
import HorizontalScroll from './HorizontalScroll';
import Image from './Image';
import LibItem from './LibItem';
-import DiscItem from './DiscItem';
import MainNavBars from './MainNavBars';
import MetaItem from './MetaItem';
import MetaPreview from './MetaPreview';
@@ -46,7 +45,6 @@ export {
HorizontalScroll,
Image,
LibItem,
- DiscItem,
MainNavBars,
MetaItem,
MetaPreview,
diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js
index 29dc2cfe1..6675d6d30 100644
--- a/src/routes/Discover/Discover.js
+++ b/src/routes/Discover/Discover.js
@@ -7,7 +7,7 @@ const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { useServices } = require('stremio/services');
const { CONSTANTS, useBinaryState, useOnScrollToBottom, withCoreSuspender } = require('stremio/common');
-const { AddonDetailsModal, Button, DelayedRenderer, Image, MainNavBars, DiscItem, MetaPreview, ModalDialog, MultiselectMenu } = require('stremio/components');
+const { AddonDetailsModal, Button, DelayedRenderer, Image, MainNavBars, MetaItem, MetaPreview, ModalDialog, MultiselectMenu } = require('stremio/components');
const useDiscover = require('./useDiscover');
const useSelectableInputs = require('./useSelectableInputs');
const styles = require('./styles');
@@ -183,8 +183,20 @@ const Discover = ({ urlParams, queryParams }) => {
:
- {discover.catalog.content.content.map((discItem, index) => (
- toggleWatched(discItem)} selected={selectedMetaItemIndex === index} key={index} data-index={index} onClick={metaItemOnClick} />
+ {discover.catalog.content.content.map((metaItem, index) => (
+
))}
}
From 987201edd32811a650b563be2f3d04604aa6a3a4 Mon Sep 17 00:00:00 2001
From: higorgoulart
Date: Sat, 8 Nov 2025 13:59:10 -0300
Subject: [PATCH 011/154] feat: review facts
---
src/components/MetaPreview/MetaPreview.js | 14 +++++++++-----
src/routes/Discover/Discover.js | 16 ++++++++--------
2 files changed, 17 insertions(+), 13 deletions(-)
diff --git a/src/components/MetaPreview/MetaPreview.js b/src/components/MetaPreview/MetaPreview.js
index e4bc6448b..e50d6c604 100644
--- a/src/components/MetaPreview/MetaPreview.js
+++ b/src/components/MetaPreview/MetaPreview.js
@@ -99,7 +99,7 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
const renderLogoFallback = React.useCallback(() => (
{name}
), [name]);
- const libAndWatchedGroup = React.useMemo(() => [
+ const metaItemActions = React.useMemo(() => [
{
icon: inLibrary ? 'remove-from-library' : 'add-to-library',
label: inLibrary ? t('REMOVE_FROM_LIB') : t('ADD_TO_LIB'),
@@ -223,7 +223,7 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
}
{
typeof toggleInLibrary === 'function' && typeof toggleWatched === 'function'
- ?
+ ?
: null
}
{
@@ -239,9 +239,13 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
null
}
{
- !compact && ratingInfo !== null
- ?
- : null
+ !compact && ratingInfo !== null ?
+
+ :
+ null
}
{
linksGroups.has(CONSTANTS.SHARE_LINK_CATEGORY) && !compact ?
diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js
index 6675d6d30..be28eeafd 100644
--- a/src/routes/Discover/Discover.js
+++ b/src/routes/Discover/Discover.js
@@ -74,17 +74,17 @@ const Discover = ({ urlParams, queryParams }) => {
}
});
}, [selectedMetaItem]);
- const toggleWatched = React.useCallback((item) => {
- if (item === null) {
+ const toggleWatched = React.useCallback(() => {
+ if (selectedMetaItem === null) {
return;
}
- if (!item.inLibrary) {
+ if (!selectedMetaItem.inLibrary) {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'AddToLibrary',
- args: item
+ args: selectedMetaItem
}
});
}
@@ -94,12 +94,12 @@ const Discover = ({ urlParams, queryParams }) => {
args: {
action: 'LibraryItemMarkAsWatched',
args: {
- id: item.id,
- is_watched: !item.watched
+ id: selectedMetaItem.id,
+ is_watched: !selectedMetaItem.watched
}
}
});
- }, []);
+ }, [selectedMetaItem]);
const metaItemsOnFocusCapture = React.useCallback((event) => {
if (event.target.dataset.index !== null && !isNaN(event.target.dataset.index)) {
setSelectedMetaItemIndex(parseInt(event.target.dataset.index, 10));
@@ -220,7 +220,7 @@ const Discover = ({ urlParams, queryParams }) => {
inLibrary={selectedMetaItem.inLibrary}
toggleInLibrary={selectedMetaItem.inLibrary ? removeFromLibrary : addToLibrary}
watched={selectedMetaItem.watched}
- toggleWatched={() => toggleWatched(selectedMetaItem)}
+ toggleWatched={toggleWatched}
metaId={selectedMetaItem.id}
like={selectedMetaItem.like}
/>
From 373ccf351ad4835339d66a7bbd8a5afb47f12f2c Mon Sep 17 00:00:00 2001
From: higorgoulart
Date: Sat, 8 Nov 2025 14:00:01 -0300
Subject: [PATCH 012/154] feat: review facts
---
src/components/IconsGroup/IconsGroup.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/IconsGroup/IconsGroup.tsx b/src/components/IconsGroup/IconsGroup.tsx
index 733560e43..041c01139 100644
--- a/src/components/IconsGroup/IconsGroup.tsx
+++ b/src/components/IconsGroup/IconsGroup.tsx
@@ -6,7 +6,7 @@ import Icon from '@stremio/stremio-icons/react';
import styles from './IconsGroup.less';
import { Tooltip } from 'stremio/common/Tooltips';
-type GroupItem = {
+type Item = {
icon: string;
label?: string;
filled?: string;
@@ -16,7 +16,7 @@ type GroupItem = {
};
type Props = {
- items: GroupItem[];
+ items: Item[];
className?: string;
};
From 97c3b7d004664024a8ac0b788880d5883f655449 Mon Sep 17 00:00:00 2001
From: higorgoulart
Date: Tue, 11 Nov 2025 17:30:00 -0300
Subject: [PATCH 013/154] feat: rename component & fix style
---
.../IconsGroup.less => ActionsGroup/ActionsGroup.less} | 0
.../IconsGroup.tsx => ActionsGroup/ActionsGroup.tsx} | 6 +++---
src/components/ActionsGroup/index.ts | 6 ++++++
src/components/IconsGroup/index.ts | 6 ------
src/components/MetaPreview/MetaPreview.js | 6 +++---
src/components/MetaPreview/Ratings/Ratings.tsx | 4 ++--
src/components/MetaPreview/styles.less | 4 ----
7 files changed, 14 insertions(+), 18 deletions(-)
rename src/components/{IconsGroup/IconsGroup.less => ActionsGroup/ActionsGroup.less} (100%)
rename src/components/{IconsGroup/IconsGroup.tsx => ActionsGroup/ActionsGroup.tsx} (88%)
create mode 100644 src/components/ActionsGroup/index.ts
delete mode 100644 src/components/IconsGroup/index.ts
diff --git a/src/components/IconsGroup/IconsGroup.less b/src/components/ActionsGroup/ActionsGroup.less
similarity index 100%
rename from src/components/IconsGroup/IconsGroup.less
rename to src/components/ActionsGroup/ActionsGroup.less
diff --git a/src/components/IconsGroup/IconsGroup.tsx b/src/components/ActionsGroup/ActionsGroup.tsx
similarity index 88%
rename from src/components/IconsGroup/IconsGroup.tsx
rename to src/components/ActionsGroup/ActionsGroup.tsx
index 041c01139..244786c2f 100644
--- a/src/components/IconsGroup/IconsGroup.tsx
+++ b/src/components/ActionsGroup/ActionsGroup.tsx
@@ -3,7 +3,7 @@
import classNames from 'classnames';
import React from 'react';
import Icon from '@stremio/stremio-icons/react';
-import styles from './IconsGroup.less';
+import styles from './ActionsGroup.less';
import { Tooltip } from 'stremio/common/Tooltips';
type Item = {
@@ -20,7 +20,7 @@ type Props = {
className?: string;
};
-const IconsGroup = ({ items, className }: Props) => {
+const ActionsGroup = ({ items, className }: Props) => {
return (
{items.map((item, index) => (
@@ -36,4 +36,4 @@ const IconsGroup = ({ items, className }: Props) => {
);
};
-export default IconsGroup;
+export default ActionsGroup;
diff --git a/src/components/ActionsGroup/index.ts b/src/components/ActionsGroup/index.ts
new file mode 100644
index 000000000..2e83d4f64
--- /dev/null
+++ b/src/components/ActionsGroup/index.ts
@@ -0,0 +1,6 @@
+// Copyright (C) 2017-2023 Smart code 203358507
+
+import ActionsGroup from './ActionsGroup';
+
+export { ActionsGroup };
+
diff --git a/src/components/IconsGroup/index.ts b/src/components/IconsGroup/index.ts
deleted file mode 100644
index 4407a8c6f..000000000
--- a/src/components/IconsGroup/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-// Copyright (C) 2017-2023 Smart code 203358507
-
-import IconsGroup from './IconsGroup';
-
-export { IconsGroup };
-
diff --git a/src/components/MetaPreview/MetaPreview.js b/src/components/MetaPreview/MetaPreview.js
index e50d6c604..60f4e0681 100644
--- a/src/components/MetaPreview/MetaPreview.js
+++ b/src/components/MetaPreview/MetaPreview.js
@@ -8,7 +8,7 @@ const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { default: Button } = require('stremio/components/Button');
const { default: Image } = require('stremio/components/Image');
-const { IconsGroup } = require('stremio/components/IconsGroup');
+const { ActionsGroup } = require('stremio/components/ActionsGroup');
const ModalDialog = require('stremio/components/ModalDialog');
const SharePrompt = require('stremio/components/SharePrompt');
const CONSTANTS = require('stremio/common/CONSTANTS');
@@ -223,7 +223,7 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
}
{
typeof toggleInLibrary === 'function' && typeof toggleWatched === 'function'
- ?
+ ?
: null
}
{
@@ -242,7 +242,7 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
!compact && ratingInfo !== null ?
:
null
diff --git a/src/components/MetaPreview/Ratings/Ratings.tsx b/src/components/MetaPreview/Ratings/Ratings.tsx
index 49ed77ba1..28de12b26 100644
--- a/src/components/MetaPreview/Ratings/Ratings.tsx
+++ b/src/components/MetaPreview/Ratings/Ratings.tsx
@@ -2,7 +2,7 @@
import React, { useMemo } from 'react';
import useRating from './useRating';
-import { IconsGroup } from 'stremio/components/IconsGroup';
+import { ActionsGroup } from 'stremio/components/ActionsGroup';
type Props = {
metaId?: string;
@@ -26,7 +26,7 @@ const Ratings = ({ ratingInfo, className }: Props) => {
},
], [liked, loved, disabled, onLiked, onLoved]);
- return
;
+ return
;
};
export default Ratings;
diff --git a/src/components/MetaPreview/styles.less b/src/components/MetaPreview/styles.less
index 81883abbe..38fb641f7 100644
--- a/src/components/MetaPreview/styles.less
+++ b/src/components/MetaPreview/styles.less
@@ -249,10 +249,6 @@
border-radius: 2rem;
}
}
-
- .ratings {
- margin-right: 0;
- }
}
}
From 67f4f349bb2158cf203e0969eb068962c45dbb2c Mon Sep 17 00:00:00 2001
From: higorgoulart
Date: Tue, 11 Nov 2025 18:48:24 -0300
Subject: [PATCH 014/154] feat: remove add to library
---
src/routes/Discover/Discover.js | 10 ----------
src/routes/MetaDetails/MetaDetails.js | 10 ----------
2 files changed, 20 deletions(-)
diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js
index be28eeafd..3560dcb2b 100644
--- a/src/routes/Discover/Discover.js
+++ b/src/routes/Discover/Discover.js
@@ -79,16 +79,6 @@ const Discover = ({ urlParams, queryParams }) => {
return;
}
- if (!selectedMetaItem.inLibrary) {
- core.transport.dispatch({
- action: 'Ctx',
- args: {
- action: 'AddToLibrary',
- args: selectedMetaItem
- }
- });
- }
-
core.transport.dispatch({
action: 'Ctx',
args: {
diff --git a/src/routes/MetaDetails/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js
index 478e59bee..0825f4287 100644
--- a/src/routes/MetaDetails/MetaDetails.js
+++ b/src/routes/MetaDetails/MetaDetails.js
@@ -69,16 +69,6 @@ const MetaDetails = ({ urlParams, queryParams }) => {
return;
}
- if (!metaDetails.metaItem.content.content.inLibrary) {
- core.transport.dispatch({
- action: 'Ctx',
- args: {
- action: 'AddToLibrary',
- args: metaDetails.metaItem.content.content
- }
- });
- }
-
core.transport.dispatch({
action: 'Ctx',
args: {
From 9ccc6b8271657fbc70d6be98304aabfaf3420392 Mon Sep 17 00:00:00 2001
From: higorgoulart
Date: Wed, 12 Nov 2025 18:37:41 -0300
Subject: [PATCH 015/154] feat: change metaDetails action
---
src/components/MetaPreview/styles.less | 4 ++++
src/routes/Discover/Discover.js | 10 ++++++++++
src/routes/MetaDetails/MetaDetails.js | 11 ++++-------
3 files changed, 18 insertions(+), 7 deletions(-)
diff --git a/src/components/MetaPreview/styles.less b/src/components/MetaPreview/styles.less
index 38fb641f7..3b21c0ed6 100644
--- a/src/components/MetaPreview/styles.less
+++ b/src/components/MetaPreview/styles.less
@@ -277,6 +277,10 @@
&::-webkit-scrollbar {
display: none;
}
+
+ .action-button {
+ padding: 0 1rem !important;
+ }
}
}
diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js
index 3560dcb2b..be28eeafd 100644
--- a/src/routes/Discover/Discover.js
+++ b/src/routes/Discover/Discover.js
@@ -79,6 +79,16 @@ const Discover = ({ urlParams, queryParams }) => {
return;
}
+ if (!selectedMetaItem.inLibrary) {
+ core.transport.dispatch({
+ action: 'Ctx',
+ args: {
+ action: 'AddToLibrary',
+ args: selectedMetaItem
+ }
+ });
+ }
+
core.transport.dispatch({
action: 'Ctx',
args: {
diff --git a/src/routes/MetaDetails/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js
index 0825f4287..5ce9b199b 100644
--- a/src/routes/MetaDetails/MetaDetails.js
+++ b/src/routes/MetaDetails/MetaDetails.js
@@ -65,18 +65,15 @@ const MetaDetails = ({ urlParams, queryParams }) => {
});
}, [metaDetails]);
const toggleWatched = React.useCallback(() => {
- if (metaDetails.metaItem.content.content === null || metaDetails.metaItem.content.type !== 'Ready') {
+ if (metaDetails.metaItem === null || metaDetails.metaItem.content.type !== 'Ready') {
return;
}
core.transport.dispatch({
- action: 'Ctx',
+ action: 'MetaDetails',
args: {
- action: 'LibraryItemMarkAsWatched',
- args: {
- id: metaDetails.metaItem.content.content.id,
- is_watched: !metaDetails.metaItem.content.content.watched
- }
+ action: 'MarkAsWatched',
+ args: !metaDetails.metaItem.watched
}
});
}, [metaDetails]);
From 5eb55d3aaf8df7180f0f7b1dc9c7c7678588a39a Mon Sep 17 00:00:00 2001
From: Neeraj TK <64883030+GaryGosh@users.noreply.github.com>
Date: Fri, 14 Nov 2025 17:45:45 +0530
Subject: [PATCH 016/154] Added the toggle subtitles shortcut
---
src/common/Shortcuts/shortcuts.json | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/common/Shortcuts/shortcuts.json b/src/common/Shortcuts/shortcuts.json
index 766288fb0..4f2d17db2 100644
--- a/src/common/Shortcuts/shortcuts.json
+++ b/src/common/Shortcuts/shortcuts.json
@@ -69,6 +69,11 @@
"label": "SETTINGS_SHORTCUT_SUBTITLES_DELAY",
"combos": [["G"], ["H"]]
},
+ {
+ "name": "toggleSubtitles",
+ "label": "SETTINGS_SHORTCUT_TOGGLE_SUBTITLES",
+ "combos": [["C"]]
+ },
{
"name": "subtitlesMenu",
"label": "SETTINGS_SHORTCUT_MENU_SUBTITLES",
From c70211153e8c555b1a3a1df36ae836b1a53f84d6 Mon Sep 17 00:00:00 2001
From: higorgoulart
Date: Sat, 15 Nov 2025 14:04:14 -0300
Subject: [PATCH 017/154] feat: review facts
---
src/routes/Discover/Discover.js | 19 +++----------------
src/routes/MetaDetails/MetaDetails.js | 2 +-
2 files changed, 4 insertions(+), 17 deletions(-)
diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js
index be28eeafd..bf0544a5d 100644
--- a/src/routes/Discover/Discover.js
+++ b/src/routes/Discover/Discover.js
@@ -79,24 +79,11 @@ const Discover = ({ urlParams, queryParams }) => {
return;
}
- if (!selectedMetaItem.inLibrary) {
- core.transport.dispatch({
- action: 'Ctx',
- args: {
- action: 'AddToLibrary',
- args: selectedMetaItem
- }
- });
- }
-
core.transport.dispatch({
- action: 'Ctx',
+ action: 'MetaDetails',
args: {
- action: 'LibraryItemMarkAsWatched',
- args: {
- id: selectedMetaItem.id,
- is_watched: !selectedMetaItem.watched
- }
+ action: 'MarkAsWatched',
+ args: !selectedMetaItem.watched
}
});
}, [selectedMetaItem]);
diff --git a/src/routes/MetaDetails/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js
index 5ce9b199b..f5dcc026d 100644
--- a/src/routes/MetaDetails/MetaDetails.js
+++ b/src/routes/MetaDetails/MetaDetails.js
@@ -73,7 +73,7 @@ const MetaDetails = ({ urlParams, queryParams }) => {
action: 'MetaDetails',
args: {
action: 'MarkAsWatched',
- args: !metaDetails.metaItem.watched
+ args: !metaDetails.metaItem.content.content.watched
}
});
}, [metaDetails]);
From a9d9c8d808145a58d444ad88e61ede486c5d22e8 Mon Sep 17 00:00:00 2001
From: higorgoulart
Date: Mon, 17 Nov 2025 19:39:03 -0300
Subject: [PATCH 018/154] feat: load model
---
src/routes/Discover/Discover.js | 21 ++++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js
index bf0544a5d..230d3b7d0 100644
--- a/src/routes/Discover/Discover.js
+++ b/src/routes/Discover/Discover.js
@@ -41,12 +41,31 @@ const Discover = ({ urlParams, queryParams }) => {
}
}, [hasNextPage, loadNextPage]);
const selectedMetaItem = React.useMemo(() => {
- return discover.catalog !== null &&
+ const item = discover.catalog !== null &&
discover.catalog.content.type === 'Ready' &&
discover.catalog.content.content[selectedMetaItemIndex] ?
discover.catalog.content.content[selectedMetaItemIndex]
:
null;
+
+ if (item !== null) {
+ core.transport.dispatch({
+ action: 'Load',
+ args: {
+ model: 'MetaDetails',
+ args: {
+ metaPath: {
+ resource: 'meta',
+ type: item.type,
+ id: item.id,
+ extra: []
+ }
+ }
+ }
+ });
+ }
+
+ return item;
}, [discover.catalog, selectedMetaItemIndex]);
const addToLibrary = React.useCallback(() => {
if (selectedMetaItem === null) {
From f73fa5931e126b409d7cd0a82e726cfac0b96570 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Wed, 19 Nov 2025 14:31:04 +0200
Subject: [PATCH 019/154] refactor(Discover): simplify
---
src/routes/Discover/Discover.js | 40 +++++++++++----------------------
1 file changed, 13 insertions(+), 27 deletions(-)
diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js
index 230d3b7d0..647f55248 100644
--- a/src/routes/Discover/Discover.js
+++ b/src/routes/Discover/Discover.js
@@ -10,6 +10,7 @@ const { CONSTANTS, useBinaryState, useOnScrollToBottom, withCoreSuspender } = re
const { AddonDetailsModal, Button, DelayedRenderer, Image, MainNavBars, MetaItem, MetaPreview, ModalDialog, MultiselectMenu } = require('stremio/components');
const useDiscover = require('./useDiscover');
const useSelectableInputs = require('./useSelectableInputs');
+const useMetaDetails = require('../MetaDetails/useMetaDetails');
const styles = require('./styles');
const SCROLL_TO_BOTTOM_THRESHOLD = 400;
@@ -23,6 +24,18 @@ const Discover = ({ urlParams, queryParams }) => {
const [addonModalOpen, openAddonModal, closeAddonModal] = useBinaryState(false);
const [selectedMetaItemIndex, setSelectedMetaItemIndex] = React.useState(0);
+ const { selectedMetaItem, metaDetailsParams } = React.useMemo(() => {
+ const item = discover.catalog?.content.type === 'Ready' &&
+ discover.catalog.content.content[selectedMetaItemIndex] || null;
+
+ return {
+ selectedMetaItem: item,
+ metaDetailsParams: item ? { type: item.type, id: item.id } : {}
+ };
+ }, [discover.catalog, selectedMetaItemIndex]);
+
+ useMetaDetails(metaDetailsParams);
+
const metasContainerRef = React.useRef();
const metaPreviewRef = React.useRef();
@@ -40,33 +53,6 @@ const Discover = ({ urlParams, queryParams }) => {
}
}
}, [hasNextPage, loadNextPage]);
- const selectedMetaItem = React.useMemo(() => {
- const item = discover.catalog !== null &&
- discover.catalog.content.type === 'Ready' &&
- discover.catalog.content.content[selectedMetaItemIndex] ?
- discover.catalog.content.content[selectedMetaItemIndex]
- :
- null;
-
- if (item !== null) {
- core.transport.dispatch({
- action: 'Load',
- args: {
- model: 'MetaDetails',
- args: {
- metaPath: {
- resource: 'meta',
- type: item.type,
- id: item.id,
- extra: []
- }
- }
- }
- });
- }
-
- return item;
- }, [discover.catalog, selectedMetaItemIndex]);
const addToLibrary = React.useCallback(() => {
if (selectedMetaItem === null) {
return;
From 6bf3b8147d5d2fdc1d6ccf32b9e7c06dfde52ba7 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Wed, 19 Nov 2025 14:47:17 +0200
Subject: [PATCH 020/154] refactor(ActionsGroup): simplify
---
src/components/ActionsGroup/ActionsGroup.tsx | 26 ++++++++++++-------
src/components/ActionsGroup/index.ts | 4 +--
src/components/MetaPreview/MetaPreview.js | 2 +-
.../MetaPreview/Ratings/Ratings.tsx | 9 ++++---
src/components/index.ts | 2 ++
5 files changed, 27 insertions(+), 16 deletions(-)
diff --git a/src/components/ActionsGroup/ActionsGroup.tsx b/src/components/ActionsGroup/ActionsGroup.tsx
index 244786c2f..8c7393fcc 100644
--- a/src/components/ActionsGroup/ActionsGroup.tsx
+++ b/src/components/ActionsGroup/ActionsGroup.tsx
@@ -3,8 +3,8 @@
import classNames from 'classnames';
import React from 'react';
import Icon from '@stremio/stremio-icons/react';
-import styles from './ActionsGroup.less';
import { Tooltip } from 'stremio/common/Tooltips';
+import styles from './ActionsGroup.less';
type Item = {
icon: string;
@@ -23,15 +23,21 @@ type Props = {
const ActionsGroup = ({ items, className }: Props) => {
return (
- {items.map((item, index) => (
-
- {item.label && }
-
-
- ))}
+ {
+ items.map((item, index) => (
+
+ {
+ item.label &&
+
+ }
+
+
+ ))
+ }
);
};
diff --git a/src/components/ActionsGroup/index.ts b/src/components/ActionsGroup/index.ts
index 2e83d4f64..4dea1b83a 100644
--- a/src/components/ActionsGroup/index.ts
+++ b/src/components/ActionsGroup/index.ts
@@ -1,6 +1,6 @@
-// Copyright (C) 2017-2023 Smart code 203358507
+// Copyright (C) 2017-2025 Smart code 203358507
import ActionsGroup from './ActionsGroup';
-export { ActionsGroup };
+export default ActionsGroup;
diff --git a/src/components/MetaPreview/MetaPreview.js b/src/components/MetaPreview/MetaPreview.js
index 60f4e0681..5fa7d8ff0 100644
--- a/src/components/MetaPreview/MetaPreview.js
+++ b/src/components/MetaPreview/MetaPreview.js
@@ -8,7 +8,7 @@ const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { default: Button } = require('stremio/components/Button');
const { default: Image } = require('stremio/components/Image');
-const { ActionsGroup } = require('stremio/components/ActionsGroup');
+const { default: ActionsGroup } = require('stremio/components/ActionsGroup');
const ModalDialog = require('stremio/components/ModalDialog');
const SharePrompt = require('stremio/components/SharePrompt');
const CONSTANTS = require('stremio/common/CONSTANTS');
diff --git a/src/components/MetaPreview/Ratings/Ratings.tsx b/src/components/MetaPreview/Ratings/Ratings.tsx
index 28de12b26..329ee4945 100644
--- a/src/components/MetaPreview/Ratings/Ratings.tsx
+++ b/src/components/MetaPreview/Ratings/Ratings.tsx
@@ -2,7 +2,7 @@
import React, { useMemo } from 'react';
import useRating from './useRating';
-import { ActionsGroup } from 'stremio/components/ActionsGroup';
+import { ActionsGroup } from 'stremio/components';
type Props = {
metaId?: string;
@@ -13,6 +13,7 @@ type Props = {
const Ratings = ({ ratingInfo, className }: Props) => {
const { onLiked, onLoved, liked, loved } = useRating(ratingInfo);
const disabled = useMemo(() => ratingInfo?.type !== 'Ready', [ratingInfo]);
+
const items = useMemo(() => [
{
icon: liked ? 'thumbs-up' : 'thumbs-up-outline',
@@ -24,9 +25,11 @@ const Ratings = ({ ratingInfo, className }: Props) => {
disabled,
onClick: onLoved,
},
- ], [liked, loved, disabled, onLiked, onLoved]);
+ ], [liked, loved, disabled]);
- return ;
+ return (
+
+ );
};
export default Ratings;
diff --git a/src/components/index.ts b/src/components/index.ts
index a47c2c709..75400b0dd 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -30,6 +30,7 @@ import TextInput from './TextInput';
import Toggle from './Toggle';
import Transition from './Transition';
import Video from './Video';
+import ActionsGroup from './ActionsGroup';
export {
AddonDetailsModal,
@@ -65,4 +66,5 @@ export {
Toggle,
Transition,
Video,
+ ActionsGroup
};
From 71e0bb44815c9f63e6927ebd33b89e23edf60c6e Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Wed, 19 Nov 2025 14:48:11 +0200
Subject: [PATCH 021/154] chore: update copyright
---
src/components/ActionsGroup/ActionsGroup.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/ActionsGroup/ActionsGroup.tsx b/src/components/ActionsGroup/ActionsGroup.tsx
index 8c7393fcc..052f25016 100644
--- a/src/components/ActionsGroup/ActionsGroup.tsx
+++ b/src/components/ActionsGroup/ActionsGroup.tsx
@@ -1,4 +1,4 @@
-// Copyright (C) 2017-2023 Smart code 203358507
+// Copyright (C) 2017-2025 Smart code 203358507
import classNames from 'classnames';
import React from 'react';
From 17ee0e95e487ef730625760b1d7f6cc4fdace1c9 Mon Sep 17 00:00:00 2001
From: Tim
Date: Thu, 22 Jan 2026 11:21:09 +0100
Subject: [PATCH 022/154] fix(Player): side drawer layout on large screens
---
src/routes/Player/SideDrawer/SideDrawer.less | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/src/routes/Player/SideDrawer/SideDrawer.less b/src/routes/Player/SideDrawer/SideDrawer.less
index 0fe22f58a..6623bc283 100644
--- a/src/routes/Player/SideDrawer/SideDrawer.less
+++ b/src/routes/Player/SideDrawer/SideDrawer.less
@@ -3,6 +3,7 @@
@import (reference) '~stremio/common/screen-sizes.less';
:import('~stremio/components/MetaPreview/styles.less') {
+ description-container: description-container;
action-buttons-container: action-buttons-container;
}
@@ -57,9 +58,14 @@
.info {
padding: @padding;
overflow-y: auto;
- flex: 1;
.side-drawer-meta-preview {
+ .description-container {
+ display: -webkit-box;
+ -webkit-line-clamp: 4;
+ -webkit-box-orient: vertical;
+ }
+
.action-buttons-container {
padding-top: 0;
margin-top: 0;
@@ -91,10 +97,6 @@
@media @phone-landscape {
.side-drawer {
max-width: 50dvw;
-
- .info {
- flex: 1;
- }
}
}
From f2c9bb6d889e70d8d745cd4ed2b2693fa95cfc77 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 12:37:53 +0200
Subject: [PATCH 023/154] chore: remove comments
---
src/routes/MetaDetails/EpisodePicker/EpisodePicker.tsx | 1 -
src/routes/MetaDetails/useSeason.js | 1 -
2 files changed, 2 deletions(-)
diff --git a/src/routes/MetaDetails/EpisodePicker/EpisodePicker.tsx b/src/routes/MetaDetails/EpisodePicker/EpisodePicker.tsx
index d00429638..b9a33e6f1 100644
--- a/src/routes/MetaDetails/EpisodePicker/EpisodePicker.tsx
+++ b/src/routes/MetaDetails/EpisodePicker/EpisodePicker.tsx
@@ -17,7 +17,6 @@ const EpisodePicker = ({ className, onSubmit }: Props) => {
const { initialSeason, initialEpisode } = useMemo(() => {
const splitPath = window.location.hash.split('/');
if (splitPath[splitPath.length - 1] === '') {
- // remove the empty element
splitPath.pop();
}
const videoId = decodeURIComponent(splitPath[splitPath.length - 1]);
diff --git a/src/routes/MetaDetails/useSeason.js b/src/routes/MetaDetails/useSeason.js
index ec310c656..192830a42 100644
--- a/src/routes/MetaDetails/useSeason.js
+++ b/src/routes/MetaDetails/useSeason.js
@@ -13,7 +13,6 @@ const useSeason = (urlParams, queryParams) => {
const nextQueryParams = new URLSearchParams(queryParams);
nextQueryParams.set('season', season);
const path = urlParams.path.endsWith('/') ?
- // remove the trailing /
urlParams.path.slice(0, -1):
urlParams.path;
From 67358359bf20c25beac6d9edea6a7891193185ae Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 14:38:59 +0200
Subject: [PATCH 024/154] refactor: remember the sub track
handle both external and embedded
---
src/routes/Player/Player.js | 47 +++++++++++++++++++++++++++++++------
1 file changed, 40 insertions(+), 7 deletions(-)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index 11a71aecf..fe6f9abde 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -90,10 +90,9 @@ const Player = ({ urlParams, queryParams }) => {
const nextVideoPopupDismissed = React.useRef(false);
const defaultSubtitlesSelected = React.useRef(false);
+ const lastSelectedSubtitleTrack = React.useRef(null);
const defaultAudioTrackSelected = React.useRef(false);
const [error, setError] = React.useState(null);
- const [previousSubtitlesTrackId, setPreviousSubtitlesTrackId] = React.useState(null);
-
const isNavigating = React.useRef(false);
@@ -256,11 +255,6 @@ const Player = ({ urlParams, queryParams }) => {
streamStateChanged({ subtitleDelay: delay });
}, [streamStateChanged]);
- const onSubtitlesTrackSelected = React.useCallback((id) => {
- video.setProp('selectedSubtitlesTrackId', id);
- video.setProp('selectedExtraSubtitlesTrackId', null);
- setPreviousSubtitlesTrackId(id);
- }, []);
const onIncreaseSubtitlesDelay = React.useCallback(() => {
const delay = video.state.extraSubtitlesDelay + 250;
onExtraSubtitlesDelayChanged(delay);
@@ -509,6 +503,14 @@ const Player = ({ urlParams, queryParams }) => {
}
}, [video.state.stream, player.streamState]);
+ React.useEffect(() => {
+ if (video.state.selectedSubtitlesTrackId !== null) {
+ lastSelectedSubtitleTrack.current = { id: video.state.selectedSubtitlesTrackId, embedded: true };
+ } else if (video.state.selectedExtraSubtitlesTrackId !== null) {
+ lastSelectedSubtitleTrack.current = { id: video.state.selectedExtraSubtitlesTrackId, embedded: false };
+ }
+ }, [video.state.selectedSubtitlesTrackId, video.state.selectedExtraSubtitlesTrackId]);
+
React.useEffect(() => {
defaultSubtitlesSelected.current = false;
defaultAudioTrackSelected.current = false;
@@ -677,6 +679,37 @@ const Player = ({ urlParams, queryParams }) => {
combo === 1 ? onUpdateSubtitlesSize(-1) : onUpdateSubtitlesSize(1);
}, [onUpdateSubtitlesSize, onUpdateSubtitlesSize]);
+ onShortcut('toggleSubtitles', () => {
+ const hasEmbedded = video.state.selectedSubtitlesTrackId !== null;
+ const hasExtra = video.state.selectedExtraSubtitlesTrackId !== null;
+ const last = lastSelectedSubtitleTrack.current;
+
+ if (hasEmbedded || hasExtra) {
+ video.setSubtitlesTrack(null);
+ video.setExtraSubtitlesTrack(null);
+ return;
+ }
+
+ if (last) {
+ const tracks = last.embedded ? video.state.subtitlesTracks : video.state.extraSubtitlesTracks;
+ if (tracks?.some((t) => t.id === last.id)) {
+ last.embedded ? onSubtitlesTrackSelected(last.id) : onExtraSubtitlesTrackSelected(last.id);
+ return;
+ }
+ }
+
+ const embeddedMatch = findTrackByLang(video.state.subtitlesTracks || [], settings.subtitlesLanguage);
+ if (embeddedMatch) {
+ onSubtitlesTrackSelected(embeddedMatch.id);
+ return;
+ }
+
+ const extraMatch = findTrackByLang(video.state.extraSubtitlesTracks || [], settings.subtitlesLanguage);
+ if (extraMatch) {
+ onExtraSubtitlesTrackSelected(extraMatch.id);
+ }
+ }, [video.state.selectedSubtitlesTrackId, video.state.selectedExtraSubtitlesTrackId, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, settings.subtitlesLanguage, onSubtitlesTrackSelected, onExtraSubtitlesTrackSelected]);
+
onShortcut('subtitlesMenu', () => {
closeMenus();
if (video.state?.subtitlesTracks?.length > 0 || video.state?.extraSubtitlesTracks?.length > 0) {
From ea5e302af72be2167ba735af5976b862f20188f8 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 14:48:52 +0200
Subject: [PATCH 025/154] refactor: simplfy subs handling
---
src/routes/Player/Player.js | 46 +++++++++++--------------------------
1 file changed, 14 insertions(+), 32 deletions(-)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index fe6f9abde..db34cbe3a 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -90,7 +90,7 @@ const Player = ({ urlParams, queryParams }) => {
const nextVideoPopupDismissed = React.useRef(false);
const defaultSubtitlesSelected = React.useRef(false);
- const lastSelectedSubtitleTrack = React.useRef(null);
+ const subtitlesEnabled = React.useRef(true);
const defaultAudioTrackSelected = React.useRef(false);
const [error, setError] = React.useState(null);
@@ -503,14 +503,6 @@ const Player = ({ urlParams, queryParams }) => {
}
}, [video.state.stream, player.streamState]);
- React.useEffect(() => {
- if (video.state.selectedSubtitlesTrackId !== null) {
- lastSelectedSubtitleTrack.current = { id: video.state.selectedSubtitlesTrackId, embedded: true };
- } else if (video.state.selectedExtraSubtitlesTrackId !== null) {
- lastSelectedSubtitleTrack.current = { id: video.state.selectedExtraSubtitlesTrackId, embedded: false };
- }
- }, [video.state.selectedSubtitlesTrackId, video.state.selectedExtraSubtitlesTrackId]);
-
React.useEffect(() => {
defaultSubtitlesSelected.current = false;
defaultAudioTrackSelected.current = false;
@@ -680,35 +672,25 @@ const Player = ({ urlParams, queryParams }) => {
}, [onUpdateSubtitlesSize, onUpdateSubtitlesSize]);
onShortcut('toggleSubtitles', () => {
- const hasEmbedded = video.state.selectedSubtitlesTrackId !== null;
- const hasExtra = video.state.selectedExtraSubtitlesTrackId !== null;
- const last = lastSelectedSubtitleTrack.current;
+ const savedTrack = player.streamState?.subtitleTrack;
- if (hasEmbedded || hasExtra) {
+ if (subtitlesEnabled.current) {
video.setSubtitlesTrack(null);
video.setExtraSubtitlesTrack(null);
- return;
- }
-
- if (last) {
- const tracks = last.embedded ? video.state.subtitlesTracks : video.state.extraSubtitlesTracks;
- if (tracks?.some((t) => t.id === last.id)) {
- last.embedded ? onSubtitlesTrackSelected(last.id) : onExtraSubtitlesTrackSelected(last.id);
- return;
+ } else if (savedTrack?.id) {
+ savedTrack.embedded ? onSubtitlesTrackSelected(savedTrack.id) : onExtraSubtitlesTrackSelected(savedTrack.id);
+ } else {
+ const embeddedMatch = findTrackByLang(video.state.subtitlesTracks || [], settings.subtitlesLanguage);
+ const extraMatch = findTrackByLang(video.state.extraSubtitlesTracks || [], settings.subtitlesLanguage);
+ if (embeddedMatch) {
+ onSubtitlesTrackSelected(embeddedMatch.id);
+ } else if (extraMatch) {
+ onExtraSubtitlesTrackSelected(extraMatch.id);
}
}
- const embeddedMatch = findTrackByLang(video.state.subtitlesTracks || [], settings.subtitlesLanguage);
- if (embeddedMatch) {
- onSubtitlesTrackSelected(embeddedMatch.id);
- return;
- }
-
- const extraMatch = findTrackByLang(video.state.extraSubtitlesTracks || [], settings.subtitlesLanguage);
- if (extraMatch) {
- onExtraSubtitlesTrackSelected(extraMatch.id);
- }
- }, [video.state.selectedSubtitlesTrackId, video.state.selectedExtraSubtitlesTrackId, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, settings.subtitlesLanguage, onSubtitlesTrackSelected, onExtraSubtitlesTrackSelected]);
+ subtitlesEnabled.current = !subtitlesEnabled.current;
+ }, [player.streamState, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, settings.subtitlesLanguage, onSubtitlesTrackSelected, onExtraSubtitlesTrackSelected]);
onShortcut('subtitlesMenu', () => {
closeMenus();
From 891147321039b52c6398e68d64a550380eedb22f Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 14:54:33 +0200
Subject: [PATCH 026/154] refactor: remove the block which is handled by useff
---
src/routes/Player/Player.js | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index db34cbe3a..adeebd206 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -679,18 +679,10 @@ const Player = ({ urlParams, queryParams }) => {
video.setExtraSubtitlesTrack(null);
} else if (savedTrack?.id) {
savedTrack.embedded ? onSubtitlesTrackSelected(savedTrack.id) : onExtraSubtitlesTrackSelected(savedTrack.id);
- } else {
- const embeddedMatch = findTrackByLang(video.state.subtitlesTracks || [], settings.subtitlesLanguage);
- const extraMatch = findTrackByLang(video.state.extraSubtitlesTracks || [], settings.subtitlesLanguage);
- if (embeddedMatch) {
- onSubtitlesTrackSelected(embeddedMatch.id);
- } else if (extraMatch) {
- onExtraSubtitlesTrackSelected(extraMatch.id);
- }
}
subtitlesEnabled.current = !subtitlesEnabled.current;
- }, [player.streamState, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, settings.subtitlesLanguage, onSubtitlesTrackSelected, onExtraSubtitlesTrackSelected]);
+ }, [player.streamState]);
onShortcut('subtitlesMenu', () => {
closeMenus();
From 1c441b9bc0213ea2eb1cc328afa96b2f0452b0b2 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 15:13:36 +0200
Subject: [PATCH 027/154] Update Player.js
---
src/routes/Player/Player.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index adeebd206..cc9985ac4 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -678,7 +678,8 @@ const Player = ({ urlParams, queryParams }) => {
video.setSubtitlesTrack(null);
video.setExtraSubtitlesTrack(null);
} else if (savedTrack?.id) {
- savedTrack.embedded ? onSubtitlesTrackSelected(savedTrack.id) : onExtraSubtitlesTrackSelected(savedTrack.id);
+ video.setSubtitlesTrack(savedTrack?.id);
+ video.setExtraSubtitlesTrack(savedTrack?.id);
}
subtitlesEnabled.current = !subtitlesEnabled.current;
From d456adff0ea5d676b475a0f242fa72ba14d35e47 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 15:20:09 +0200
Subject: [PATCH 028/154] refactor: correctly set tracks
---
src/routes/Player/Player.js | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index cc9985ac4..54ad14bc8 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -678,8 +678,7 @@ const Player = ({ urlParams, queryParams }) => {
video.setSubtitlesTrack(null);
video.setExtraSubtitlesTrack(null);
} else if (savedTrack?.id) {
- video.setSubtitlesTrack(savedTrack?.id);
- video.setExtraSubtitlesTrack(savedTrack?.id);
+ savedTrack.embedded ? video.setSubtitlesTrack(savedTrack.id) : video.setExtraSubtitlesTrack(savedTrack.id);
}
subtitlesEnabled.current = !subtitlesEnabled.current;
From 3cad4910401ba5d4d8e17901f37f356dbc8fe551 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 15:20:21 +0200
Subject: [PATCH 029/154] chore: update translations
---
package.json | 2 +-
pnpm-lock.yaml | 10 +++++-----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/package.json b/package.json
index 0c9ca54ec..4f283d0ac 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,7 @@
"react-i18next": "^15.1.3",
"react-is": "18.3.1",
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
- "stremio-translations": "github:Stremio/stremio-translations#7c0c337f32163aa13158bb90cd6133da43feafef",
+ "stremio-translations": "github:Stremio/stremio-translations#1bfcb6d2a4f37bb647959ba0bbbd1ade1415c2fe",
"url": "0.11.4",
"use-long-press": "^3.2.0"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 435a809e8..a55a4f3bd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -90,8 +90,8 @@ importers:
specifier: github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6
version: https://codeload.github.com/Stremio/spatial-navigation/tar.gz/64871b1422466f5f45d24ebc8bbd315b2ebab6a6
stremio-translations:
- specifier: github:Stremio/stremio-translations#7c0c337f32163aa13158bb90cd6133da43feafef
- version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/7c0c337f32163aa13158bb90cd6133da43feafef
+ specifier: github:Stremio/stremio-translations#1bfcb6d2a4f37bb647959ba0bbbd1ade1415c2fe
+ version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/1bfcb6d2a4f37bb647959ba0bbbd1ade1415c2fe
url:
specifier: 0.11.4
version: 0.11.4
@@ -4133,8 +4133,8 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
- stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/7c0c337f32163aa13158bb90cd6133da43feafef:
- resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/7c0c337f32163aa13158bb90cd6133da43feafef}
+ stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/1bfcb6d2a4f37bb647959ba0bbbd1ade1415c2fe:
+ resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/1bfcb6d2a4f37bb647959ba0bbbd1ade1415c2fe}
version: 1.45.0
string-length@4.0.2:
@@ -9378,7 +9378,7 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
- stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/7c0c337f32163aa13158bb90cd6133da43feafef: {}
+ stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/1bfcb6d2a4f37bb647959ba0bbbd1ade1415c2fe: {}
string-length@4.0.2:
dependencies:
From dbed391a86f1eb60c8450a678219311f2cb93e2d Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 16:44:08 +0200
Subject: [PATCH 030/154] chore: bump v5.0.0-beta.30
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 4f283d0ac..a9a78e7bc 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "stremio",
"displayName": "Stremio",
- "version": "5.0.0-beta.29",
+ "version": "5.0.0-beta.30",
"author": "Smart Code OOD",
"private": true,
"license": "gpl-2.0",
From 1c9813ebc96326d19e7dceafecc74cd43521add8 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 21:04:57 +0200
Subject: [PATCH 031/154] fix(Image): update return renderFallback type
---
src/components/Image/Image.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/Image/Image.tsx b/src/components/Image/Image.tsx
index e64078fbc..5c7a93c00 100644
--- a/src/components/Image/Image.tsx
+++ b/src/components/Image/Image.tsx
@@ -7,7 +7,7 @@ type Props = {
src: string,
alt: string,
fallbackSrc: string,
- renderFallback: () => void,
+ renderFallback: () => React.ReactNode,
onError: (event: React.SyntheticEvent) => void,
};
From c28a52a73c741a073008ad6c532e754508c9fb93 Mon Sep 17 00:00:00 2001
From: ArtificialSloth
Date: Tue, 27 Jan 2026 20:00:58 -0500
Subject: [PATCH 032/154] fix: add playingOnExternalDevice flag used to pause
the video if it starts playing while using an external player.
---
src/routes/Player/Player.js | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index 54ad14bc8..e0043221f 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -92,6 +92,7 @@ const Player = ({ urlParams, queryParams }) => {
const defaultSubtitlesSelected = React.useRef(false);
const subtitlesEnabled = React.useRef(true);
const defaultAudioTrackSelected = React.useRef(false);
+ const playingOnExternalDevice = React.useRef(false);
const [error, setError] = React.useState(null);
const isNavigating = React.useRef(false);
@@ -189,6 +190,7 @@ const Player = ({ urlParams, queryParams }) => {
}, []);
const onPlayRequested = React.useCallback(() => {
+ playingOnExternalDevice.current = false;
video.setPaused(false);
setSeeking(false);
}, []);
@@ -412,7 +414,9 @@ const Player = ({ urlParams, queryParams }) => {
}, [video.state.time, video.state.duration, video.state.manifest, seeking]);
React.useEffect(() => {
- if (video.state.paused !== null) {
+ if (playingOnExternalDevice.current && video.state.paused === false) {
+ onPauseRequested();
+ } else if (video.state.paused !== null) {
pausedChanged(video.state.paused);
}
}, [video.state.paused]);
@@ -507,6 +511,7 @@ const Player = ({ urlParams, queryParams }) => {
defaultSubtitlesSelected.current = false;
defaultAudioTrackSelected.current = false;
nextVideoPopupDismissed.current = false;
+ playingOnExternalDevice.current = false;
// we need a timeout here to make sure that previous page unloads and the new one loads
// avoiding race conditions and flickering
setTimeout(() => isNavigating.current = false, 1000);
@@ -548,6 +553,7 @@ const Player = ({ urlParams, queryParams }) => {
};
const onCoreEvent = ({ event }) => {
if (event === 'PlayingOnDevice') {
+ playingOnExternalDevice.current = true;
onPauseRequested();
}
};
From e29adde4bd80f365f2baef6ac1a807ff176f4f62 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Tue, 3 Feb 2026 16:44:25 +0800
Subject: [PATCH 033/154] fix: use translate prefix correction
---
src/common/useTranslate.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/common/useTranslate.js b/src/common/useTranslate.js
index 7214a4a1e..cf249b572 100644
--- a/src/common/useTranslate.js
+++ b/src/common/useTranslate.js
@@ -9,7 +9,7 @@ const useTranslate = () => {
const string = useCallback((key) => t(key), [t]);
const stringWithPrefix = useCallback((value, prefix, fallback = null) => {
- const key = `${prefix}${value}`;
+ const key = `${prefix}_${value}`;
const defaultValue = fallback ?? value.charAt(0).toUpperCase() + value.slice(1);
return t(key, {
From 0a1746dfe27fdb7d86d8dff55a606e12ffe0229f Mon Sep 17 00:00:00 2001
From: Botzy
Date: Tue, 3 Feb 2026 11:13:31 +0200
Subject: [PATCH 034/154] fix: links prefix and revert change to useTranslate
---
src/common/useTranslate.js | 2 +-
src/components/MetaPreview/MetaLinks/MetaLinks.js | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/common/useTranslate.js b/src/common/useTranslate.js
index cf249b572..7214a4a1e 100644
--- a/src/common/useTranslate.js
+++ b/src/common/useTranslate.js
@@ -9,7 +9,7 @@ const useTranslate = () => {
const string = useCallback((key) => t(key), [t]);
const stringWithPrefix = useCallback((value, prefix, fallback = null) => {
- const key = `${prefix}_${value}`;
+ const key = `${prefix}${value}`;
const defaultValue = fallback ?? value.charAt(0).toUpperCase() + value.slice(1);
return t(key, {
diff --git a/src/components/MetaPreview/MetaLinks/MetaLinks.js b/src/components/MetaPreview/MetaLinks/MetaLinks.js
index 3be090911..6e58a5d1e 100644
--- a/src/components/MetaPreview/MetaLinks/MetaLinks.js
+++ b/src/components/MetaPreview/MetaLinks/MetaLinks.js
@@ -14,7 +14,7 @@ const MetaLinks = ({ className, label, links }) => {
{
typeof label === 'string' && label.length > 0 ?
- { stringWithPrefix(label.toUpperCase(), 'LINKS') }
+ { stringWithPrefix(label.toUpperCase(), 'LINKS_') }
:
null
From fa135977487841c3f6085513b3b7533963227c97 Mon Sep 17 00:00:00 2001
From: Fawazorg
Date: Sat, 7 Feb 2026 20:29:13 +0300
Subject: [PATCH 035/154] fix: replace hardcoded strings with translation keys
---
src/routes/Intro/Intro.js | 2 +-
src/routes/Intro/PasswordResetModal/PasswordResetModal.js | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js
index 5a2f80aaa..1f041c004 100644
--- a/src/routes/Intro/Intro.js
+++ b/src/routes/Intro/Intro.js
@@ -183,7 +183,7 @@ const Intro = ({ queryParams }) => {
return;
}
if (!state.privacyPolicyAccepted) {
- dispatch({ type: 'error', error: 'You must accept the Privacy Policy' });
+ dispatch({ type: 'error', error: t('MUST_ACCEPT_PRIVACY_POLICY') });
return;
}
openLoaderModal();
diff --git a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js b/src/routes/Intro/PasswordResetModal/PasswordResetModal.js
index 8c69c1489..e5c27a6b5 100644
--- a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js
+++ b/src/routes/Intro/PasswordResetModal/PasswordResetModal.js
@@ -19,7 +19,7 @@ const PasswordResetModal = ({ email, onCloseRequest }) => {
emailRef.current.value.length > 0 && emailRef.current.validity.valid ?
platform.openExternal('https://www.strem.io/reset-password/' + emailRef.current.value, '_blank')
:
- setError('Invalid email');
+ setError(t('INVALID_EMAIL'));
}, []);
const passwordResetModalButtons = React.useMemo(() => {
return [
@@ -31,7 +31,7 @@ const PasswordResetModal = ({ email, onCloseRequest }) => {
}
},
{
- label: t('SEND'),
+ label: t('BUTTON_SEND'),
props: {
onClick: goToPasswordReset
}
@@ -52,7 +52,7 @@ const PasswordResetModal = ({ email, onCloseRequest }) => {
ref={emailRef}
className={styles['credentials-text-input']}
type={'email'}
- placeholder={'Email'}
+ placeholder={t('WEBSITE_PLACEHOLDER_EMAIL')}
defaultValue={typeof email === 'string' ? email : ''}
onChange={emailOnChange}
onSubmit={goToPasswordReset}
From 93ed428e8b1eb9bc26703fc1b23f851383134739 Mon Sep 17 00:00:00 2001
From: Fawazorg
Date: Sat, 7 Feb 2026 20:54:57 +0300
Subject: [PATCH 036/154] Fix Email key
---
src/routes/Intro/PasswordResetModal/PasswordResetModal.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js b/src/routes/Intro/PasswordResetModal/PasswordResetModal.js
index e5c27a6b5..497568cc9 100644
--- a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js
+++ b/src/routes/Intro/PasswordResetModal/PasswordResetModal.js
@@ -52,7 +52,7 @@ const PasswordResetModal = ({ email, onCloseRequest }) => {
ref={emailRef}
className={styles['credentials-text-input']}
type={'email'}
- placeholder={t('WEBSITE_PLACEHOLDER_EMAIL')}
+ placeholder={t('EMAIL')}
defaultValue={typeof email === 'string' ? email : ''}
onChange={emailOnChange}
onSubmit={goToPasswordReset}
From 6bd28847f2ac4eabdda4b0bc0712d6012c6d3acf Mon Sep 17 00:00:00 2001
From: Fawazorg
Date: Sat, 7 Feb 2026 23:18:27 +0300
Subject: [PATCH 037/154] fix: disabled translation key for next video popup
---
src/routes/Settings/Player/usePlayerOptions.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/routes/Settings/Player/usePlayerOptions.ts b/src/routes/Settings/Player/usePlayerOptions.ts
index 27081817b..69d651e64 100644
--- a/src/routes/Settings/Player/usePlayerOptions.ts
+++ b/src/routes/Settings/Player/usePlayerOptions.ts
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
+import { CONSTANTS, languageNames, useLanguageSorting, usePlatform } from 'stremio/common';
import { useServices } from 'stremio/services';
-import { CONSTANTS, languageNames, usePlatform, useLanguageSorting } from 'stremio/common';
const LANGUAGES_NAMES: Record = languageNames;
@@ -232,12 +232,12 @@ const usePlayerOptions = (profile: Profile) => {
const nextVideoPopupDurationSelect = useMemo(() => ({
options: CONSTANTS.NEXT_VIDEO_POPUP_DURATIONS.map((duration) => ({
value: `${duration}`,
- label: duration === 0 ? 'Disabled' : `${duration / 1000} ${t('SECONDS')}`
+ label: duration === 0 ? t('SETTINGS_DISABLED') : `${duration / 1000} ${t('SECONDS')}`
})),
value: `${profile.settings.nextVideoNotificationDuration}`,
title: () => {
return profile.settings.nextVideoNotificationDuration === 0 ?
- 'Disabled'
+ t('SETTINGS_DISABLED')
:
`${profile.settings.nextVideoNotificationDuration / 1000} ${t('SECONDS')}`;
},
From ce2c021e5f927000fcf6ced282fc58a59f51a722 Mon Sep 17 00:00:00 2001
From: Fawazorg
Date: Sun, 8 Feb 2026 20:54:35 +0300
Subject: [PATCH 038/154] fix: normalize catalog name casing for add-on filter
translation key
---
src/routes/Addons/useSelectableInputs.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/routes/Addons/useSelectableInputs.js b/src/routes/Addons/useSelectableInputs.js
index 8659b99e6..5cd817734 100644
--- a/src/routes/Addons/useSelectableInputs.js
+++ b/src/routes/Addons/useSelectableInputs.js
@@ -18,7 +18,7 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t) => {
() => {
const selectableCatalog = remoteAddons.selectable.catalogs
.find(({ id }) => id === remoteAddons.selected.request.path.id);
- return selectableCatalog ? t.stringWithPrefix(selectableCatalog.name, 'ADDON_') : remoteAddons.selected.request.path.id;
+ return selectableCatalog ? t.stringWithPrefix(selectableCatalog.name.toUpperCase(), 'ADDON_') : remoteAddons.selected.request.path.id;
}
: null,
onSelect: (value) => {
From 1e0963e8af1efb5ab623160399c5f758ba5a0adb Mon Sep 17 00:00:00 2001
From: Fawazorg
Date: Mon, 9 Feb 2026 00:17:46 +0300
Subject: [PATCH 039/154] fix: missing key translate for server add url
---
src/routes/Settings/Streaming/URLsManager/AddItem/AddItem.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/routes/Settings/Streaming/URLsManager/AddItem/AddItem.tsx b/src/routes/Settings/Streaming/URLsManager/AddItem/AddItem.tsx
index d0a2e0c41..07e829696 100644
--- a/src/routes/Settings/Streaming/URLsManager/AddItem/AddItem.tsx
+++ b/src/routes/Settings/Streaming/URLsManager/AddItem/AddItem.tsx
@@ -1,6 +1,7 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { ChangeEvent, useCallback, useState } from 'react';
+import { useTranslation } from 'react-i18next';
import Icon from '@stremio/stremio-icons/react';
import { Button, TextInput } from 'stremio/components';
import styles from './AddItem.less';
@@ -11,6 +12,7 @@ type Props = {
};
const AddItem = ({ onCancel, handleAddUrl }: Props) => {
+ const { t } = useTranslation();
const [inputValue, setInputValue] = useState('');
const handleValueChange = useCallback(({ target }: ChangeEvent) => {
@@ -28,7 +30,7 @@ const AddItem = ({ onCancel, handleAddUrl }: Props) => {
value={inputValue}
onChange={handleValueChange}
onSubmit={onSubmit}
- placeholder={'Enter URL'}
+ placeholder={t('SETTINGS_SERVER_ADD_URL_PLACEHOLDER')}
/>