mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 17:15:48 +00:00
Merge b9540af66f into b24426250b
This commit is contained in:
commit
d569d639fe
9 changed files with 154 additions and 46 deletions
|
|
@ -8,7 +8,7 @@
|
||||||
@width-mobile: 3rem;
|
@width-mobile: 3rem;
|
||||||
|
|
||||||
|
|
||||||
.ratings-container {
|
.group-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media @phone-landscape {
|
@media @phone-landscape {
|
||||||
.ratings-container {
|
.group-container {
|
||||||
height: @height-mobile;
|
height: @height-mobile;
|
||||||
|
|
||||||
.icon-container {
|
.icon-container {
|
||||||
45
src/components/ActionsGroup/ActionsGroup.tsx
Normal file
45
src/components/ActionsGroup/ActionsGroup.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div className={classNames(styles['group-container'], className)}>
|
||||||
|
{
|
||||||
|
items.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={classNames(styles['icon-container'], item.className, { [styles['disabled']]: item.disabled })}
|
||||||
|
onClick={item.onClick}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
item.label &&
|
||||||
|
<Tooltip label={item.label} position={'top'} />
|
||||||
|
}
|
||||||
|
<Icon name={item.icon} className={styles['icon']} />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActionsGroup;
|
||||||
6
src/components/ActionsGroup/index.ts
Normal file
6
src/components/ActionsGroup/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// Copyright (C) 2017-2025 Smart code 203358507
|
||||||
|
|
||||||
|
import ActionsGroup from './ActionsGroup';
|
||||||
|
|
||||||
|
export default ActionsGroup;
|
||||||
|
|
||||||
|
|
@ -8,6 +8,7 @@ const { useTranslation } = require('react-i18next');
|
||||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||||
const { default: Button } = require('stremio/components/Button');
|
const { default: Button } = require('stremio/components/Button');
|
||||||
const { default: Image } = require('stremio/components/Image');
|
const { default: Image } = require('stremio/components/Image');
|
||||||
|
const { default: ActionsGroup } = require('stremio/components/ActionsGroup');
|
||||||
const ModalDialog = require('stremio/components/ModalDialog');
|
const ModalDialog = require('stremio/components/ModalDialog');
|
||||||
const SharePrompt = require('stremio/components/SharePrompt');
|
const SharePrompt = require('stremio/components/SharePrompt');
|
||||||
const CONSTANTS = require('stremio/common/CONSTANTS');
|
const CONSTANTS = require('stremio/common/CONSTANTS');
|
||||||
|
|
@ -25,7 +26,7 @@ const ALLOWED_LINK_REDIRECTS = [
|
||||||
routesRegexp.metadetails.regexp
|
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 { t } = useTranslation();
|
||||||
const [shareModalOpen, openShareModal, closeShareModal] = useBinaryState(false);
|
const [shareModalOpen, openShareModal, closeShareModal] = useBinaryState(false);
|
||||||
const linksGroups = React.useMemo(() => {
|
const linksGroups = React.useMemo(() => {
|
||||||
|
|
@ -98,6 +99,18 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
|
||||||
const renderLogoFallback = React.useCallback(() => (
|
const renderLogoFallback = React.useCallback(() => (
|
||||||
<div className={styles['logo-placeholder']}>{name}</div>
|
<div className={styles['logo-placeholder']}>{name}</div>
|
||||||
), [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 (
|
return (
|
||||||
<div className={classnames(className, styles['meta-preview-container'], { [styles['compact']]: compact })} ref={ref}>
|
<div className={classnames(className, styles['meta-preview-container'], { [styles['compact']]: compact })} ref={ref}>
|
||||||
{
|
{
|
||||||
|
|
@ -195,19 +208,6 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['action-buttons-container']}>
|
<div className={styles['action-buttons-container']}>
|
||||||
{
|
|
||||||
typeof toggleInLibrary === 'function' ?
|
|
||||||
<ActionButton
|
|
||||||
className={styles['action-button']}
|
|
||||||
icon={inLibrary ? 'remove-from-library' : 'add-to-library'}
|
|
||||||
label={inLibrary ? t('REMOVE_FROM_LIB') : t('ADD_TO_LIB')}
|
|
||||||
tooltip={compact}
|
|
||||||
tabIndex={compact ? -1 : 0}
|
|
||||||
onClick={toggleInLibrary}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
null
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
typeof trailerHref === 'string' ?
|
typeof trailerHref === 'string' ?
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
|
@ -221,6 +221,11 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
typeof toggleInLibrary === 'function' && typeof toggleWatched === 'function'
|
||||||
|
? <ActionsGroup items={metaItemActions} className={styles['group-container']} />
|
||||||
|
: null
|
||||||
|
}
|
||||||
{
|
{
|
||||||
typeof showHref === 'string' && compact ?
|
typeof showHref === 'string' && compact ?
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
|
@ -237,7 +242,7 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
|
||||||
!compact && ratingInfo !== null ?
|
!compact && ratingInfo !== null ?
|
||||||
<Ratings
|
<Ratings
|
||||||
ratingInfo={ratingInfo}
|
ratingInfo={ratingInfo}
|
||||||
className={styles['ratings']}
|
className={styles['group-container']}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
|
|
@ -298,6 +303,8 @@ MetaPreview.propTypes = {
|
||||||
trailerStreams: PropTypes.array,
|
trailerStreams: PropTypes.array,
|
||||||
inLibrary: PropTypes.bool,
|
inLibrary: PropTypes.bool,
|
||||||
toggleInLibrary: PropTypes.func,
|
toggleInLibrary: PropTypes.func,
|
||||||
|
watched: PropTypes.bool,
|
||||||
|
toggleWatched: PropTypes.func,
|
||||||
ratingInfo: PropTypes.object,
|
ratingInfo: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,7 @@
|
||||||
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import useRating from './useRating';
|
import useRating from './useRating';
|
||||||
import styles from './Ratings.less';
|
import { ActionsGroup } from 'stremio/components';
|
||||||
import Icon from '@stremio/stremio-icons/react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
metaId?: string;
|
metaId?: string;
|
||||||
|
|
@ -16,15 +14,21 @@ const Ratings = ({ ratingInfo, className }: Props) => {
|
||||||
const { onLiked, onLoved, liked, loved } = useRating(ratingInfo);
|
const { onLiked, onLoved, liked, loved } = useRating(ratingInfo);
|
||||||
const disabled = useMemo(() => ratingInfo?.type !== 'Ready', [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 (
|
return (
|
||||||
<div className={classNames(styles['ratings-container'], className)}>
|
<ActionsGroup items={items} className={className} />
|
||||||
<div className={classNames(styles['icon-container'], { [styles['disabled']]: disabled })} onClick={onLiked}>
|
|
||||||
<Icon name={liked ? 'thumbs-up' : 'thumbs-up-outline'} className={styles['icon']} />
|
|
||||||
</div>
|
|
||||||
<div className={classNames(styles['icon-container'], { [styles['disabled']]: disabled })} onClick={onLoved}>
|
|
||||||
<Icon name={loved ? 'heart' : 'heart-outline'} className={styles['icon']} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
.action-buttons-container {
|
.action-buttons-container {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
.action-button:not(:last-child) {
|
.action-button:not(:last-child), .group-container:not(:last-child) {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -207,11 +207,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
.group-container {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
.ratings {
|
&:global(.wide) {
|
||||||
margin-bottom: 1rem;
|
width: auto;
|
||||||
margin-right: 1rem;
|
padding: 0 2rem;
|
||||||
|
border-radius: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,17 +242,13 @@
|
||||||
padding-top: 1.5rem;
|
padding-top: 1.5rem;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
.action-button {
|
.action-button, .group-container {
|
||||||
padding: 0 1.5rem !important;
|
padding: 0 1.5rem !important;
|
||||||
margin-right: 0rem !important;
|
margin-right: 0rem !important;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
border-radius: 2rem;
|
border-radius: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ratings {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,6 +277,10 @@
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
padding: 0 1rem !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import TextInput from './TextInput';
|
||||||
import Toggle from './Toggle';
|
import Toggle from './Toggle';
|
||||||
import Transition from './Transition';
|
import Transition from './Transition';
|
||||||
import Video from './Video';
|
import Video from './Video';
|
||||||
|
import ActionsGroup from './ActionsGroup';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AddonDetailsModal,
|
AddonDetailsModal,
|
||||||
|
|
@ -65,4 +66,5 @@ export {
|
||||||
Toggle,
|
Toggle,
|
||||||
Transition,
|
Transition,
|
||||||
Video,
|
Video,
|
||||||
|
ActionsGroup
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ const { CONSTANTS, useBinaryState, useOnScrollToBottom, withCoreSuspender } = re
|
||||||
const { AddonDetailsModal, Button, DelayedRenderer, Image, MainNavBars, MetaItem, MetaPreview, ModalDialog, MultiselectMenu } = require('stremio/components');
|
const { AddonDetailsModal, Button, DelayedRenderer, Image, MainNavBars, MetaItem, MetaPreview, ModalDialog, MultiselectMenu } = require('stremio/components');
|
||||||
const useDiscover = require('./useDiscover');
|
const useDiscover = require('./useDiscover');
|
||||||
const useSelectableInputs = require('./useSelectableInputs');
|
const useSelectableInputs = require('./useSelectableInputs');
|
||||||
|
const useMetaDetails = require('../MetaDetails/useMetaDetails');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
||||||
const SCROLL_TO_BOTTOM_THRESHOLD = 400;
|
const SCROLL_TO_BOTTOM_THRESHOLD = 400;
|
||||||
|
|
@ -23,6 +24,18 @@ const Discover = ({ urlParams, queryParams }) => {
|
||||||
const [addonModalOpen, openAddonModal, closeAddonModal] = useBinaryState(false);
|
const [addonModalOpen, openAddonModal, closeAddonModal] = useBinaryState(false);
|
||||||
const [selectedMetaItemIndex, setSelectedMetaItemIndex] = React.useState(0);
|
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 metasContainerRef = React.useRef();
|
||||||
const metaPreviewRef = React.useRef();
|
const metaPreviewRef = React.useRef();
|
||||||
|
|
||||||
|
|
@ -40,14 +53,6 @@ const Discover = ({ urlParams, queryParams }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [hasNextPage, loadNextPage]);
|
}, [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(() => {
|
const addToLibrary = React.useCallback(() => {
|
||||||
if (selectedMetaItem === null) {
|
if (selectedMetaItem === null) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -74,6 +79,19 @@ const Discover = ({ urlParams, queryParams }) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [selectedMetaItem]);
|
}, [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) => {
|
const metaItemsOnFocusCapture = React.useCallback((event) => {
|
||||||
if (event.target.dataset.index !== null && !isNaN(event.target.dataset.index)) {
|
if (event.target.dataset.index !== null && !isNaN(event.target.dataset.index)) {
|
||||||
setSelectedMetaItemIndex(parseInt(event.target.dataset.index, 10));
|
setSelectedMetaItemIndex(parseInt(event.target.dataset.index, 10));
|
||||||
|
|
@ -193,6 +211,8 @@ const Discover = ({ urlParams, queryParams }) => {
|
||||||
trailerStreams={selectedMetaItem.trailerStreams}
|
trailerStreams={selectedMetaItem.trailerStreams}
|
||||||
inLibrary={selectedMetaItem.inLibrary}
|
inLibrary={selectedMetaItem.inLibrary}
|
||||||
toggleInLibrary={selectedMetaItem.inLibrary ? removeFromLibrary : addToLibrary}
|
toggleInLibrary={selectedMetaItem.inLibrary ? removeFromLibrary : addToLibrary}
|
||||||
|
watched={selectedMetaItem.watched}
|
||||||
|
toggleWatched={toggleWatched}
|
||||||
metaId={selectedMetaItem.id}
|
metaId={selectedMetaItem.id}
|
||||||
like={selectedMetaItem.like}
|
like={selectedMetaItem.like}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,19 @@ const MetaDetails = ({ urlParams, queryParams }) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [metaDetails]);
|
}, [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(() => {
|
const toggleNotifications = React.useCallback(() => {
|
||||||
if (metaDetails.libraryItem) {
|
if (metaDetails.libraryItem) {
|
||||||
core.transport.dispatch({
|
core.transport.dispatch({
|
||||||
|
|
@ -172,6 +185,8 @@ const MetaDetails = ({ urlParams, queryParams }) => {
|
||||||
trailerStreams={metaDetails.metaItem.content.content.trailerStreams}
|
trailerStreams={metaDetails.metaItem.content.content.trailerStreams}
|
||||||
inLibrary={metaDetails.metaItem.content.content.inLibrary}
|
inLibrary={metaDetails.metaItem.content.content.inLibrary}
|
||||||
toggleInLibrary={metaDetails.metaItem.content.content.inLibrary ? removeFromLibrary : addToLibrary}
|
toggleInLibrary={metaDetails.metaItem.content.content.inLibrary ? removeFromLibrary : addToLibrary}
|
||||||
|
watched={metaDetails.metaItem.content.content.watched}
|
||||||
|
toggleWatched={toggleWatched}
|
||||||
metaId={metaDetails.metaItem.content.content.id}
|
metaId={metaDetails.metaItem.content.content.id}
|
||||||
ratingInfo={metaDetails.ratingInfo}
|
ratingInfo={metaDetails.ratingInfo}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue