mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-08-09 00:27:21 +00:00
Merge branch 'development' of github.com:Stremio/stremio-web into media-patch
This commit is contained in:
commit
e1700f80eb
7 changed files with 205 additions and 75 deletions
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -1833,9 +1833,9 @@
|
|||
"integrity": "sha512-yT3No1gIWKLV2BhQIeSgG94EzXxmEqXJLulO+pFpziqWNUbmmEKeE+nRvW5wtoIK4SLy+v0bLd0b6HBH3KFfWw=="
|
||||
},
|
||||
"@stremio/stremio-core-web": {
|
||||
"version": "0.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.36.0.tgz",
|
||||
"integrity": "sha512-A63yB+Pml/c6jtK4l5r7aacZ2Jm+ofaj60MfVCZfzSE4csBPMuoROYx6TM6nI6NU8iUDTcIU+OYoWcMfaT7v7A==",
|
||||
"version": "0.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.38.0.tgz",
|
||||
"integrity": "sha512-VAutpvUPzoBJ7U0IigOB6aIPVWLKeTQoAnpccsOLBEDhYUEMAcVqGirRx8Vyz1kEA+lhAQgBsDbBWONPQMONRA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "7.15.4"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"@babel/runtime": "7.16.0",
|
||||
"@sentry/browser": "6.13.3",
|
||||
"@stremio/stremio-colors": "4.0.1",
|
||||
"@stremio/stremio-core-web": "0.36.0",
|
||||
"@stremio/stremio-core-web": "0.38.0",
|
||||
"@stremio/stremio-icons": "3.0.5",
|
||||
"@stremio/stremio-video": "0.0.19-rc.1",
|
||||
"a-color-picker": "1.2.1",
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const filterInvalidDOMProps = require('filter-invalid-dom-props').default;
|
||||
const classnames = require('classnames');
|
||||
const filterInvalidDOMProps = require('filter-invalid-dom-props').default;
|
||||
const Icon = require('@stremio/stremio-icons/dom');
|
||||
const Button = require('stremio/common/Button');
|
||||
const Image = require('stremio/common/Image');
|
||||
|
|
|
|||
|
|
@ -7,6 +7,19 @@ const FocusLock = require('react-focus-lock').default;
|
|||
const { useRouteFocused } = require('stremio-router');
|
||||
const styles = require('./styles');
|
||||
|
||||
const getAnchorElement = (element) => {
|
||||
if (element === document.documentElement) {
|
||||
return element;
|
||||
}
|
||||
|
||||
const style = window.getComputedStyle(element);
|
||||
if (style.overflowY.indexOf('auto') !== -1 || style.overflowY.indexOf('scroll') !== -1) {
|
||||
return element;
|
||||
}
|
||||
|
||||
return getAnchorElement(element.parentElement);
|
||||
};
|
||||
|
||||
const Popup = ({ open, direction, renderLabel, renderMenu, dataset, onCloseRequest, ...props }) => {
|
||||
const routeFocused = useRouteFocused();
|
||||
const labelRef = React.useRef(null);
|
||||
|
|
@ -49,14 +62,16 @@ const Popup = ({ open, direction, renderLabel, renderMenu, dataset, onCloseReque
|
|||
React.useLayoutEffect(() => {
|
||||
if (open) {
|
||||
const autoDirection = [];
|
||||
const documentRect = document.documentElement.getBoundingClientRect();
|
||||
const anchor = getAnchorElement(labelRef.current);
|
||||
const anchorRect = anchor.getBoundingClientRect();
|
||||
|
||||
const labelRect = labelRef.current.getBoundingClientRect();
|
||||
const menuRect = menuRef.current.getBoundingClientRect();
|
||||
const labelPosition = {
|
||||
left: labelRect.left - documentRect.left,
|
||||
top: labelRect.top - documentRect.top,
|
||||
right: (documentRect.width + documentRect.left) - (labelRect.left + labelRect.width),
|
||||
bottom: (documentRect.height + documentRect.top) - (labelRect.top + labelRect.height)
|
||||
left: labelRect.left - anchorRect.left,
|
||||
top: labelRect.top - anchorRect.top,
|
||||
right: (anchorRect.width + anchorRect.left) - (labelRect.left + labelRect.width),
|
||||
bottom: (anchorRect.height + anchorRect.top) - (labelRect.top + labelRect.height)
|
||||
};
|
||||
|
||||
if (menuRect.height <= labelPosition.bottom) {
|
||||
|
|
@ -87,7 +102,7 @@ const Popup = ({ open, direction, renderLabel, renderMenu, dataset, onCloseReque
|
|||
return renderLabel({
|
||||
...props,
|
||||
ref: labelRef,
|
||||
className: styles['label-container'],
|
||||
className: classnames(styles['label-container'], props.className, { 'active': open }),
|
||||
children: open ?
|
||||
<FocusLock ref={menuRef} className={classnames(styles['menu-container'], styles[`menu-direction-${autoDirection}`], styles[`menu-direction-${direction}`])} autoFocus={false} lockProps={{ onMouseDown: menuOnMouseDown }}>
|
||||
{renderMenu()}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,37 @@
|
|||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { useRouteFocused } = require('stremio-router');
|
||||
const Icon = require('@stremio/stremio-icons/dom');
|
||||
const { Button, Image } = require('stremio/common');
|
||||
const { Button, Image, Popup, useBinaryState } = require('stremio/common');
|
||||
const VideoPlaceholder = require('./VideoPlaceholder');
|
||||
const styles = require('./styles');
|
||||
|
||||
const Video = ({ className, id, title, thumbnail, episode, released, upcoming, watched, progress, scheduled, deepLinks, ...props }) => {
|
||||
const { core } = useServices();
|
||||
const routeFocused = useRouteFocused();
|
||||
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
|
||||
const popupLabelOnContextMenu = React.useCallback((event) => {
|
||||
if (!event.nativeEvent.togglePopupPrevented && !event.nativeEvent.ctrlKey) {
|
||||
toggleMenu();
|
||||
event.nativeEvent.preventDefault();
|
||||
}
|
||||
}, [toggleMenu]);
|
||||
const popupMenuOnContextMenu = React.useCallback((event) => {
|
||||
event.nativeEvent.togglePopupPrevented = true;
|
||||
}, []);
|
||||
const toggleWatchedOnClick = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
closeMenu();
|
||||
core.transport.dispatch({
|
||||
action: 'MetaDetails',
|
||||
args: {
|
||||
action: 'MarkAsWatched',
|
||||
args: [id, !watched]
|
||||
}
|
||||
});
|
||||
}, [id, watched]);
|
||||
const href = React.useMemo(() => {
|
||||
return deepLinks ?
|
||||
typeof deepLinks.player === 'string' ?
|
||||
|
|
@ -21,74 +46,115 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w
|
|||
:
|
||||
null;
|
||||
}, [deepLinks]);
|
||||
return (
|
||||
<Button href={href} {...props} className={classnames(className, styles['video-container'])} title={title}>
|
||||
{
|
||||
typeof thumbnail === 'string' && thumbnail.length > 0 ?
|
||||
<div className={styles['thumbnail-container']}>
|
||||
<Image
|
||||
className={styles['thumbnail']}
|
||||
src={thumbnail}
|
||||
alt={' '}
|
||||
renderFallback={() => (
|
||||
<Icon
|
||||
className={styles['placeholder-icon']}
|
||||
icon={'ic_broken_link'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
const renderLabel = React.useMemo(() => function renderLabel({ className, id, title, thumbnail, episode, released, upcoming, watched, progress, scheduled, children, ...props }) {
|
||||
return (
|
||||
<Button {...props} className={classnames(className, styles['video-container'])} title={title}>
|
||||
{
|
||||
typeof thumbnail === 'string' && thumbnail.length > 0 ?
|
||||
<div className={styles['thumbnail-container']}>
|
||||
<Image
|
||||
className={styles['thumbnail']}
|
||||
src={thumbnail}
|
||||
alt={' '}
|
||||
renderFallback={() => (
|
||||
<Icon
|
||||
className={styles['placeholder-icon']}
|
||||
icon={'ic_broken_link'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles['info-container']}>
|
||||
<div className={styles['title-container']}>
|
||||
{episode !== null && !isNaN(episode) ? `${episode}. ` : null}
|
||||
{typeof title === 'string' && title.length > 0 ? title : id}
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles['info-container']}>
|
||||
<div className={styles['title-container']}>
|
||||
{episode !== null && !isNaN(episode) ? `${episode}. ` : null}
|
||||
{typeof title === 'string' && title.length > 0 ? title : id}
|
||||
</div>
|
||||
<div className={styles['flex-row-container']}>
|
||||
{
|
||||
released instanceof Date && !isNaN(released.getTime()) ?
|
||||
<div className={styles['released-container']}>
|
||||
{released.toLocaleString(undefined, { year: '2-digit', month: 'short', day: 'numeric' })}
|
||||
</div>
|
||||
:
|
||||
scheduled ?
|
||||
<div className={styles['released-container']} title={'To be announced'}>
|
||||
TBA
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles['upcoming-watched-container']}>
|
||||
<div className={styles['flex-row-container']}>
|
||||
{
|
||||
upcoming ?
|
||||
<div className={styles['upcoming-container']}>
|
||||
<div className={styles['flag-label']}>Upcoming</div>
|
||||
released instanceof Date && !isNaN(released.getTime()) ?
|
||||
<div className={styles['released-container']}>
|
||||
{released.toLocaleString(undefined, { year: '2-digit', month: 'short', day: 'numeric' })}
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
watched ?
|
||||
<div className={styles['watched-container']}>
|
||||
<div className={styles['flag-label']}>Watched</div>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
scheduled ?
|
||||
<div className={styles['released-container']} title={'To be announced'}>
|
||||
TBA
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles['upcoming-watched-container']}>
|
||||
{
|
||||
upcoming ?
|
||||
<div className={styles['upcoming-container']}>
|
||||
<div className={styles['flag-label']}>Upcoming</div>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
watched ?
|
||||
<div className={styles['watched-container']}>
|
||||
<div className={styles['flag-label']}>Watched</div>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
progress !== null && !isNaN(progress) && progress > 0 ?
|
||||
<div className={styles['progress-bar-container']}>
|
||||
<div className={styles['progress-bar']} style={{ width: `${Math.min(progress, 1) * 100}%` }} />
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}, []);
|
||||
const renderMenu = React.useMemo(() => function renderMenu() {
|
||||
return (
|
||||
<div className={styles['context-menu-content']} onContextMenu={popupMenuOnContextMenu}>
|
||||
<Button className={styles['context-menu-option-container']} title={'Watch'}>
|
||||
<div className={styles['context-menu-option-label']}>Watch</div>
|
||||
</Button>
|
||||
<Button className={styles['context-menu-option-container']} title={watched ? 'Mark as non-watched' : 'Mark as watched'} onClick={toggleWatchedOnClick}>
|
||||
<div className={styles['context-menu-option-label']}>{watched ? 'Mark as non-watched' : 'Mark as watched'}</div>
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
progress !== null && !isNaN(progress) && progress > 0 ?
|
||||
<div className={styles['progress-bar-container']}>
|
||||
<div className={styles['progress-bar']} style={{ width: `${Math.min(progress, 1) * 100}%` }} />
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</Button>
|
||||
);
|
||||
}, [watched, toggleWatchedOnClick]);
|
||||
React.useEffect(() => {
|
||||
if (!routeFocused) {
|
||||
closeMenu();
|
||||
}
|
||||
}, [routeFocused]);
|
||||
return (
|
||||
<Popup
|
||||
className={className}
|
||||
id={id}
|
||||
title={title}
|
||||
thumbnail={thumbnail}
|
||||
episode={episode}
|
||||
released={released}
|
||||
upcoming={upcoming}
|
||||
watched={watched}
|
||||
progress={progress}
|
||||
scheduled={scheduled}
|
||||
href={href}
|
||||
{...props}
|
||||
onContextMenu={popupLabelOnContextMenu}
|
||||
open={menuOpen}
|
||||
onCloseRequest={closeMenu}
|
||||
renderLabel={renderLabel}
|
||||
renderMenu={renderMenu}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,21 @@
|
|||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
@import (reference) '~stremio/common/screen-sizes.less';
|
||||
|
||||
:import('~stremio/common/Popup/styles.less') {
|
||||
context-menu-container: menu-container;
|
||||
menu-direction-top-left: menu-direction-top-left;
|
||||
menu-direction-bottom-left: menu-direction-bottom-left;
|
||||
menu-direction-top-right: menu-direction-top-right;
|
||||
menu-direction-bottom-right: menu-direction-bottom-right;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
&:hover, &:focus {
|
||||
&:hover, &:focus, &:global(.active) {
|
||||
background-color: @color-background;
|
||||
}
|
||||
|
||||
|
|
@ -124,8 +132,49 @@
|
|||
|
||||
@media only screen and (max-width: @minimum) {
|
||||
.video-container {
|
||||
&:hover, &:focus {
|
||||
&:hover, &:focus, &:global(.active) {
|
||||
background-color: @color-surface-light5-20;
|
||||
}
|
||||
|
||||
.context-menu-container {
|
||||
max-width: calc(90% - 1.5rem);
|
||||
|
||||
&.menu-direction-top-left, &.menu-direction-bottom-left {
|
||||
right: 1.5rem;
|
||||
}
|
||||
|
||||
&.menu-direction-top-right, &.menu-direction-bottom-right {
|
||||
left: 1.5rem;
|
||||
}
|
||||
|
||||
&.menu-direction-top-left, &.menu-direction-top-right {
|
||||
bottom: 90%;
|
||||
}
|
||||
|
||||
&.menu-direction-bottom-left, &.menu-direction-bottom-right {
|
||||
top: 90%;
|
||||
}
|
||||
|
||||
.context-menu-content {
|
||||
background-color: @color-background-dark1;
|
||||
|
||||
.context-menu-option-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: @color-background;
|
||||
}
|
||||
|
||||
.context-menu-option-label {
|
||||
font-size: 1rem;
|
||||
max-height: 2.4em;
|
||||
color: @color-surface-light5-90;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ const useSearch = (queryParams) => {
|
|||
const state = core.transport.getState('search');
|
||||
if (state.selected !== null) {
|
||||
const [, query] = state.selected.extra.find(([name]) => name === 'search');
|
||||
const responses = state.catalogs.filter((catalog) => catalog.content.type === 'Ready');
|
||||
const responses = state.catalogs.filter((catalog) => catalog.content?.type === 'Ready');
|
||||
core.transport.analytics({
|
||||
event: 'Search',
|
||||
args: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue