Merge branch 'development' of https://github.com/Stremio/stremio-web into refactor/player-audio-menu

This commit is contained in:
Tim 2024-12-24 15:04:15 +01:00
commit 2eec7ed844
30 changed files with 579 additions and 233 deletions

74
package-lock.json generated
View file

@ -1,20 +1,20 @@
{
"name": "stremio",
"version": "5.0.0-beta.14",
"version": "5.0.0-beta.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "stremio",
"version": "5.0.0-beta.14",
"version": "5.0.0-beta.15",
"license": "gpl-2.0",
"dependencies": {
"@babel/runtime": "7.26.0",
"@sentry/browser": "8.42.0",
"@stremio/stremio-colors": "5.2.0",
"@stremio/stremio-core-web": "0.48.2",
"@stremio/stremio-icons": "5.4.0",
"@stremio/stremio-video": "0.0.46",
"@stremio/stremio-core-web": "0.48.3",
"@stremio/stremio-icons": "5.4.1",
"@stremio/stremio-video": "0.0.48",
"a-color-picker": "1.2.1",
"bowser": "2.11.0",
"buffer": "6.0.3",
@ -67,6 +67,7 @@
"postcss-loader": "8.1.1",
"readdirp": "4.0.2",
"terser-webpack-plugin": "5.3.10",
"thread-loader": "^4.0.4",
"ts-loader": "^9.5.1",
"typescript": "^5.7.2",
"typescript-eslint": "^8.17.0",
@ -1869,7 +1870,6 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@ -1880,8 +1880,7 @@
"node_modules/@babel/runtime/node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/@babel/template": {
"version": "7.25.9",
@ -3261,7 +3260,6 @@
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.42.0.tgz",
"integrity": "sha512-xzgRI0wglKYsPrna574w1t38aftuvo44gjOKFvPNGPnYfiW9y4m+64kUz3JFbtanvOrKPcaITpdYiB4DeJXEbA==",
"license": "MIT",
"dependencies": {
"@sentry/core": "8.42.0"
},
@ -3273,7 +3271,6 @@
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.42.0.tgz",
"integrity": "sha512-dkIw5Wdukwzngg5gNJ0QcK48LyJaMAnBspqTqZ3ItR01STi6Z+6+/Bt5XgmrvDgRD+FNBinflc5zMmfdFXXhvw==",
"license": "MIT",
"dependencies": {
"@sentry/core": "8.42.0"
},
@ -3285,7 +3282,6 @@
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.42.0.tgz",
"integrity": "sha512-oNcJEBlDfXnRFYC5Mxj5fairyZHNqlnU4g8kPuztB9G5zlsyLgWfPxzcn1ixVQunth2/WZRklDi4o1ZfyHww7w==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "8.42.0",
"@sentry/core": "8.42.0"
@ -3298,7 +3294,6 @@
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.42.0.tgz",
"integrity": "sha512-XrPErqVhPsPh/oFLVKvz7Wb+Fi2J1zCPLeZCxWqFuPWI2agRyLVu0KvqJyzSpSrRAEJC/XFzuSVILlYlXXSfgA==",
"license": "MIT",
"dependencies": {
"@sentry-internal/replay": "8.42.0",
"@sentry/core": "8.42.0"
@ -3311,7 +3306,6 @@
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.42.0.tgz",
"integrity": "sha512-lStrEk609KJHwXfDrOgoYVVoFFExixHywxSExk7ZDtwj2YPv6r6Y1gogvgr7dAZj7jWzadHkxZ33l9EOSJBfug==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "8.42.0",
"@sentry-internal/feedback": "8.42.0",
@ -3327,7 +3321,6 @@
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.42.0.tgz",
"integrity": "sha512-ac6O3pgoIbU6rpwz6LlwW0wp3/GAHuSI0C5IsTgIY6baN8rOBnlAtG6KrHDDkGmUQ2srxkDJu9n1O6Td3cBCqw==",
"license": "MIT",
"engines": {
"node": ">=14.18"
}
@ -3375,14 +3368,12 @@
"node_modules/@stremio/stremio-colors": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@stremio/stremio-colors/-/stremio-colors-5.2.0.tgz",
"integrity": "sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg==",
"license": "MIT"
"integrity": "sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg=="
},
"node_modules/@stremio/stremio-core-web": {
"version": "0.48.2",
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.48.2.tgz",
"integrity": "sha512-zkz4ftGMoK9RmIDlGLd/DYLtaXuf4HxnMEN1NduKNXDlYPSJ4Q/b1hCbXrVqVK/nx6s+8X4XyYr9IOwFLaT5ew==",
"license": "MIT",
"version": "0.48.3",
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.48.3.tgz",
"integrity": "sha512-JL8pOLOEVACYG+33Dtp/mrB2/vuc7RoYZdxX1BQa5MPR8EzsODjpvL5uETmdxo/swgtMZyx2A6/e1B53eKA4oQ==",
"dependencies": {
"@babel/runtime": "7.24.1"
}
@ -3406,9 +3397,9 @@
"license": "MIT"
},
"node_modules/@stremio/stremio-icons": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@stremio/stremio-icons/-/stremio-icons-5.4.0.tgz",
"integrity": "sha512-rRWNER+wLgMjxd6sKT0MMq4lzXDOobY3GNdT3NDeeymBtB/CD0YmYqQuUOyYDjEZ1btIbNaniUOBoPW9d3ZQ8A==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/@stremio/stremio-icons/-/stremio-icons-5.4.1.tgz",
"integrity": "sha512-7g4JP7tPRT1UDZxbuH/Urq7fc6te3joy8qyx/NGWIW7wO169TTISO7ZWdejzESvUVgZ/7i6rzkRmXZ3wefWcBg==",
"workspaces": [
"react",
"react-native",
@ -3417,9 +3408,10 @@
]
},
"node_modules/@stremio/stremio-video": {
"version": "0.0.46",
"resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.46.tgz",
"integrity": "sha512-U15CGB6CrUZKq3IKcEouAEH2RQoLy2+BI/hDStEYEACxlRlFaavKPI2opl37muh9TY089RnZVBYAM3yDidBZdg==",
"version": "0.0.48",
"resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.48.tgz",
"integrity": "sha512-6ALGXCZC4NPsfhPcrwFWQzvH6UMMRsgSkHetnOhv9WmZ5ubiyUdbBzj9atGiGuuQz8pRcze66ztrub+dsaQbpw==",
"license": "MIT",
"dependencies": {
"buffer": "6.0.3",
"color": "4.2.3",
@ -10162,6 +10154,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
"dev": true,
"license": "MIT"
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@ -13940,6 +13939,29 @@
"node": ">=0.2.6"
}
},
"node_modules/thread-loader": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/thread-loader/-/thread-loader-4.0.4.tgz",
"integrity": "sha512-tXagu6Hivd03wB2tiS1bqvw345sc7mKei32EgpYpq31ZLes9FN0mEK2nKzXLRFgwt3PsBB0E/MZDp159rDoqwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"json-parse-better-errors": "^1.0.2",
"loader-runner": "^4.1.0",
"neo-async": "^2.6.2",
"schema-utils": "^4.2.0"
},
"engines": {
"node": ">= 16.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"webpack": "^5.0.0"
}
},
"node_modules/thunky": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",

9
package.json Executable file → Normal file
View file

@ -1,7 +1,7 @@
{
"name": "stremio",
"displayName": "Stremio",
"version": "5.0.0-beta.14",
"version": "5.0.0-beta.15",
"author": "Smart Code OOD",
"private": true,
"license": "gpl-2.0",
@ -16,9 +16,9 @@
"@babel/runtime": "7.26.0",
"@sentry/browser": "8.42.0",
"@stremio/stremio-colors": "5.2.0",
"@stremio/stremio-core-web": "0.48.2",
"@stremio/stremio-icons": "5.4.0",
"@stremio/stremio-video": "0.0.46",
"@stremio/stremio-core-web": "0.48.3",
"@stremio/stremio-icons": "5.4.1",
"@stremio/stremio-video": "0.0.48",
"a-color-picker": "1.2.1",
"bowser": "2.11.0",
"buffer": "6.0.3",
@ -71,6 +71,7 @@
"postcss-loader": "8.1.1",
"readdirp": "4.0.2",
"terser-webpack-plugin": "5.3.10",
"thread-loader": "^4.0.4",
"ts-loader": "^9.5.1",
"typescript": "^5.7.2",
"typescript-eslint": "^8.17.0",

View file

@ -102,7 +102,7 @@
}
}
@media (orientation: landscape) and (max-height: @xsmall) {
@media (orientation: landscape) and (max-height: @minimum) {
.event-modal {
.modal-dialog-container {
.modal-dialog-content {

View file

@ -166,6 +166,7 @@
object-position: center;
object-fit: cover;
opacity: 0.9;
overflow-clip-margin: unset;
}
.placeholder-icon {

View file

@ -0,0 +1,60 @@
import { cloneElement, useCallback, useEffect, useMemo, useState } from 'react';
import classNames from 'classnames';
type Props = {
children: JSX.Element,
when: boolean,
name: string,
};
const Transition = ({ children, when, name }: Props) => {
const [element, setElement] = useState<HTMLElement | null>(null);
const [mounted, setMounted] = useState(false);
const [state, setState] = useState('enter');
const [active, setActive] = useState(false);
const callbackRef = useCallback((element: HTMLElement | null) => {
setElement(element);
}, []);
const className = useMemo(() => {
const animationClass = `${name}-${state}`;
const activeClass = active ? `${name}-active` : null;
return children && classNames(
children.props.className,
animationClass,
activeClass,
);
}, [name, state, active, children]);
const onTransitionEnd = useCallback(() => {
state === 'exit' && setMounted(false);
}, [state]);
useEffect(() => {
setState(when ? 'enter' : 'exit');
when && setMounted(true);
}, [when]);
useEffect(() => {
requestAnimationFrame(() => {
setActive(!!element);
});
}, [element]);
useEffect(() => {
element?.addEventListener('transitionend', onTransitionEnd);
return () => element?.removeEventListener('transitionend', onTransitionEnd);
}, [element, onTransitionEnd]);
return (
mounted && cloneElement(children, {
ref: callbackRef,
className,
})
);
};
export default Transition;

View file

@ -0,0 +1,2 @@
import Transition from './Transition';
export default Transition;

View file

@ -38,4 +38,17 @@
100% {
transform: translateY(0%);
}
}
.slide-left-enter {
transform: translateX(100%);
}
.slide-left-active {
transform: translateX(0%);
transition: transform 0.3s cubic-bezier(0.32, 0, 0.67, 0);
}
.slide-left-exit {
transform: translateX(100%);
}

View file

@ -29,6 +29,7 @@ const Slider = require('./Slider');
const { default: TextInput } = require('./TextInput');
const { ToastProvider, useToast } = require('./Toast');
const { TooltipProvider, Tooltip } = require('./Tooltips');
const { default: Transition } = require('./Transition');
const Video = require('./Video');
const comparatorWithPriorities = require('./comparatorWithPriorities');
const CONSTANTS = require('./CONSTANTS');
@ -84,6 +85,7 @@ module.exports = {
useToast,
TooltipProvider,
Tooltip,
Transition,
Video,
comparatorWithPriorities,
CONSTANTS,

View file

@ -9,16 +9,20 @@ const { Button, Image, Multiselect } = require('stremio/common');
const { useServices } = require('stremio/services');
const Stream = require('./Stream');
const styles = require('./styles');
const { usePlatform } = require('stremio/common');
const ALL_ADDONS_KEY = 'ALL';
const StreamsList = ({ className, video, ...props }) => {
const { t } = useTranslation();
const { core } = useServices();
const platform = usePlatform();
const streamsContainerRef = React.useRef(null);
const [selectedAddon, setSelectedAddon] = React.useState(ALL_ADDONS_KEY);
const onAddonSelected = React.useCallback((event) => {
streamsContainerRef.current.scrollTo({ top: 0, left: 0, behavior: platform.name === 'ios' ? 'smooth' : 'instant' });
setSelectedAddon(event.value);
}, []);
}, [platform]);
const backButtonOnClick = React.useCallback(() => {
if (video.deepLinks && typeof video.deepLinks.metaDetailsVideos === 'string') {
window.location.replace(video.deepLinks.metaDetailsVideos + (
@ -142,7 +146,7 @@ const StreamsList = ({ className, video, ...props }) => {
:
null
}
<div className={styles['streams-container']}>
<div className={styles['streams-container']} ref={streamsContainerRef}>
{filteredStreams.map((stream, index) => (
<Stream
key={index}

View file

@ -36,10 +36,6 @@
background-color: var(--overlay-color);
}
&:focus {
background-color: var(--primary-foreground-color);
}
&>:first-child {
margin-right: 0.5rem;
}

View file

@ -36,9 +36,8 @@ const ControlBar = ({
onSeekRequested,
onToggleSubtitlesMenu,
onToggleAudioMenu,
onToggleInfoMenu,
onToggleSpeedMenu,
onToggleVideosMenu,
onToggleSideDrawer,
onToggleOptionsMenu,
onToggleStatisticsMenu,
...props
@ -52,9 +51,6 @@ const ControlBar = ({
const onAudioButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.audioMenuClosePrevented = true;
}, []);
const onInfoButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.infoMenuClosePrevented = true;
}, []);
const onSpeedButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.speedMenuClosePrevented = true;
}, []);
@ -155,9 +151,6 @@ const ControlBar = ({
<Button className={classnames(styles['control-bar-button'], { 'disabled': playbackSpeed === null })} tabIndex={-1} onMouseDown={onSpeedButtonMouseDown} onClick={onToggleSpeedMenu}>
<Icon className={styles['icon']} name={'speed'} />
</Button>
<Button className={classnames(styles['control-bar-button'], { 'disabled': metaItem === null || metaItem.type !== 'Ready' })} tabIndex={-1} onMouseDown={onInfoButtonMouseDown} onClick={onToggleInfoMenu}>
<Icon className={styles['icon']} name={'about'} />
</Button>
<Button className={classnames(styles['control-bar-button'], { 'disabled': !chromecastServiceActive })} tabIndex={-1} onClick={onChromecastButtonClick}>
<Icon className={styles['icon']} name={'cast'} />
</Button>
@ -169,7 +162,7 @@ const ControlBar = ({
</Button>
{
metaItem?.content?.videos?.length > 0 ?
<Button className={styles['control-bar-button']} tabIndex={-1} onMouseDown={onVideosButtonMouseDown} onClick={onToggleVideosMenu}>
<Button className={styles['control-bar-button']} tabIndex={-1} onMouseDown={onVideosButtonMouseDown} onClick={onToggleSideDrawer}>
<Icon className={styles['icon']} name={'episodes'} />
</Button>
:
@ -208,9 +201,8 @@ ControlBar.propTypes = {
onSeekRequested: PropTypes.func,
onToggleSubtitlesMenu: PropTypes.func,
onToggleAudioMenu: PropTypes.func,
onToggleInfoMenu: PropTypes.func,
onToggleSpeedMenu: PropTypes.func,
onToggleVideosMenu: PropTypes.func,
onToggleSideDrawer: PropTypes.func,
onToggleOptionsMenu: PropTypes.func,
onToggleStatisticsMenu: PropTypes.func,
};

View file

@ -100,14 +100,16 @@
.control-bar-buttons-menu-container {
position: absolute;
right: 0.15rem;
right: 0rem;
bottom: 4.5rem;
flex-direction: column;
padding: 0.5rem;
margin: 0.5rem;
max-width: calc(100dvw - 1rem);
border-radius: var(--border-radius);
background-color: var(--modal-background-color);
box-shadow: 0 1.35rem 2.7rem @color-background-dark5-40,
0 1.1rem 0.85rem @color-background-dark5-20;
overflow-x: auto;
&:not(:global(.open)) {
display: none;

View file

@ -1,77 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
// const Stream = require('stremio/routes/MetaDetails/StreamsList/Stream');
// const AddonDetails = require('stremio/common/AddonDetailsModal/AddonDetails');
const { MetaPreview, CONSTANTS } = require('stremio/common');
const styles = require('./styles');
const InfoMenu = ({ className, ...props }) => {
const metaItem = React.useMemo(() => {
return props.metaItem !== null ?
{
...props.metaItem,
links: props.metaItem.links.filter(({ category }) => category === CONSTANTS.SHARE_LINK_CATEGORY)
}
:
null;
}, [props.metaItem]);
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.infoMenuClosePrevented = true;
}, []);
return (
<div className={classnames(className, styles['info-menu-container'])} onMouseDown={onMouseDown}>
{
metaItem !== null ?
<MetaPreview
className={styles['meta-preview']}
compact={true}
name={metaItem.name}
logo={metaItem.logo}
runtime={metaItem.runtime}
releaseInfo={metaItem.releaseInfo}
released={metaItem.released}
description={metaItem.description}
links={metaItem.links}
/>
:
null
}
{/* {
props.stream !== null ?
<Stream
{...props.stream}
className={classnames(styles['stream'], 'active')}
addonName={props.addon !== null ? props.addon.manifest.name : ''}
/>
:
null
} */}
{/* {
props.addon !== null ?
<AddonDetails
id={props.addon.manifest.id}
name={props.addon.manifest.name}
version={props.addon.manifest.version}
logo={props.addon.manifest.logo}
description={props.addon.manifest.description}
types={props.addon.manifest.types}
transportUrl={props.addon.transportUrl}
/>
:
null
} */}
</div>
);
};
InfoMenu.propTypes = {
className: PropTypes.string,
metaItem: PropTypes.object,
addon: PropTypes.object,
stream: PropTypes.object
};
module.exports = InfoMenu;

View file

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

View file

@ -1,10 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
.info-menu-container {
width: 30rem;
padding: 2rem;
.stream {
pointer-events: none;
}
}

View file

@ -8,19 +8,19 @@ const langs = require('langs');
const { useTranslation } = require('react-i18next');
const { useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services');
const { HorizontalNavBar, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common');
const { HorizontalNavBar, Transition, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common');
const BufferingLoader = require('./BufferingLoader');
const VolumeChangeIndicator = require('./VolumeChangeIndicator');
const Error = require('./Error');
const ControlBar = require('./ControlBar');
const NextVideoPopup = require('./NextVideoPopup');
const StatisticsMenu = require('./StatisticsMenu');
const InfoMenu = require('./InfoMenu');
const OptionsMenu = require('./OptionsMenu');
const VideosMenu = require('./VideosMenu');
const SubtitlesMenu = require('./SubtitlesMenu');
const { default: AudioMenu } = require('./AudioMenu');
const SpeedMenu = require('./SpeedMenu');
const { default: SideDrawerButton } = require('./SideDrawerButton');
const { default: SideDrawer } = require('./SideDrawer');
const usePlayer = require('./usePlayer');
const useSettings = require('./useSettings');
const useStatistics = require('./useStatistics');
@ -56,24 +56,22 @@ const Player = ({ urlParams, queryParams }) => {
const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false);
const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
const [audioMenuOpen, , closeAudioMenu, toggleAudioMenu] = useBinaryState(false);
const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false);
const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] = useBinaryState(false);
const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
const [sideDrawerOpen, , closeSideDrawer, toggleSideDrawer] = useBinaryState(false);
const menusOpen = React.useMemo(() => {
return optionsMenuOpen || subtitlesMenuOpen || audioMenuOpen || infoMenuOpen || speedMenuOpen || videosMenuOpen || statisticsMenuOpen;
}, [optionsMenuOpen, subtitlesMenuOpen, audioMenuOpen, infoMenuOpen, speedMenuOpen, videosMenuOpen, statisticsMenuOpen]);
return optionsMenuOpen || subtitlesMenuOpen || audioMenuOpen || speedMenuOpen || statisticsMenuOpen || sideDrawerOpen;
}, [optionsMenuOpen, subtitlesMenuOpen, audioMenuOpen, speedMenuOpen, statisticsMenuOpen, sideDrawerOpen]);
const closeMenus = React.useCallback(() => {
closeOptionsMenu();
closeSubtitlesMenu();
closeAudioMenu();
closeInfoMenu();
closeSpeedMenu();
closeVideosMenu();
closeStatisticsMenu();
closeSideDrawer();
}, []);
const overlayHidden = React.useMemo(() => {
@ -243,18 +241,14 @@ const Player = ({ urlParams, queryParams }) => {
if (!event.nativeEvent.audioMenuClosePrevented) {
closeAudioMenu();
}
if (!event.nativeEvent.infoMenuClosePrevented) {
closeInfoMenu();
}
if (!event.nativeEvent.speedMenuClosePrevented) {
closeSpeedMenu();
}
if (!event.nativeEvent.videosMenuClosePrevented) {
closeVideosMenu();
}
if (!event.nativeEvent.statisticsMenuClosePrevented) {
closeStatisticsMenu();
}
closeSideDrawer();
}, []);
const onContainerMouseMove = React.useCallback((event) => {
@ -422,13 +416,6 @@ const Player = ({ urlParams, queryParams }) => {
}
}, [video.state.audioTracks]);
React.useEffect(() => {
if (player.metaItem === null || player.metaItem.type !== 'Ready') {
closeInfoMenu();
closeVideosMenu();
}
}, [player.metaItem]);
React.useEffect(() => {
if (video.state.playbackSpeed === null) {
closeSpeedMenu();
@ -538,7 +525,7 @@ const Player = ({ urlParams, queryParams }) => {
case 'KeyI': {
closeMenus();
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
toggleInfoMenu();
toggleSideDrawer();
}
break;
@ -551,14 +538,6 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'KeyV': {
closeMenus();
if (player.metaItem !== null && player.metaItem.type === 'Ready' && player.metaItem?.content?.videos?.length > 0) {
toggleVideosMenu();
}
break;
}
case 'KeyD': {
closeMenus();
if (streamingServer.statistics !== null && streamingServer.statistics.type !== 'Err' && player.selected && typeof player.selected.stream.infoHash === 'string' && typeof player.selected.stream.fileIdx === 'number') {
@ -599,7 +578,7 @@ const Player = ({ urlParams, queryParams }) => {
window.removeEventListener('keyup', onKeyUp);
window.removeEventListener('wheel', onWheel);
};
}, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, settings.seekShortTimeDuration, routeFocused, menusOpen, nextVideoPopupOpen, video.state.paused, video.state.time, video.state.volume, video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, video.state.playbackSpeed, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu, toggleStatisticsMenu]);
}, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, settings.seekShortTimeDuration, routeFocused, menusOpen, nextVideoPopupOpen, video.state.paused, video.state.time, video.state.volume, video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, video.state.playbackSpeed, toggleSubtitlesMenu, toggleStatisticsMenu, toggleSideDrawer]);
React.useEffect(() => {
video.events.on('error', onError);
@ -684,6 +663,15 @@ const Player = ({ urlParams, queryParams }) => {
onMouseMove={onBarMouseMove}
onMouseOver={onBarMouseMove}
/>
{
player.metaItem?.type === 'Ready' ?
<SideDrawerButton
className={classnames(styles['layer'], styles['side-drawer-button-layer'])}
onClick={toggleSideDrawer}
/>
:
null
}
<ControlBar
className={classnames(styles['layer'], styles['control-bar-layer'])}
paused={video.state.paused}
@ -709,10 +697,9 @@ const Player = ({ urlParams, queryParams }) => {
onToggleOptionsMenu={toggleOptionsMenu}
onToggleSubtitlesMenu={toggleSubtitlesMenu}
onToggleAudioMenu={toggleAudioMenu}
onToggleInfoMenu={toggleInfoMenu}
onToggleSpeedMenu={toggleSpeedMenu}
onToggleVideosMenu={toggleVideosMenu}
onToggleStatisticsMenu={toggleStatisticsMenu}
onToggleSideDrawer={toggleSideDrawer}
onMouseMove={onBarMouseMove}
onMouseOver={onBarMouseMove}
/>
@ -737,6 +724,14 @@ const Player = ({ urlParams, queryParams }) => {
:
null
}
<Transition when={sideDrawerOpen} name={'slide-left'}>
<SideDrawer
className={classnames(styles['layer'], styles['side-drawer-layer'])}
metaItem={player.metaItem?.content}
seriesInfo={player.seriesInfo}
closeSideDrawer={closeSideDrawer}
/>
</Transition>
{
subtitlesMenuOpen ?
<SubtitlesMenu
@ -772,17 +767,6 @@ const Player = ({ urlParams, queryParams }) => {
:
null
}
{
infoMenuOpen ?
<InfoMenu
className={classnames(styles['layer'], styles['menu-layer'])}
stream={player.selected !== null ? player.selected.stream : null}
addon={player.addon}
metaItem={player.metaItem !== null && player.metaItem.type === 'Ready' ? player.metaItem.content : null}
/>
:
null
}
{
speedMenuOpen ?
<SpeedMenu
@ -793,16 +777,6 @@ const Player = ({ urlParams, queryParams }) => {
:
null
}
{
videosMenuOpen ?
<VideosMenu
className={classnames(styles['layer'], styles['menu-layer'])}
metaItem={player.metaItem !== null && player.metaItem.type === 'Ready' ? player.metaItem.content : null}
seriesInfo={player.seriesInfo}
/>
:
null
}
{
optionsMenuOpen ?
<OptionsMenu

View file

@ -0,0 +1,115 @@
// Copyright (C) 2017-2024 Smart code 203358507
@import (reference) '~stremio/common/screen-sizes.less';
:import('~stremio/common/MetaPreview/styles.less') {
action-buttons-container: action-buttons-container;
}
@padding: 1rem;
.side-drawer {
display: flex;
flex-direction: column;
padding: @padding;
height: 100dvh;
max-width: 35rem;
overflow-y: auto;
position: relative;
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
background-color: var(--modal-background-color);
box-shadow: 0 1.35rem 2.7rem var(--color-background-dark5-40), 0 1.1rem 0.85rem var(--color-background-dark5-20);
backdrop-filter: blur(15px);
transition: transform 0.3s ease-in-out;
z-index: 1;
.close-button {
display: none;
position: absolute;
top: 1.3rem;
right: 1.3rem;
padding: 0.5rem;
background-color: transparent;
cursor: pointer;
z-index: 2;
border-radius: var(--border-radius);
transition: 0.3s all ease-in-out;
-webkit-tap-highlight-color: transparent;
.icon {
color: var(--primary-foreground-color);
width: 2rem;
height: 2rem;
opacity: 0.6;
transition: 0.3s opacity ease-in-out;
}
&:hover {
background-color: var(--overlay-color);
.icon {
opacity: 1;
}
}
}
.info {
padding: @padding;
overflow-y: auto;
flex: none;
.side-drawer-meta-preview {
.action-buttons-container {
padding-top: 0;
margin-top: 0;
}
}
}
.series-content {
flex: 2;
display: flex;
flex-direction: column;
.videos {
overflow-y: auto;
}
}
}
@media screen and (max-width: @small) {
.side-drawer {
max-width: 40dvw;
}
}
@media (orientation: portrait) and (max-width: @xsmall) {
.side-drawer {
max-width: 100dvw;
.close-button {
display: block;
}
}
}
@media (orientation: landscape) and (max-width: @xsmall) {
.side-drawer {
max-width: 50dvw;
.info {
max-height: 30dvh;
}
}
}
@media screen and (max-width: @xxsmall) {
.side-drawer {
padding: calc(@padding / 2);
.info {
padding: calc(@padding / 2);
}
}
}

View file

@ -0,0 +1,118 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { useMemo, useCallback, useState, forwardRef, memo } from 'react';
import classNames from 'classnames';
import Icon from '@stremio/stremio-icons/react';
import { useServices } from 'stremio/services';
import { CONSTANTS } from 'stremio/common';
import MetaPreview from 'stremio/common/MetaPreview/MetaPreview';
import Video from 'stremio/common/Video/Video';
import SeasonsBar from 'stremio/routes/MetaDetails/VideosList/SeasonsBar';
import styles from './SideDrawer.less';
type Props = {
className?: string;
seriesInfo: SeriesInfo;
metaItem: MetaItem;
closeSideDrawer: () => void;
};
const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, className, closeSideDrawer, ...props }: Props, ref) => {
const { core } = useServices();
const [season, setSeason] = useState<number>(seriesInfo?.season);
const metaItem = useMemo(() => {
return seriesInfo ?
{
...props.metaItem,
links: props.metaItem.links.filter(({ category }) => category === CONSTANTS.SHARE_LINK_CATEGORY)
}
:
props.metaItem;
}, [props.metaItem]);
const videos = useMemo(() => {
return Array.isArray(metaItem.videos) ?
metaItem.videos.filter((video) => video.season === season)
:
metaItem.videos;
}, [metaItem, season]);
const seasons = useMemo(() => {
return props.metaItem.videos
.map(({ season }) => season)
.filter((season, index, seasons) => {
return seasons.indexOf(season) === index;
})
.sort((a, b) => (a || Number.MAX_SAFE_INTEGER) - (b || Number.MAX_SAFE_INTEGER));
}, [props.metaItem.videos]);
const seasonOnSelect = useCallback((event: { value: string }) => {
setSeason(parseInt(event.value));
}, []);
const onMarkVideoAsWatched = useCallback((video: Video, watched: boolean) => {
core.transport.dispatch({
action: 'Player',
args: {
action: 'MarkVideoAsWatched',
args: [video, !watched]
}
});
}, []);
const onMouseDown = (event: React.MouseEvent) => {
event.stopPropagation();
};
return (
<div ref={ref} className={classNames(styles['side-drawer'], className)} onMouseDown={onMouseDown}>
<div className={styles['close-button']} onClick={closeSideDrawer}>
<Icon className={styles['icon']} name={'chevron-forward'} />
</div>
<div className={styles['info']}>
<MetaPreview
className={styles['side-drawer-meta-preview']}
compact={true}
name={metaItem.name}
logo={metaItem.logo}
runtime={metaItem.runtime}
releaseInfo={metaItem.releaseInfo}
released={metaItem.released}
description={metaItem.description}
links={metaItem.links}
/>
</div>
{
seriesInfo ?
<div className={styles['series-content']}>
<SeasonsBar
season={season}
seasons={seasons}
onSelect={seasonOnSelect}
/>
<div className={styles['videos']}>
{videos.map((video, index) => (
<Video
key={index}
className={styles['video']}
id={video.id}
title={video.title}
thumbnail={video.thumbnail}
episode={video.episode}
released={video.released}
upcoming={video.upcoming}
watched={video.watched}
progress={video.progress}
deepLinks={video.deepLinks}
scheduled={video.scheduled}
onMarkVideoAsWatched={onMarkVideoAsWatched}
/>
))}
</div>
</div>
: null
}
</div>
);
}));
export default SideDrawer;

View file

@ -0,0 +1,2 @@
import SideDrawer from './SideDrawer';
export default SideDrawer;

View file

@ -0,0 +1,47 @@
// Copyright (C) 2017-2024 Smart code 203358507
@import (reference) '~stremio/common/screen-sizes.less';
.side-drawer-button {
height: 12.5rem;
width: 7.5rem;
display: flex;
align-items: center;
justify-content: start;
padding-left: 0.5rem;
border-radius: 50%;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
background-color: var(--modal-background-color);
-webkit-tap-highlight-color: transparent;
cursor: pointer;
opacity: 1;
will-change: opacity;
transition: opacity 0.3s ease-in-out, border-radius 0.3s ease-in-out;
.icon {
position: relative;
width: 2.5rem;
height: 2.5rem;
color: var(--primary-foreground-color);
opacity: 0.6;
transition: 0.3s opacity ease-in-out;
}
&:hover {
.icon {
opacity: 1;
}
}
}
@media screen and (max-width: @xsmall) {
.side-drawer-button {
height: 8rem;
width: 4.5rem;
.icon {
width: 2rem;
height: 2rem;
}
}
}

View file

@ -0,0 +1,21 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React from 'react';
import classNames from 'classnames';
import Icon from '@stremio/stremio-icons/react';
import styles from './SideDrawerButton.less';
type Props = {
className: string,
onClick: () => void,
};
const SideDrawerButton = ({ className, onClick }: Props) => {
return (
<div className={classNames(className, styles['side-drawer-button'])} onClick={onClick}>
<Icon name={'chevron-back'} className={styles['icon']} />
</div>
);
};
export default SideDrawerButton;

View file

@ -0,0 +1,2 @@
import SideDrawerButton from './SideDrawerButton';
export default SideDrawerButton;

View file

@ -129,19 +129,19 @@ const SubtitlesMenu = React.memo((props) => {
const onSubtitlesOffsetChanged = React.useCallback((event) => {
const delta = event.value === 'increment' ? 1 : -1;
if (typeof props.selectedSubtitlesTrackId === 'string') {
if (props.extraSubtitlesOffset !== null && !isNaN(props.extraSubtitlesOffset)) {
const offset = Math.max(0, Math.min(100, Math.floor(props.extraSubtitlesOffset + delta)));
if (typeof props.onExtraSubtitlesOffsetChanged === 'function') {
props.onExtraSubtitlesOffsetChanged(offset);
}
}
} else if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
if (props.subtitlesOffset !== null && !isNaN(props.subtitlesOffset)) {
const offset = Math.max(0, Math.min(100, Math.floor(props.subtitlesOffset + delta)));
if (typeof props.onSubtitlesOffsetChanged === 'function') {
props.onSubtitlesOffsetChanged(offset);
}
}
} else if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
if (props.extraSubtitlesOffset !== null && !isNaN(props.extraSubtitlesOffset)) {
const offset = Math.max(0, Math.min(100, Math.floor(props.extraSubtitlesOffset + delta)));
if (typeof props.onExtraSubtitlesOffsetChanged === 'function') {
props.onExtraSubtitlesOffsetChanged(offset);
}
}
}
}, [props.selectedSubtitlesTrackId, props.selectedExtraSubtitlesTrackId, props.subtitlesOffset, props.extraSubtitlesOffset, props.onSubtitlesOffsetChanged, props.onExtraSubtitlesOffsetChanged]);
return (

View file

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

View file

@ -1,7 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
.videos-menu-container {
width: 30rem;
padding: 1rem;
padding-bottom: 0;
}

View file

@ -1,6 +1,7 @@
// Copyright (C) 2017-2023 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@import (reference) '~stremio/common/screen-sizes.less';
:import('~stremio/common/Slider/styles.less') {
active-slider-within: active-slider-within;
@ -18,7 +19,7 @@ html:not(.active-slider-within) {
.player-container.overlayHidden {
cursor: none;
.nav-bar-layer, .control-bar-layer, .menu-layer {
.nav-bar-layer, .control-bar-layer, .menu-layer, .side-drawer-button-layer {
opacity: 0;
transition: opacity 200ms;
}
@ -83,6 +84,13 @@ html:not(.active-slider-within) {
}
}
&.side-drawer-button-layer {
top: 50%;
right: -4rem;
left: initial;
transform: translateY(-50%);
}
&.control-bar-layer {
top: initial;
overflow: visible;
@ -101,7 +109,7 @@ html:not(.active-slider-within) {
&.menu-layer {
top: initial;
left: initial;
right: 2rem;
right: 4rem;
bottom: 8rem;
max-height: calc(100% - 13.5rem);
max-width: calc(100% - 4rem);
@ -112,5 +120,33 @@ html:not(.active-slider-within) {
backdrop-filter: blur(15px);
overflow: auto;
}
&.side-drawer-layer {
bottom: 0;
right: 0;
left: initial;
bottom: initial;
}
}
}
}
@media screen and (max-width: @xsmall) {
.player-container {
.layer {
&.side-drawer-button-layer {
right: -2rem;
}
}
}
}
@media (orientation: portrait) and (max-width: @minimum) {
.player-container {
.layer {
&.menu-layer {
right: 2.5rem;
bottom: 11rem;
}
}
}
}

View file

@ -15,7 +15,7 @@ type MetaItemPreview = {
posterShape: PosterShape,
releaseInfo: string | null,
runtime: string | null,
released: string | null,
released: Date | null | undefined,
trailerStreams: TrailerStream[],
links: Link[],
behaviorHints: BehaviorHints,

View file

@ -14,4 +14,9 @@ type Video = {
episode?: number,
streams: Stream[],
trailerStreams: TrailerStream[],
watched: boolean,
progress: number,
upcoming: boolean,
deepLinks: VideoDeepLinks,
scheduled: boolean,
};

View file

@ -25,6 +25,11 @@ type Subtitle = {
url: string,
};
type SeriesInfo = {
episode: number,
season: number,
};
type Player = {
addon: Addon | null,
libraryItem: LibraryItemPlayer | null,
@ -36,10 +41,7 @@ type Player = {
streamRequest: ResourceRequest,
subtitlesPath: ResourceRequestPath,
} | null,
seriesInfo: {
season: number,
episode: number,
} | null,
seriesInfo: SeriesInfo | null,
subtitles: Subtitle[],
title: string | null,
};

View file

@ -1,8 +1,10 @@
// Copyright (C) 2017-2023 Smart code 203358507
const path = require('path');
const os = require('os');
const { execSync } = require('child_process');
const webpack = require('webpack');
const threadLoader = require('thread-loader');
const HtmlWebPackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
@ -14,6 +16,25 @@ const pachageJson = require('./package.json');
const COMMIT_HASH = execSync('git rev-parse HEAD').toString().trim();
const THREAD_LOADER = {
loader: 'thread-loader',
options: {
name: 'shared-pool',
workers: os.cpus().length,
},
};
threadLoader.warmup(
THREAD_LOADER.options,
[
'babel-loader',
'ts-loader',
'css-loader',
'postcss-loader',
'less-loader',
],
);
module.exports = (env, argv) => ({
mode: argv.mode,
devtool: argv.mode === 'production' ? 'source-map' : 'eval-source-map',
@ -30,20 +51,31 @@ module.exports = (env, argv) => ({
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react'
],
use: [
THREAD_LOADER,
{
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react'
],
}
}
}
]
},
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: 'ts-loader',
use: [
THREAD_LOADER,
{
loader: 'ts-loader',
options: {
happyPackMode: true,
}
}
]
},
{
test: /\.less$/,
@ -55,6 +87,7 @@ module.exports = (env, argv) => ({
esModule: false
}
},
THREAD_LOADER,
{
loader: 'css-loader',
options: {