Merge pull request #489 from Stremio/refactor-continue-watching

Update continue watching ui/ux
This commit is contained in:
Tim 2023-10-30 05:12:02 -07:00 committed by GitHub
commit d3739e74b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 230 additions and 41 deletions

View file

@ -0,0 +1,72 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const { useServices } = require('stremio/services');
const useNotifications = require('stremio/common/useNotifications');
const LibItem = require('stremio/common/LibItem');
const ContinueWatchingItem = ({ _id, deepLinks, ...props }) => {
const { core } = useServices();
const notifications = useNotifications();
const newVideos = React.useMemo(() => {
const count = notifications.items?.[_id]?.length ?? 0;
return Math.min(Math.max(count, 0), 99);
}, [_id, notifications.items]);
const onClick = React.useCallback(() => {
if (deepLinks?.metaDetailsVideos ?? deepLinks?.metaDetailsStreams) {
window.location = deepLinks?.metaDetailsVideos ?? deepLinks?.metaDetailsStreams;
}
}, [deepLinks]);
const onPlayClick = React.useCallback((event) => {
event.stopPropagation();
if (deepLinks?.player ?? deepLinks?.metaDetailsStreams ?? deepLinks?.metaDetailsVideos) {
window.location = deepLinks?.player ?? deepLinks?.metaDetailsStreams ?? deepLinks?.metaDetailsVideos;
}
}, [deepLinks]);
const onDismissClick = React.useCallback((event) => {
event.stopPropagation();
if (typeof _id === 'string') {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'RewindLibraryItem',
args: _id
}
});
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'DismissNotificationItem',
args: _id
}
});
}
}, [_id]);
return (
<LibItem
{...props}
posterChangeCursor={true}
newVideos={newVideos}
onClick={onClick}
onPlayClick={onPlayClick}
onDismissClick={onDismissClick}
/>
);
};
ContinueWatchingItem.propTypes = {
_id: PropTypes.string,
deepLinks: PropTypes.shape({
metaDetailsVideos: PropTypes.string,
metaDetailsStreams: PropTypes.string,
player: PropTypes.string
}),
};
module.exports = ContinueWatchingItem;

View file

@ -0,0 +1,5 @@
// Copyright (C) 2017-2023 Smart code 203358507
const ContineWatchingItem = require('./ContinueWatchingItem');
module.exports = ContineWatchingItem;

View file

