From 6833bb719d3e894e6c511bec96a309cfd2f364c9 Mon Sep 17 00:00:00 2001 From: higorgoulart Date: Fri, 31 Oct 2025 19:52:30 -0300 Subject: [PATCH 01/46] 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 02/46] 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 03/46] 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 04/46] 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 05/46] 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 06/46] 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 07/46] 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 08/46] 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 09/46] 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 c70211153e8c555b1a3a1df36ae836b1a53f84d6 Mon Sep 17 00:00:00 2001 From: higorgoulart Date: Sat, 15 Nov 2025 14:04:14 -0300 Subject: [PATCH 10/46] 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 11/46] 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 12/46] 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 13/46] 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 14/46] 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 9fe7430bc71d1760a387b6be5e48682b2c8211fe Mon Sep 17 00:00:00 2001 From: Ignacio Date: Sat, 28 Mar 2026 23:59:06 -0300 Subject: [PATCH 15/46] fix: persist subtitle language preference across streams --- src/routes/Player/Player.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 98402ed1f..22af20a32 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -31,6 +31,7 @@ const { default: Indicator } = require('./Indicator/Indicator'); const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang); const findTrackById = (tracks, id) => tracks.find((track) => track.id === id); +const getTrackLang = (tracks, id) => id !== null ? findTrackById(tracks, id)?.lang : null; const Player = ({ urlParams, queryParams }) => { const { t } = useTranslation(); @@ -241,9 +242,10 @@ const Player = ({ urlParams, queryParams }) => { subtitleTrack: { id, embedded: true, + lang: getTrackLang(video.state.subtitlesTracks, id), }, }); - }, [streamStateChanged]); + }, [streamStateChanged, video.state.subtitlesTracks]); const onExtraSubtitlesTrackSelected = React.useCallback((id) => { video.setExtraSubtitlesTrack(id); @@ -251,9 +253,10 @@ const Player = ({ urlParams, queryParams }) => { subtitleTrack: { id, embedded: false, + lang: getTrackLang(video.state.extraSubtitlesTracks, id), }, }); - }, [streamStateChanged]); + }, [streamStateChanged, video.state.extraSubtitlesTracks]); const onAudioTrackSelected = React.useCallback((id) => { video.setAudioTrack(id); @@ -466,13 +469,16 @@ const Player = ({ urlParams, queryParams }) => { } const savedTrackId = player.streamState?.subtitleTrack?.id; + const savedLang = player.streamState?.subtitleTrack?.lang; + const fallbackLang = savedLang ?? settings.subtitlesLanguage; + const subtitlesTrack = savedTrackId ? findTrackById(video.state.subtitlesTracks, savedTrackId) : - findTrackByLang(video.state.subtitlesTracks, settings.subtitlesLanguage); + findTrackByLang(video.state.subtitlesTracks, fallbackLang); const extraSubtitlesTrack = savedTrackId ? findTrackById(video.state.extraSubtitlesTracks, savedTrackId) : - findTrackByLang(video.state.extraSubtitlesTracks, settings.subtitlesLanguage); + findTrackByLang(video.state.extraSubtitlesTracks, fallbackLang); if (subtitlesTrack && subtitlesTrack.id) { video.setSubtitlesTrack(subtitlesTrack.id); From fcd85bdcf4012c4630feeadfdf17b063b793b9bd Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 30 Mar 2026 18:40:24 +0300 Subject: [PATCH 16/46] feat: handle http/magnets from search bar --- src/common/index.js | 2 + src/common/usePlayUrl.ts | 59 +++++++++++++++++++ src/common/useTorrent.js | 8 ++- .../NavMenu/NavMenuContent.js | 17 ++++-- .../HorizontalNavBar/SearchBar/SearchBar.js | 17 +++--- src/services/Core/CoreTransport.js | 3 + src/services/Core/types.d.ts | 1 + 7 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 src/common/usePlayUrl.ts diff --git a/src/common/index.js b/src/common/index.js index 1b248c1ff..1d564ad38 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -26,6 +26,7 @@ const { default: useSettings } = require('./useSettings'); const { default: useShell } = require('./useShell'); const useStreamingServer = require('./useStreamingServer'); const { default: useTimeout } = require('./useTimeout'); +const { default: usePlayUrl } = require('./usePlayUrl'); const useTorrent = require('./useTorrent'); const useTranslate = require('./useTranslate'); const { default: useOrientation } = require('./useOrientation'); @@ -65,6 +66,7 @@ module.exports = { useShell, useStreamingServer, useTimeout, + usePlayUrl, useTorrent, useTranslate, useOrientation, diff --git a/src/common/usePlayUrl.ts b/src/common/usePlayUrl.ts new file mode 100644 index 000000000..4e537a592 --- /dev/null +++ b/src/common/usePlayUrl.ts @@ -0,0 +1,59 @@ +import { useCallback } from 'react'; +import magnet from 'magnet-uri'; +import { useServices } from 'stremio/services'; +import useToast from 'stremio/common/Toast/useToast'; +import useTorrent from 'stremio/common/useTorrent'; +import useStreamingServer from 'stremio/common/useStreamingServer'; + +const HTTP_REGEX = /^https?:\/\/.+/i; + +const usePlayUrl = () => { + const { core } = useServices(); + const toast = useToast(); + const { createTorrentFromMagnet } = useTorrent(); + const streamingServer = useStreamingServer(); + + const handlePlayUrl = useCallback(async (text: string): Promise => { + if (!text || !text.trim()) return false; + const trimmed = text.trim(); + + if (HTTP_REGEX.test(trimmed)) { + try { + const encoded = await core.transport.encodeStream({ url: trimmed }); + if (typeof encoded === 'string') { + window.location.hash = `#/player/${encodeURIComponent(encoded)}`; + return true; + } + } catch (e) { + console.error('Failed to encode stream:', e); + } + return false; + } + + try { + const parsed = magnet.decode(trimmed); + if (parsed && typeof parsed.infoHash === 'string') { + const serverReady = streamingServer.settings !== null + && streamingServer.settings.type === 'Ready'; + if (!serverReady) { + toast.show({ + type: 'error', + title: 'Streaming server is not available. Cannot play magnet links.', + timeout: 5000 + }); + return false; + } + createTorrentFromMagnet(trimmed); + return true; + } + } catch (e) { + // Not a valid magnet + } + + return false; + }, [streamingServer.settings, createTorrentFromMagnet]); + + return { handlePlayUrl }; +}; + +export default usePlayUrl; diff --git a/src/common/useTorrent.js b/src/common/useTorrent.js index 0ae117d3a..64b529b04 100644 --- a/src/common/useTorrent.js +++ b/src/common/useTorrent.js @@ -6,6 +6,8 @@ const { useServices } = require('stremio/services'); const useToast = require('stremio/common/Toast/useToast'); const useStreamingServer = require('stremio/common/useStreamingServer'); +const CREATE_TORRENT_TIMEOUT = 20000; + const useTorrent = () => { const { core } = useServices(); const streamingServer = useStreamingServer(); @@ -25,10 +27,10 @@ const useTorrent = () => { createTorrentTimeout.current = setTimeout(() => { toast.show({ type: 'error', - title: 'It\'s taking a long time to get metadata from the torrent.', - timeout: 10000 + title: 'Failed to get metadata from the torrent. No peers found.', + timeout: 8000 }); - }, 10000); + }, CREATE_TORRENT_TIMEOUT); } }, []); React.useEffect(() => { diff --git a/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js b/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js index 4b2fdc87e..b6309a44c 100644 --- a/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js +++ b/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js @@ -10,7 +10,8 @@ const { Button } = require('stremio/components'); const { default: useFullscreen } = require('stremio/common/useFullscreen'); const useProfile = require('stremio/common/useProfile'); const usePWA = require('stremio/common/usePWA'); -const useTorrent = require('stremio/common/useTorrent'); +const { default: usePlayUrl } = require('stremio/common/usePlayUrl'); +const useToast = require('stremio/common/Toast/useToast'); const { withCoreSuspender } = require('stremio/common/CoreSuspender'); const useStreamingServer = require('stremio/common/useStreamingServer'); const styles = require('./styles'); @@ -20,7 +21,8 @@ const NavMenuContent = ({ onClick }) => { const { core } = useServices(); const profile = useProfile(); const streamingServer = useStreamingServer(); - const { createTorrentFromMagnet } = useTorrent(); + const { handlePlayUrl } = usePlayUrl(); + const toast = useToast(); const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen(); const [isIOSPWA, isAndroidPWA] = usePWA(); const streamingServerWarningDismissed = React.useMemo(() => { @@ -40,11 +42,18 @@ const NavMenuContent = ({ onClick }) => { const onPlayMagnetLinkClick = React.useCallback(async () => { try { const clipboardText = await navigator.clipboard.readText(); - createTorrentFromMagnet(clipboardText); + const handled = await handlePlayUrl(clipboardText); + if (!handled) { + toast.show({ + type: 'error', + title: 'Clipboard does not contain a valid URL or magnet link.', + timeout: 5000 + }); + } } catch(e) { console.error(e); } - }, []); + }, [handlePlayUrl]); return (
diff --git a/src/components/NavBar/HorizontalNavBar/SearchBar/SearchBar.js b/src/components/NavBar/HorizontalNavBar/SearchBar/SearchBar.js index 0bc95cd73..e8f58bdff 100644 --- a/src/components/NavBar/HorizontalNavBar/SearchBar/SearchBar.js +++ b/src/components/NavBar/HorizontalNavBar/SearchBar/SearchBar.js @@ -9,7 +9,7 @@ const { default: Icon } = require('@stremio/stremio-icons/react'); const { useRouteFocused } = require('stremio-router'); const Button = require('stremio/components/Button').default; const TextInput = require('stremio/components/TextInput').default; -const useTorrent = require('stremio/common/useTorrent'); +const { default: usePlayUrl } = require('stremio/common/usePlayUrl'); const { withCoreSuspender } = require('stremio/common/CoreSuspender'); const useSearchHistory = require('./useSearchHistory'); const useLocalSearch = require('./useLocalSearch'); @@ -21,7 +21,7 @@ const SearchBar = React.memo(({ className, query, active }) => { const routeFocused = useRouteFocused(); const searchHistory = useSearchHistory(); const localSearch = useLocalSearch(); - const { createTorrentFromMagnet } = useTorrent(); + const { handlePlayUrl } = usePlayUrl(); const [historyOpen, openHistory, closeHistory, ] = useBinaryState(query === null ? true : false); const [currentQuery, setCurrentQuery] = React.useState(query || ''); @@ -52,12 +52,14 @@ const SearchBar = React.memo(({ className, query, active }) => { const value = searchInputRef.current.value; setCurrentQuery(value); openHistory(); - try { - createTorrentFromMagnet(value); - } catch (error) { - console.error('Failed to create torrent from magnet:', error); + }, []); + + const queryInputOnPaste = React.useCallback((event) => { + const pasted = event.clipboardData.getData('text'); + if (pasted) { + handlePlayUrl(pasted); } - }, [createTorrentFromMagnet]); + }, [handlePlayUrl]); const queryInputOnSubmit = React.useCallback((event) => { event.preventDefault(); @@ -108,6 +110,7 @@ const SearchBar = React.memo(({ className, query, active }) => { defaultValue={query} tabIndex={-1} onChange={queryInputOnChange} + onPaste={queryInputOnPaste} onSubmit={queryInputOnSubmit} onClick={openHistory} /> diff --git a/src/services/Core/CoreTransport.js b/src/services/Core/CoreTransport.js index b140551f6..c497d75c6 100644 --- a/src/services/Core/CoreTransport.js +++ b/src/services/Core/CoreTransport.js @@ -52,6 +52,9 @@ function CoreTransport(args) { this.decodeStream = async function(stream) { return bridge.call(['decodeStream'], [stream]); }; + this.encodeStream = async function(stream) { + return bridge.call(['encodeStream'], [stream]); + }; } module.exports = CoreTransport; diff --git a/src/services/Core/types.d.ts b/src/services/Core/types.d.ts index 7e4249ac8..668479e12 100644 --- a/src/services/Core/types.d.ts +++ b/src/services/Core/types.d.ts @@ -17,6 +17,7 @@ interface CoreTransport { getState: (model: string) => Promise, dispatch: (action: Action, model?: string) => Promise, decodeStream: (stream: string) => Promise, + encodeStream: (stream: object) => Promise, analytics: (event: AnalyticsEvent) => Promise, on: (name: string, listener: () => void) => void, off: (name: string, listener: () => void) => void, From e0961ec686a6046e8439d9871010500d074a0aae Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 30 Mar 2026 18:47:20 +0300 Subject: [PATCH 17/46] remove the try catch --- src/common/usePlayUrl.ts | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/common/usePlayUrl.ts b/src/common/usePlayUrl.ts index 4e537a592..2490b89bb 100644 --- a/src/common/usePlayUrl.ts +++ b/src/common/usePlayUrl.ts @@ -30,24 +30,20 @@ const usePlayUrl = () => { return false; } - try { - const parsed = magnet.decode(trimmed); - if (parsed && typeof parsed.infoHash === 'string') { - const serverReady = streamingServer.settings !== null - && streamingServer.settings.type === 'Ready'; - if (!serverReady) { - toast.show({ - type: 'error', - title: 'Streaming server is not available. Cannot play magnet links.', - timeout: 5000 - }); - return false; - } - createTorrentFromMagnet(trimmed); - return true; + const parsed = magnet.decode(trimmed); + if (parsed && typeof parsed.infoHash === 'string') { + const serverReady = streamingServer.settings !== null + && streamingServer.settings.type === 'Ready'; + if (!serverReady) { + toast.show({ + type: 'error', + title: 'Streaming server is not available. Cannot play magnet links.', + timeout: 5000 + }); + return false; } - } catch (e) { - // Not a valid magnet + createTorrentFromMagnet(trimmed); + return true; } return false; From ccc58326116b6c46ad7779087f20068c0bf64269 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 30 Mar 2026 21:39:13 +0300 Subject: [PATCH 18/46] chore update core for testing --- package.json | 2 +- pnpm-lock.yaml | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 4f1f16806..1a0465419 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@babel/runtime": "7.26.0", "@sentry/browser": "8.42.0", "@stremio/stremio-colors": "5.2.0", - "@stremio/stremio-core-web": "0.55.0", + "@stremio/stremio-core-web": "https://stremio.github.io/stremio-core/stremio-core-web/feat/add-encode-stream-func/dev/stremio-stremio-core-web-0.55.0.tgz", "@stremio/stremio-icons": "5.8.0", "@stremio/stremio-video": "0.0.70", "a-color-picker": "1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 321a4110b..4bc53214a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: 5.2.0 version: 5.2.0 '@stremio/stremio-core-web': - specifier: 0.55.0 - version: 0.55.0 + specifier: https://stremio.github.io/stremio-core/stremio-core-web/feat/add-encode-stream-func/dev/stremio-stremio-core-web-0.55.0.tgz + version: https://stremio.github.io/stremio-core/stremio-core-web/feat/add-encode-stream-func/dev/stremio-stremio-core-web-0.55.0.tgz '@stremio/stremio-icons': specifier: 5.8.0 version: 5.8.0 @@ -1120,8 +1120,9 @@ packages: '@stremio/stremio-colors@5.2.0': resolution: {integrity: sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg==} - '@stremio/stremio-core-web@0.55.0': - resolution: {integrity: sha512-MdalnThEwnA8osQh+3/5OMzVIYZOoYmd94dN3nmCeT4rfV7IZXRFUg/uyCY+5bqigStlE3SfKEaGiSc6UnNtlQ==} + '@stremio/stremio-core-web@https://stremio.github.io/stremio-core/stremio-core-web/feat/add-encode-stream-func/dev/stremio-stremio-core-web-0.55.0.tgz': + resolution: {tarball: https://stremio.github.io/stremio-core/stremio-core-web/feat/add-encode-stream-func/dev/stremio-stremio-core-web-0.55.0.tgz} + version: 0.55.0 '@stremio/stremio-icons@5.8.0': resolution: {integrity: sha512-IVUvQbIWfA4YEHCTed7v/sdQJCJ+OOCf84LTWpkE2W6GLQ+15WHcMEJrVkE1X3ekYJnGg3GjT0KLO6tKSU0P4w==} @@ -5870,7 +5871,7 @@ snapshots: '@stremio/stremio-colors@5.2.0': {} - '@stremio/stremio-core-web@0.55.0': + '@stremio/stremio-core-web@https://stremio.github.io/stremio-core/stremio-core-web/feat/add-encode-stream-func/dev/stremio-stremio-core-web-0.55.0.tgz': dependencies: '@babel/runtime': 7.24.1 From 6cb0c75555414d3149359f3bc7ce34f183c8c305 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Tue, 31 Mar 2026 12:56:08 +0300 Subject: [PATCH 19/46] feat: add loading state --- src/common/Toast/ToastContext.js | 1 + src/common/Toast/ToastProvider.js | 7 ++++++- src/common/usePlayUrl.ts | 10 ++++++++++ src/common/useTorrent.js | 7 +++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/common/Toast/ToastContext.js b/src/common/Toast/ToastContext.js index 6a5ede356..cefe9071e 100644 --- a/src/common/Toast/ToastContext.js +++ b/src/common/Toast/ToastContext.js @@ -6,6 +6,7 @@ const React = require('react'); const ToastContext = React.createContext({ show: () => { }, + remove: () => { }, clear: () => { } }); diff --git a/src/common/Toast/ToastProvider.js b/src/common/Toast/ToastProvider.js index a9cab9bb4..e375267af 100644 --- a/src/common/Toast/ToastProvider.js +++ b/src/common/Toast/ToastProvider.js @@ -42,7 +42,7 @@ const ToastProvider = ({ className, children }) => { }, show: (item) => { if (filters.some((filter) => filter(item))) { - return; + return null; } const timeout = typeof item.timeout === 'number' && !isNaN(item.timeout) ? @@ -64,6 +64,11 @@ const ToastProvider = ({ className, children }) => { onClose: itemOnClose } }); + return id; + }, + remove: (id) => { + clearTimeout(id); + dispatch({ type: 'remove', id }); }, clear: () => { dispatch({ type: 'clear' }); diff --git a/src/common/usePlayUrl.ts b/src/common/usePlayUrl.ts index 2490b89bb..49fe386ed 100644 --- a/src/common/usePlayUrl.ts +++ b/src/common/usePlayUrl.ts @@ -18,6 +18,11 @@ const usePlayUrl = () => { const trimmed = text.trim(); if (HTTP_REGEX.test(trimmed)) { + toast.show({ + type: 'success', + title: 'Loading HTTP stream…', + timeout: 3000 + }); try { const encoded = await core.transport.encodeStream({ url: trimmed }); if (typeof encoded === 'string') { @@ -27,6 +32,11 @@ const usePlayUrl = () => { } catch (e) { console.error('Failed to encode stream:', e); } + toast.show({ + type: 'error', + title: 'Failed to load HTTP stream.', + timeout: 5000 + }); return false; } diff --git a/src/common/useTorrent.js b/src/common/useTorrent.js index 64b529b04..96237e3a5 100644 --- a/src/common/useTorrent.js +++ b/src/common/useTorrent.js @@ -13,9 +13,15 @@ const useTorrent = () => { const streamingServer = useStreamingServer(); const toast = useToast(); const createTorrentTimeout = React.useRef(null); + const parsingToastId = React.useRef(null); const createTorrentFromMagnet = React.useCallback((text) => { const parsed = magnet.decode(text); if (parsed && typeof parsed.infoHash === 'string') { + parsingToastId.current = toast.show({ + type: 'success', + title: 'Parsing magnet link…', + timeout: CREATE_TORRENT_TIMEOUT + }); core.transport.dispatch({ action: 'StreamingServer', args: { @@ -38,6 +44,7 @@ const useTorrent = () => { const [, { type }] = streamingServer.torrent; if (type === 'Ready') { clearTimeout(createTorrentTimeout.current); + toast.remove(parsingToastId.current); } } }, [streamingServer.torrent]); From de32e3a765aa3afed650ea6d9384f49b494aca82 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Tue, 31 Mar 2026 13:11:48 +0300 Subject: [PATCH 20/46] fix: correct message for fail --- src/common/useTorrent.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/useTorrent.js b/src/common/useTorrent.js index 96237e3a5..cad2af9ba 100644 --- a/src/common/useTorrent.js +++ b/src/common/useTorrent.js @@ -31,9 +31,10 @@ const useTorrent = () => { }); clearTimeout(createTorrentTimeout.current); createTorrentTimeout.current = setTimeout(() => { + toast.remove(parsingToastId.current); toast.show({ type: 'error', - title: 'Failed to get metadata from the torrent. No peers found.', + title: 'Failed to parse magnet link.', timeout: 8000 }); }, CREATE_TORRENT_TIMEOUT); From e260e523227745902820ad6d37ee27402764a832 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Tue, 31 Mar 2026 13:45:08 +0300 Subject: [PATCH 21/46] refactor: change type to info --- src/App/ServicesToaster.js | 2 +- src/common/useTorrent.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/App/ServicesToaster.js b/src/App/ServicesToaster.js index a12757169..7ca7e2914 100644 --- a/src/App/ServicesToaster.js +++ b/src/App/ServicesToaster.js @@ -44,7 +44,7 @@ const ServicesToaster = () => { } case 'MagnetParsed': { toast.show({ - type: 'success', + type: 'info', title: 'Magnet link parsed', timeout: 4000 }); diff --git a/src/common/useTorrent.js b/src/common/useTorrent.js index cad2af9ba..2527154a2 100644 --- a/src/common/useTorrent.js +++ b/src/common/useTorrent.js @@ -19,7 +19,7 @@ const useTorrent = () => { if (parsed && typeof parsed.infoHash === 'string') { parsingToastId.current = toast.show({ type: 'success', - title: 'Parsing magnet link…', + title: 'Loading magnet link…', timeout: CREATE_TORRENT_TIMEOUT }); core.transport.dispatch({ From c99d7c5d8273919af330a237a02cd86464365cc8 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Sun, 5 Apr 2026 15:05:47 +0300 Subject: [PATCH 22/46] use new action metaItemMarkAsWatched --- src/routes/Discover/Discover.js | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index 647f55248..c81cd18f2 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -10,7 +10,6 @@ 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; @@ -24,18 +23,11 @@ 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 } : {} - }; + const selectedMetaItem = React.useMemo(() => { + return discover.catalog?.content.type === 'Ready' && + discover.catalog.content.content[selectedMetaItemIndex] || null; }, [discover.catalog, selectedMetaItemIndex]); - useMetaDetails(metaDetailsParams); - const metasContainerRef = React.useRef(); const metaPreviewRef = React.useRef(); @@ -85,10 +77,13 @@ const Discover = ({ urlParams, queryParams }) => { } core.transport.dispatch({ - action: 'MetaDetails', + action: 'Ctx', args: { - action: 'MarkAsWatched', - args: !selectedMetaItem.watched + action: 'MetaItemMarkAsWatched', + args: { + meta_item: selectedMetaItem, + is_watched: !selectedMetaItem.watched, + } } }); }, [selectedMetaItem]); From 234d83ad346c2ee5b3b51aba4ddcb92aec55de8d Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Wed, 8 Apr 2026 13:49:00 +0300 Subject: [PATCH 23/46] chore: use dev build --- package.json | 2 +- pnpm-lock.yaml | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 4f1f16806..ac0bdaef0 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@babel/runtime": "7.26.0", "@sentry/browser": "8.42.0", "@stremio/stremio-colors": "5.2.0", - "@stremio/stremio-core-web": "0.55.0", + "@stremio/stremio-core-web": "https://stremio.github.io/stremio-core/stremio-core-web/feat/add-mark-metaitem-as-watched/stremio-stremio-core-web-0.55.0.tgz", "@stremio/stremio-icons": "5.8.0", "@stremio/stremio-video": "0.0.70", "a-color-picker": "1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 321a4110b..84d18a1c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: 5.2.0 version: 5.2.0 '@stremio/stremio-core-web': - specifier: 0.55.0 - version: 0.55.0 + specifier: https://stremio.github.io/stremio-core/stremio-core-web/feat/add-mark-metaitem-as-watched/stremio-stremio-core-web-0.55.0.tgz + version: https://stremio.github.io/stremio-core/stremio-core-web/feat/add-mark-metaitem-as-watched/stremio-stremio-core-web-0.55.0.tgz '@stremio/stremio-icons': specifier: 5.8.0 version: 5.8.0 @@ -1120,8 +1120,9 @@ packages: '@stremio/stremio-colors@5.2.0': resolution: {integrity: sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg==} - '@stremio/stremio-core-web@0.55.0': - resolution: {integrity: sha512-MdalnThEwnA8osQh+3/5OMzVIYZOoYmd94dN3nmCeT4rfV7IZXRFUg/uyCY+5bqigStlE3SfKEaGiSc6UnNtlQ==} + '@stremio/stremio-core-web@https://stremio.github.io/stremio-core/stremio-core-web/feat/add-mark-metaitem-as-watched/stremio-stremio-core-web-0.55.0.tgz': + resolution: {tarball: https://stremio.github.io/stremio-core/stremio-core-web/feat/add-mark-metaitem-as-watched/stremio-stremio-core-web-0.55.0.tgz} + version: 0.55.0 '@stremio/stremio-icons@5.8.0': resolution: {integrity: sha512-IVUvQbIWfA4YEHCTed7v/sdQJCJ+OOCf84LTWpkE2W6GLQ+15WHcMEJrVkE1X3ekYJnGg3GjT0KLO6tKSU0P4w==} @@ -5870,7 +5871,7 @@ snapshots: '@stremio/stremio-colors@5.2.0': {} - '@stremio/stremio-core-web@0.55.0': + '@stremio/stremio-core-web@https://stremio.github.io/stremio-core/stremio-core-web/feat/add-mark-metaitem-as-watched/stremio-stremio-core-web-0.55.0.tgz': dependencies: '@babel/runtime': 7.24.1 From b69165e86895406c1c646ede9f410f3c95f84d6f Mon Sep 17 00:00:00 2001 From: Ignacio Date: Fri, 10 Apr 2026 10:27:25 -0300 Subject: [PATCH 24/46] fix: pass track object to subtitle callbacks and fix lang fallback chain --- src/routes/Player/Player.js | 40 ++++++++----------- .../Player/SubtitlesMenu/SubtitlesMenu.js | 15 +++---- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 22af20a32..f9834b467 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -31,7 +31,6 @@ const { default: Indicator } = require('./Indicator/Indicator'); const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang); const findTrackById = (tracks, id) => tracks.find((track) => track.id === id); -const getTrackLang = (tracks, id) => id !== null ? findTrackById(tracks, id)?.lang : null; const Player = ({ urlParams, queryParams }) => { const { t } = useTranslation(); @@ -236,27 +235,19 @@ const Player = ({ urlParams, queryParams }) => { }, []); - const onSubtitlesTrackSelected = React.useCallback((id) => { - video.setSubtitlesTrack(id); + const onSubtitlesTrackSelected = React.useCallback((track) => { + video.setSubtitlesTrack(track?.id ?? null); streamStateChanged({ - subtitleTrack: { - id, - embedded: true, - lang: getTrackLang(video.state.subtitlesTracks, id), - }, + subtitleTrack: track ? { id: track.id, embedded: true, lang: track.lang } : null, }); - }, [streamStateChanged, video.state.subtitlesTracks]); + }, [streamStateChanged]); - const onExtraSubtitlesTrackSelected = React.useCallback((id) => { - video.setExtraSubtitlesTrack(id); + const onExtraSubtitlesTrackSelected = React.useCallback((track) => { + video.setExtraSubtitlesTrack(track?.id ?? null); streamStateChanged({ - subtitleTrack: { - id, - embedded: false, - lang: getTrackLang(video.state.extraSubtitlesTracks, id), - }, + subtitleTrack: track ? { id: track.id, embedded: false, lang: track.lang } : null, }); - }, [streamStateChanged, video.state.extraSubtitlesTracks]); + }, [streamStateChanged]); const onAudioTrackSelected = React.useCallback((id) => { video.setAudioTrack(id); @@ -470,15 +461,16 @@ const Player = ({ urlParams, queryParams }) => { const savedTrackId = player.streamState?.subtitleTrack?.id; const savedLang = player.streamState?.subtitleTrack?.lang; - const fallbackLang = savedLang ?? settings.subtitlesLanguage; - const subtitlesTrack = savedTrackId ? - findTrackById(video.state.subtitlesTracks, savedTrackId) : - findTrackByLang(video.state.subtitlesTracks, fallbackLang); + const subtitlesTrack = + savedTrackId ? findTrackById(video.state.subtitlesTracks, savedTrackId) : + savedLang ? findTrackByLang(video.state.subtitlesTracks, savedLang) : + findTrackByLang(video.state.subtitlesTracks, settings.subtitlesLanguage); - const extraSubtitlesTrack = savedTrackId ? - findTrackById(video.state.extraSubtitlesTracks, savedTrackId) : - findTrackByLang(video.state.extraSubtitlesTracks, fallbackLang); + const extraSubtitlesTrack = + savedTrackId ? findTrackById(video.state.extraSubtitlesTracks, savedTrackId) : + savedLang ? findTrackByLang(video.state.extraSubtitlesTracks, savedLang) : + findTrackByLang(video.state.extraSubtitlesTracks, settings.subtitlesLanguage); if (subtitlesTrack && subtitlesTrack.id) { video.setSubtitlesTrack(subtitlesTrack.id); diff --git a/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js b/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js index fe690d0c4..3cc9bdc9c 100644 --- a/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js +++ b/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js @@ -80,25 +80,26 @@ const SubtitlesMenu = React.memo((props) => { } } else if (track.embedded) { if (typeof props.onSubtitlesTrackSelected === 'function') { - props.onSubtitlesTrackSelected(track.id); + props.onSubtitlesTrackSelected(track); } } else { if (typeof props.onExtraSubtitlesTrackSelected === 'function') { - props.onExtraSubtitlesTrackSelected(track.id); + props.onExtraSubtitlesTrackSelected(track); } } }, [props.subtitlesTracks, props.extraSubtitlesTracks, props.onSubtitlesTrackSelected, props.onExtraSubtitlesTrackSelected]); const subtitlesTrackOnClick = React.useCallback((event) => { - if (event.currentTarget.dataset.embedded === 'true') { + const track = subtitlesTracksForLanguage.find((t) => t.id === event.currentTarget.dataset.id) ?? null; + if (track?.embedded) { if (typeof props.onSubtitlesTrackSelected === 'function') { - props.onSubtitlesTrackSelected(event.currentTarget.dataset.id); + props.onSubtitlesTrackSelected(track); } } else { if (typeof props.onExtraSubtitlesTrackSelected === 'function') { - props.onExtraSubtitlesTrackSelected(event.currentTarget.dataset.id); + props.onExtraSubtitlesTrackSelected(track); } } - }, [props.onSubtitlesTrackSelected, props.onExtraSubtitlesTrackSelected]); + }, [subtitlesTracksForLanguage, props.onSubtitlesTrackSelected, props.onExtraSubtitlesTrackSelected]); const onSubtitlesDelayChanged = React.useCallback((value) => { if (typeof props.selectedExtraSubtitlesTrackId === 'string') { if (props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay)) { @@ -175,7 +176,7 @@ const SubtitlesMenu = React.memo((props) => { subtitlesTracksForLanguage.length > 0 ?
{subtitlesTracksForLanguage.map((track, index) => ( -