diff --git a/package-lock.json b/package-lock.json index 5be6d2466..1f87bbd55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json old mode 100755 new mode 100644 index 80b43d540..8a4665615 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/common/EventModal/styles.less b/src/common/EventModal/styles.less index 24c1d053d..b251175e5 100644 --- a/src/common/EventModal/styles.less +++ b/src/common/EventModal/styles.less @@ -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 { diff --git a/src/common/MetaItem/styles.less b/src/common/MetaItem/styles.less index c2d094ac9..17c7c4884 100644 --- a/src/common/MetaItem/styles.less +++ b/src/common/MetaItem/styles.less @@ -166,6 +166,7 @@ object-position: center; object-fit: cover; opacity: 0.9; + overflow-clip-margin: unset; } .placeholder-icon { diff --git a/src/common/Transition/Transition.tsx b/src/common/Transition/Transition.tsx new file mode 100644 index 000000000..e12b2a42b --- /dev/null +++ b/src/common/Transition/Transition.tsx @@ -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(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; diff --git a/src/common/Transition/index.ts b/src/common/Transition/index.ts new file mode 100644 index 000000000..dd0f32cda --- /dev/null +++ b/src/common/Transition/index.ts @@ -0,0 +1,2 @@ +import Transition from './Transition'; +export default Transition; diff --git a/src/common/animations.less b/src/common/animations.less index 4b0776d16..6ca879343 100644 --- a/src/common/animations.less +++ b/src/common/animations.less @@ -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%); } \ No newline at end of file diff --git a/src/common/index.js b/src/common/index.js index 45eb0ef41..3690cfae5 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -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, diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index 840664c8b..56f9c0d08 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -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 } -
+
{filteredStreams.map((stream, index) => ( :first-child { margin-right: 0.5rem; } diff --git a/src/routes/Player/ControlBar/ControlBar.js b/src/routes/Player/ControlBar/ControlBar.js index ca9498320..a5fe1adf9 100644 --- a/src/routes/Player/ControlBar/ControlBar.js +++ b/src/routes/Player/ControlBar/ControlBar.js @@ -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 = ({ - @@ -169,7 +162,7 @@ const ControlBar = ({ { metaItem?.content?.videos?.length > 0 ? - : @@ -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, }; diff --git a/src/routes/Player/ControlBar/styles.less b/src/routes/Player/ControlBar/styles.less index 147a3f0a2..bcfbbdaef 100644 --- a/src/routes/Player/ControlBar/styles.less +++ b/src/routes/Player/ControlBar/styles.less @@ -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; diff --git a/src/routes/Player/InfoMenu/InfoMenu.js b/src/routes/Player/InfoMenu/InfoMenu.js deleted file mode 100644 index eec5e7286..000000000 --- a/src/routes/Player/InfoMenu/InfoMenu.js +++ /dev/null @@ -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 ( -
- { - metaItem !== null ? - - : - null - } - {/* { - props.stream !== null ? - - : - null - } */} - {/* { - props.addon !== null ? - - : - null - } */} -
- ); -}; - -InfoMenu.propTypes = { - className: PropTypes.string, - metaItem: PropTypes.object, - addon: PropTypes.object, - stream: PropTypes.object -}; - -module.exports = InfoMenu; diff --git a/src/routes/Player/InfoMenu/index.js b/src/routes/Player/InfoMenu/index.js deleted file mode 100644 index 8dae65490..000000000 --- a/src/routes/Player/InfoMenu/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const InfoMenu = require('./InfoMenu'); - -module.exports = InfoMenu; diff --git a/src/routes/Player/InfoMenu/styles.less b/src/routes/Player/InfoMenu/styles.less deleted file mode 100644 index 32a72b77d..000000000 --- a/src/routes/Player/InfoMenu/styles.less +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -.info-menu-container { - width: 30rem; - padding: 2rem; - - .stream { - pointer-events: none; - } -} \ No newline at end of file diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 2d93689d7..2d48f326d 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -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' ? + + : + null + } { 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 } + + + { subtitlesMenuOpen ? { : null } - { - infoMenuOpen ? - - : - null - } { speedMenuOpen ? { : null } - { - videosMenuOpen ? - - : - null - } { optionsMenuOpen ? void; +}; + +const SideDrawer = memo(forwardRef(({ seriesInfo, className, closeSideDrawer, ...props }: Props, ref) => { + const { core } = useServices(); + const [season, setSeason] = useState(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 ( +
+
+ +
+
+ +
+ { + seriesInfo ? +
+ +
+ {videos.map((video, index) => ( +
+
+ : null + } + +
+ ); +})); + +export default SideDrawer; diff --git a/src/routes/Player/SideDrawer/index.ts b/src/routes/Player/SideDrawer/index.ts new file mode 100644 index 000000000..e19e268d3 --- /dev/null +++ b/src/routes/Player/SideDrawer/index.ts @@ -0,0 +1,2 @@ +import SideDrawer from './SideDrawer'; +export default SideDrawer; diff --git a/src/routes/Player/SideDrawerButton/SideDrawerButton.less b/src/routes/Player/SideDrawerButton/SideDrawerButton.less new file mode 100644 index 000000000..e7e86027f --- /dev/null +++ b/src/routes/Player/SideDrawerButton/SideDrawerButton.less @@ -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; + } + } +} \ No newline at end of file diff --git a/src/routes/Player/SideDrawerButton/SideDrawerButton.tsx b/src/routes/Player/SideDrawerButton/SideDrawerButton.tsx new file mode 100644 index 000000000..81409f434 --- /dev/null +++ b/src/routes/Player/SideDrawerButton/SideDrawerButton.tsx @@ -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 ( +
+ +
+ ); +}; + +export default SideDrawerButton; diff --git a/src/routes/Player/SideDrawerButton/index.ts b/src/routes/Player/SideDrawerButton/index.ts new file mode 100644 index 000000000..73585e555 --- /dev/null +++ b/src/routes/Player/SideDrawerButton/index.ts @@ -0,0 +1,2 @@ +import SideDrawerButton from './SideDrawerButton'; +export default SideDrawerButton; diff --git a/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js b/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js index 3fed474b1..515dc4758 100644 --- a/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js +++ b/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js @@ -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 ( diff --git a/src/routes/Player/VideosMenu/index.js b/src/routes/Player/VideosMenu/index.js deleted file mode 100644 index c2fa21666..000000000 --- a/src/routes/Player/VideosMenu/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const VideosMenu = require('./VideosMenu'); - -module.exports = VideosMenu; diff --git a/src/routes/Player/VideosMenu/styles.less b/src/routes/Player/VideosMenu/styles.less deleted file mode 100644 index 8cc7e4805..000000000 --- a/src/routes/Player/VideosMenu/styles.less +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -.videos-menu-container { - width: 30rem; - padding: 1rem; - padding-bottom: 0; -} \ No newline at end of file diff --git a/src/routes/Player/styles.less b/src/routes/Player/styles.less index 8bff42fe1..e74604294 100644 --- a/src/routes/Player/styles.less +++ b/src/routes/Player/styles.less @@ -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; + } } -} \ No newline at end of file +} + +@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; + } + } + } +} diff --git a/src/types/MetaItem.d.ts b/src/types/MetaItem.d.ts index 0d692a580..56e7db3c7 100644 --- a/src/types/MetaItem.d.ts +++ b/src/types/MetaItem.d.ts @@ -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, diff --git a/src/types/Video.d.ts b/src/types/Video.d.ts index 92bc704f5..7ed3d0fac 100644 --- a/src/types/Video.d.ts +++ b/src/types/Video.d.ts @@ -14,4 +14,9 @@ type Video = { episode?: number, streams: Stream[], trailerStreams: TrailerStream[], + watched: boolean, + progress: number, + upcoming: boolean, + deepLinks: VideoDeepLinks, + scheduled: boolean, }; diff --git a/src/types/models/Player.d.ts b/src/types/models/Player.d.ts index 6664e83f5..2add1456b 100644 --- a/src/types/models/Player.d.ts +++ b/src/types/models/Player.d.ts @@ -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, }; diff --git a/webpack.config.js b/webpack.config.js index 1b6c6601b..b23ee4c15 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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: {