@ -4,7 +4,6 @@ const React = require('react');
const { useServices } = require('stremio/services');
const PropTypes = require('prop-types');
const MetaItem = require('stremio/common/MetaItem');
const useNotifications = require('stremio/common/useNotifications');
const { t } = require('i18next');
const OPTIONS = [
@ -16,11 +15,6 @@ const OPTIONS = [
const LibItem = ({ _id, removable, ...props }) => {
const { core } = useServices();
const notifications = useNotifications();
const newVideos = React.useMemo(() => {
const count = notifications.items?.[_id]?.length ?? 0;
return Math.min(Math.max(count, 0), 99);
}, [_id, notifications.items]);
const options = React.useMemo(() => {
return OPTIONS
.filter(({ value }) => {
@ -104,7 +98,6 @@ const LibItem = ({ _id, removable, ...props }) => {
return (
<MetaItem
{...props}
newVideos={newVideos}
options={options}
optionOnSelect={optionOnSelect}
/>

View file

@ -3,17 +3,18 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const filterInvalidDOMProps = require('filter-invalid-dom-props').default;
const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button');
const Image = require('stremio/common/Image');
const Multiselect = require('stremio/common/Multiselect');
const PlayIconCircleCentered = require('stremio/common/PlayIconCircleCentered');
const useBinaryState = require('stremio/common/useBinaryState');
const { ICON_FOR_TYPE } = require('stremio/common/CONSTANTS');
const styles = require('./styles');
const MetaItem = React.memo(({ className, type, name, poster, posterShape, playIcon, progress, newVideos, options, deepLinks, dataset, optionOnSelect, ...props }) => {
const MetaItem = React.memo(({ className, type, name, poster, posterShape, posterChangeCursor, progress, newVideos, options, deepLinks, dataset, optionOnSelect, onDismissClick, onPlayClick, ...props }) => {
const { t } = useTranslation();
const [menuOpen, onMenuOpen, onMenuClose] = useBinaryState(false);
const href = React.useMemo(() => {
return deepLinks ?
@ -64,7 +65,16 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, playI
), []);
return (
<Button title={name} href={href} {...filterInvalidDOMProps(props)} className={classnames(className, styles['meta-item-container'], styles['poster-shape-poster'], styles[`poster-shape-${posterShape}`], { 'active': menuOpen })} onClick={metaItemOnClick}>
<div className={styles['poster-container']}>
<div className={classnames(styles['poster-container'], { 'poster-change-cursor': posterChangeCursor })}>
{
onDismissClick ?
<div title={t('LIBRARY_RESUME_DISMISS')} className={styles['dismiss-icon-layer']} onClick={onDismissClick}>
<Icon className={styles['dismiss-icon']} name={'close'} />
<div className={styles['dismiss-icon-backdrop']} />
</div>
:
null
}
<div className={styles['poster-image-layer']}>
<Image
className={styles['poster-image']}
@ -74,9 +84,11 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, playI
/>
</div>
{
playIcon ?
<div className={styles['play-icon-layer']}>
<PlayIconCircleCentered className={styles['play-icon']} />
onPlayClick ?
<div title={t('CONTINUE_WATCHING')} className={styles['play-icon-layer']} onClick={onPlayClick}>
<Icon className={styles['play-icon']} name={'play'} />
<div className={styles['play-icon-outer']} />
<div className={styles['play-icon-background']} />
</div>
:
null
@ -96,7 +108,10 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, playI
<div className={styles['layer']} />
<div className={styles['layer']} />
<div className={styles['layer']}>
+{newVideos}
<Icon className={styles['icon']} name={'add'} />
<div className={styles['label']}>
{newVideos}
</div>
</div>
</div>
:
@ -140,7 +155,7 @@ MetaItem.propTypes = {
name: PropTypes.string,
poster: PropTypes.string,
posterShape: PropTypes.oneOf(['poster', 'landscape', 'square']),
playIcon: PropTypes.bool,
posterChangeCursor: PropTypes.bool,
progress: PropTypes.number,
newVideos: PropTypes.number,
options: PropTypes.array,
@ -151,7 +166,9 @@ MetaItem.propTypes = {
}),
dataset: PropTypes.object,
optionOnSelect: PropTypes.func,
onClick: PropTypes.func
onDismissClick: PropTypes.func,
onPlayClick: PropTypes.func,
onClick: PropTypes.func,
};
module.exports = MetaItem;

View file

@ -18,6 +18,8 @@
play-icon-circle-centered-icon: icon;
}
@play-icon-size: 4rem;
.meta-item-container {
padding: 1rem;
overflow: visible;
@ -28,7 +30,25 @@
.poster-container {
box-shadow: 0 0 0 0.2rem var(--primary-foreground-color);
transform: scale(1.05);
.dismiss-icon-layer {
opacity: 1;
}
.poster-image-layer {
transform: scale(1.05);
}
.play-icon-layer {
.play-icon-outer {
color: transparent;
}
.play-icon-background {
background-color: var(--secondary-accent-color);
opacity: 1;
}
}
}
.title-bar-container {
@ -61,7 +81,49 @@
z-index: 0;
background-color: var(--overlay-color);
border-radius: var(--border-radius);
transition: all 0.1s ease-out;
&:global(.poster-change-cursor) {
.poster-image-layer {
&:hover {
cursor: zoom-in;
}
}
}
.dismiss-icon-layer {
z-index: -2;
position: absolute;
top: 0.5rem;
left: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
height: 1.5rem;
width: 1.5rem;
border-radius: 100%;
opacity: 0;
transition: opacity 0.1s ease-in;
.dismiss-icon {
z-index: 1;
position: relative;
height: 1.25rem;
width: 1.25rem;
color: var(--primary-foreground-color);
opacity: 0.8;
}
.dismiss-icon-backdrop {
z-index: 0;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: var(--primary-background-color);
opacity: 0.6;
}
}
.poster-image-layer {
position: absolute;
@ -74,6 +136,7 @@
flex-direction: row;
align-items: center;
justify-content: center;
transition: transform 0.1s ease-out;
.poster-image {
flex: none;
@ -94,27 +157,55 @@
}
.play-icon-layer {
position: absolute;
top: 30%;
right: 0;
bottom: 30%;
left: 0;
z-index: -2;
overflow: visible;
position: absolute;
top: 50%;
left: 50%;
margin-top: calc(@play-icon-size / -2);
margin-left: calc(@play-icon-size / -2);
display: flex;
align-items: center;
justify-content: center;
height: @play-icon-size;
width: @play-icon-size;
transition: transform 0.1s ease-out;
.play-icon {
display: block;
width: 100%;
height: 100%;
filter: drop-shadow(0 0 0.5rem @color-background);
z-index: 2;
position: relative;
height: 2.25rem;
width: 2.25rem;
color: var(--primary-foreground-color);
}
.play-icon-circle-centered-background {
color: @color-accent4-90;
}
.play-icon-outer {
z-index: 1;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
color: var(--primary-foreground-color);
box-shadow: 0 0 0 0.15rem currentColor inset;
border-radius: 100%;
transition: color 0.1s ease-in;
}
.play-icon-circle-centered-icon {
color: @color-surface-light5-90;
}
.play-icon-background {
z-index: 0;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: var(--primary-background-color);
border-radius: 100%;
opacity: 0.4;
transition: all 0.1s ease-in;
}
&:hover {
transform: scale(1.1);
}
}
@ -157,12 +248,9 @@
display: flex;
align-items: center;
justify-content: center;
height: 1.6rem;
width: 2.75rem;
height: 1.25rem;
width: 2.25rem;
border-radius: 0.25rem;
font-size: 1rem;
font-weight: 600;
color: var(--secondary-foreground-color);
&:nth-child(1) {
top: 0.5rem;
@ -183,6 +271,18 @@
right: 1rem;
background-color: var(--primary-foreground-color);
}
.icon {
height: 0.8rem;
width: 0.8rem;
color: var(--primary-accent-color);
}
.label {
font-size: 0.8rem;
font-weight: 600;
color: var(--primary-accent-color);
}
}
}
}

View file

@ -4,6 +4,7 @@ const AddonDetailsModal = require('./AddonDetailsModal');
const Button = require('./Button');
const Checkbox = require('./Checkbox');
const ColorInput = require('./ColorInput');
const ContinueWatchingItem = require('./ContinueWatchingItem');
const DelayedRenderer = require('./DelayedRenderer');
const Image = require('./Image');
const LibItem = require('./LibItem');
@ -50,6 +51,7 @@ module.exports = {
Button,
Checkbox,
ColorInput,
ContinueWatchingItem,
DelayedRenderer,
Image,
LibItem,

View file

@ -4,7 +4,7 @@ const React = require('react');
const classnames = require('classnames');
const debounce = require('lodash.debounce');
const { useTranslation } = require('react-i18next');
const { MainNavBars, MetaRow, LibItem, MetaItem, StreamingServerWarning, useStreamingServer, withCoreSuspender, getVisibleChildrenRange } = require('stremio/common');
const { MainNavBars, MetaRow, ContinueWatchingItem, MetaItem, StreamingServerWarning, useStreamingServer, withCoreSuspender, getVisibleChildrenRange } = require('stremio/common');
const useBoard = require('./useBoard');
const useContinueWatchingPreview = require('./useContinueWatchingPreview');
const styles = require('./styles');
@ -46,7 +46,7 @@ const Board = () => {
className={classnames(styles['board-row'], styles['continue-watching-row'], 'animation-fade-in')}
title={t('BOARD_CONTINUE_WATCHING')}
items={continueWatchingPreview.items}
itemComponent={LibItem}
itemComponent={ContinueWatchingItem}
deepLinks={continueWatchingPreview.deepLinks}
/>
: