diff --git a/src/components/MetaPreview/Ratings/Ratings.less b/src/components/ActionsGroup/ActionsGroup.less similarity index 96% rename from src/components/MetaPreview/Ratings/Ratings.less rename to src/components/ActionsGroup/ActionsGroup.less index ffba0415b..09e903435 100644 --- a/src/components/MetaPreview/Ratings/Ratings.less +++ b/src/components/ActionsGroup/ActionsGroup.less @@ -8,7 +8,7 @@ @width-mobile: 3rem; -.ratings-container { +.group-container { display: flex; flex-direction: row; align-items: center; @@ -46,7 +46,7 @@ } @media @phone-landscape { - .ratings-container { + .group-container { height: @height-mobile; .icon-container { diff --git a/src/components/ActionsGroup/ActionsGroup.tsx b/src/components/ActionsGroup/ActionsGroup.tsx new file mode 100644 index 000000000..052f25016 --- /dev/null +++ b/src/components/ActionsGroup/ActionsGroup.tsx @@ -0,0 +1,45 @@ +// Copyright (C) 2017-2025 Smart code 203358507 + +import classNames from 'classnames'; +import React from 'react'; +import Icon from '@stremio/stremio-icons/react'; +import { Tooltip } from 'stremio/common/Tooltips'; +import styles from './ActionsGroup.less'; + +type Item = { + icon: string; + label?: string; + filled?: string; + disabled?: boolean; + className?: string; + onClick?: () => void; +}; + +type Props = { + items: Item[]; + className?: string; +}; + +const ActionsGroup = ({ items, className }: Props) => { + return ( +
+ { + items.map((item, index) => ( +
+ { + item.label && + + } + +
+ )) + } +
+ ); +}; + +export default ActionsGroup; diff --git a/src/components/ActionsGroup/index.ts b/src/components/ActionsGroup/index.ts new file mode 100644 index 000000000..4dea1b83a --- /dev/null +++ b/src/components/ActionsGroup/index.ts @@ -0,0 +1,6 @@ +// Copyright (C) 2017-2025 Smart code 203358507 + +import ActionsGroup from './ActionsGroup'; + +export default ActionsGroup; + diff --git a/src/components/MetaPreview/MetaPreview.js b/src/components/MetaPreview/MetaPreview.js index 13717919a..5fa7d8ff0 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 { default: ActionsGroup } = require('stremio/components/ActionsGroup'); const ModalDialog = require('stremio/components/ModalDialog'); const SharePrompt = require('stremio/components/SharePrompt'); const CONSTANTS = require('stremio/common/CONSTANTS'); @@ -25,7 +26,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(() => { @@ -98,6 +99,18 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou const renderLogoFallback = React.useCallback(() => (
{name}
), [name]); + const metaItemActions = 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]); return (
{ @@ -195,19 +208,6 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou }
- { - typeof toggleInLibrary === 'function' ? - - : - null - } { typeof trailerHref === 'string' ? + : null + } { typeof showHref === 'string' && compact ? : null @@ -298,6 +303,8 @@ MetaPreview.propTypes = { trailerStreams: PropTypes.array, inLibrary: PropTypes.bool, toggleInLibrary: PropTypes.func, + watched: PropTypes.bool, + toggleWatched: PropTypes.func, ratingInfo: PropTypes.object, }; diff --git a/src/components/MetaPreview/Ratings/Ratings.tsx b/src/components/MetaPreview/Ratings/Ratings.tsx index 6bef0cc6d..329ee4945 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 { ActionsGroup } from 'stremio/components'; type Props = { metaId?: string; @@ -16,15 +14,21 @@ 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]); + return ( -
-
- -
-
- -
-
+ ); }; diff --git a/src/components/MetaPreview/styles.less b/src/components/MetaPreview/styles.less index 3fea95a5f..3b21c0ed6 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,11 +207,20 @@ } } } - } + + .group-container { + margin-bottom: 1rem; - .ratings { - margin-bottom: 1rem; - margin-right: 1rem; + &:global(.wide) { + width: auto; + padding: 0 2rem; + border-radius: 4rem; + } + + &:not(:last-child) { + margin-right: 1rem; + } + } } } @@ -233,17 +242,13 @@ padding-top: 1.5rem; gap: 0.5rem; - .action-button { + .action-button, .group-container { padding: 0 1.5rem !important; margin-right: 0rem !important; height: 3rem; border-radius: 2rem; } } - - .ratings { - margin-right: 0; - } } } @@ -272,6 +277,10 @@ &::-webkit-scrollbar { display: none; } + + .action-button { + padding: 0 1rem !important; + } } } 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 }; diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index a28a86405..bcdbf5af9 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,14 +53,6 @@ const Discover = ({ urlParams, queryParams }) => { } } }, [hasNextPage, loadNextPage]); - const selectedMetaItem = React.useMemo(() => { - return discover.catalog !== null && - discover.catalog.content.type === 'Ready' && - discover.catalog.content.content[selectedMetaItemIndex] ? - discover.catalog.content.content[selectedMetaItemIndex] - : - null; - }, [discover.catalog, selectedMetaItemIndex]); const addToLibrary = React.useCallback(() => { if (selectedMetaItem === null) { return; @@ -74,6 +79,19 @@ const Discover = ({ urlParams, queryParams }) => { } }); }, [selectedMetaItem]); + const toggleWatched = React.useCallback(() => { + if (selectedMetaItem === null) { + return; + } + + core.transport.dispatch({ + action: 'MetaDetails', + args: { + action: 'MarkAsWatched', + args: !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)); @@ -193,6 +211,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 9f2279bd9..8376cdf2c 100644 --- a/src/routes/MetaDetails/MetaDetails.js +++ b/src/routes/MetaDetails/MetaDetails.js @@ -64,6 +64,19 @@ const MetaDetails = ({ urlParams, queryParams }) => { } }); }, [metaDetails]); + const toggleWatched = React.useCallback(() => { + if (metaDetails.metaItem === null || metaDetails.metaItem.content.type !== 'Ready') { + return; + } + + core.transport.dispatch({ + action: 'MetaDetails', + args: { + action: 'MarkAsWatched', + args: !metaDetails.metaItem.content.content.watched + } + }); + }, [metaDetails]); const toggleNotifications = React.useCallback(() => { if (metaDetails.libraryItem) { core.transport.dispatch({ @@ -172,6 +185,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} />