mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-20 19:02:15 +00:00
Merge pull request #489 from Stremio/refactor-continue-watching
Update continue watching ui/ux
This commit is contained in:
commit
d3739e74b6
7 changed files with 230 additions and 41 deletions
72
src/common/ContinueWatchingItem/ContinueWatchingItem.js
Normal file
72
src/common/ContinueWatchingItem/ContinueWatchingItem.js
Normal 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;
|
||||
5
src/common/ContinueWatchingItem/index.js
Normal file
5
src/common/ContinueWatchingItem/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const ContineWatchingItem = require('./ContinueWatchingItem');
|
||||
|
||||
module.exports = ContineWatchingItem;
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
:
|
||||
|
|
|
|||
Loading…
Reference in a new issue