mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-20 02:22:09 +00:00
refactor: align player menus with buttons
This commit is contained in:
parent
dc180589fc
commit
0ca7e5a53e
47 changed files with 572 additions and 505 deletions
140
package-lock.json
generated
140
package-lock.json
generated
|
|
@ -46,6 +46,7 @@
|
|||
"@babel/plugin-proposal-object-rest-spread": "7.16.0",
|
||||
"@babel/preset-env": "7.16.0",
|
||||
"@babel/preset-react": "7.16.0",
|
||||
"@types/classnames": "^2.3.1",
|
||||
"@types/react": "^18.2.9",
|
||||
"babel-loader": "8.2.3",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
|
|
@ -63,6 +64,8 @@
|
|||
"postcss-loader": "6.2.0",
|
||||
"readdirp": "3.6.0",
|
||||
"terser-webpack-plugin": "5.2.4",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.3.3",
|
||||
"webpack": "5.61.0",
|
||||
"webpack-cli": "4.9.1",
|
||||
"webpack-dev-server": "^4.7.4",
|
||||
|
|
@ -3085,6 +3088,16 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/classnames": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.1.tgz",
|
||||
"integrity": "sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==",
|
||||
"deprecated": "This is a stub types definition. classnames provides its own type definitions, so you do not need this installed.",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"classnames": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"dev": true,
|
||||
|
|
@ -13023,6 +13036,120 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader": {
|
||||
"version": "9.5.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz",
|
||||
"integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"enhanced-resolve": "^5.0.0",
|
||||
"micromatch": "^4.0.0",
|
||||
"semver": "^7.3.4",
|
||||
"source-map": "^0.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*",
|
||||
"webpack": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader/node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader/node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ts-loader/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader/node_modules/semver": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
|
||||
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader/node_modules/source-map": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
||||
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"license": "0BSD"
|
||||
|
|
@ -13077,6 +13204,19 @@
|
|||
"is-typedarray": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
"@babel/plugin-proposal-object-rest-spread": "7.16.0",
|
||||
"@babel/preset-env": "7.16.0",
|
||||
"@babel/preset-react": "7.16.0",
|
||||
"@types/classnames": "^2.3.1",
|
||||
"@types/react": "^18.2.9",
|
||||
"babel-loader": "8.2.3",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
|
|
@ -66,6 +67,8 @@
|
|||
"postcss-loader": "6.2.0",
|
||||
"readdirp": "3.6.0",
|
||||
"terser-webpack-plugin": "5.2.4",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.3.3",
|
||||
"webpack": "5.61.0",
|
||||
"webpack-cli": "4.9.1",
|
||||
"webpack-dev-server": "^4.7.4",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ const useTorrent = require('./useTorrent');
|
|||
const useTranslate = require('./useTranslate');
|
||||
const platform = require('./platform');
|
||||
const EventModal = require('./EventModal');
|
||||
const { default: useOnClickOutside } = require('./useOnClickOutside');
|
||||
const { default: useKeyboardEvent } = require('./useKeyboardEvent');
|
||||
const { default: useMouseEvent } = require('./useMouseEvent');
|
||||
|
||||
module.exports = {
|
||||
AddonDetailsModal,
|
||||
|
|
@ -96,4 +99,7 @@ module.exports = {
|
|||
useTranslate,
|
||||
platform,
|
||||
EventModal,
|
||||
useOnClickOutside,
|
||||
useKeyboardEvent,
|
||||
useMouseEvent,
|
||||
};
|
||||
|
|
|
|||
16
src/common/useKeyboardEvent.ts
Normal file
16
src/common/useKeyboardEvent.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const useKeyboardEvent = (name: string, handler: (shift: boolean) => void, ignore?: boolean) => {
|
||||
useEffect(() => {
|
||||
const onKeyDown = ({ code, shiftKey }: KeyboardEvent) => {
|
||||
!ignore && code === name && handler(shiftKey);
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
return () => document.removeEventListener('keydown', onKeyDown);
|
||||
}, [handler, ignore]);
|
||||
};
|
||||
|
||||
export default useKeyboardEvent;
|
||||
17
src/common/useMouseEvent.ts
Normal file
17
src/common/useMouseEvent.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const useMouseEvent = (name: string, handler: () => void, ignore?: boolean) => {
|
||||
useEffect(() => {
|
||||
const onWheel = ({ deltaY }: WheelEvent) => {
|
||||
!ignore && name === 'ScrollDown' && deltaY > 0 && handler();
|
||||
!ignore && name === 'ScrollUp' && deltaY < 0 && handler();
|
||||
};
|
||||
|
||||
document.addEventListener('wheel', onWheel);
|
||||
return () => document.removeEventListener('wheel', onWheel);
|
||||
}, [handler, ignore]);
|
||||
};
|
||||
|
||||
export default useMouseEvent;
|
||||
19
src/common/useOnClickOutside.tsx
Normal file
19
src/common/useOnClickOutside.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const useOnClickOutside = (ref: React.MutableRefObject<HTMLElement>, handler: () => void) => {
|
||||
useEffect(() => {
|
||||
const onClickOutside = (event: MouseEvent) => {
|
||||
const element = event.target as Node;
|
||||
if (ref.current && !ref.current.contains(element)) {
|
||||
handler();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', onClickOutside);
|
||||
return () => document.removeEventListener('click', onClickOutside);
|
||||
}, [handler]);
|
||||
};
|
||||
|
||||
export default useOnClickOutside;
|
||||
2
src/modules.d.ts
vendored
2
src/modules.d.ts
vendored
|
|
@ -1,2 +1,2 @@
|
|||
declare module '*';
|
||||
declare module 'classnames';
|
||||
declare module 'stremio/common';
|
||||
66
src/routes/Player/ControlBar/Control/Control.tsx
Normal file
66
src/routes/Player/ControlBar/Control/Control.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Icon from '@stremio/stremio-icons/react';
|
||||
import { Button, useBinaryState, useOnClickOutside, useKeyboardEvent } from 'stremio/common';
|
||||
import styles from './styles.less';
|
||||
|
||||
type Props = {
|
||||
children?: JSX.Element,
|
||||
className?: string,
|
||||
disabled?: boolean,
|
||||
icon: string,
|
||||
title?: string,
|
||||
shortcut?: string,
|
||||
onMenuChange?: (state: boolean) => void,
|
||||
onClick?: () => void,
|
||||
};
|
||||
|
||||
const Control = ({ children, className, disabled, icon, title, shortcut, onMenuChange, onClick }: Props) => {
|
||||
const ref = useRef(null);
|
||||
const [menuOpen,, closeMenu, toggleMenu] = useBinaryState();
|
||||
|
||||
const onButtonClick = () => {
|
||||
toggleMenu();
|
||||
onClick && onClick();
|
||||
};
|
||||
|
||||
const onMenuClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const onClickOutside = useCallback(() => {
|
||||
menuOpen && closeMenu();
|
||||
}, [menuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
onMenuChange && onMenuChange(menuOpen);
|
||||
}, [menuOpen, onMenuChange]);
|
||||
|
||||
useOnClickOutside(ref, onClickOutside);
|
||||
useKeyboardEvent('Escape', closeMenu);
|
||||
shortcut && useKeyboardEvent(shortcut, toggleMenu);
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
className={classNames(className, styles['control-button'], { 'disabled': disabled })}
|
||||
tabIndex={-1}
|
||||
title={title}
|
||||
onClick={onButtonClick}
|
||||
>
|
||||
<Icon className={styles['icon']} name={icon} />
|
||||
{
|
||||
children && menuOpen ?
|
||||
<div className={styles['menu-container']} onClick={onMenuClick}>
|
||||
{children}
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Control;
|
||||
4
src/routes/Player/ControlBar/Control/index.ts
Normal file
4
src/routes/Player/ControlBar/Control/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
import Control from './Control';
|
||||
export default Control;
|
||||
36
src/routes/Player/ControlBar/Control/styles.less
Normal file
36
src/routes/Player/ControlBar/Control/styles.less
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
.control-button {
|
||||
flex: none;
|
||||
position: relative;
|
||||
width: 4rem;
|
||||
height: 5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
|
||||
&:global(.disabled) {
|
||||
.icon {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: none;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
.menu-container {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 8rem;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--modal-background-color);
|
||||
box-shadow: 0 1.35rem 2.7rem rgba(0, 0, 0, 0.4), 0 1.1rem 0.85rem rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(15px);
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,107 +1,100 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||
const { Button } = require('stremio/common');
|
||||
const { t } = require('i18next');
|
||||
const { useBinaryState } = require('stremio/common');
|
||||
const { useServices } = require('stremio/services');
|
||||
const useStatistics = require('./useStatistics');
|
||||
const SeekBar = require('./SeekBar');
|
||||
const VolumeSlider = require('./VolumeSlider');
|
||||
const { StatisticsMenu, SpeedMenu, InfoMenu, VideosMenu, SubtitlesMenu, OptionsMenu } = require('./Menus');
|
||||
const { default: Control } = require('./Control');
|
||||
const styles = require('./styles');
|
||||
const { useBinaryState } = require('stremio/common');
|
||||
const { t } = require('i18next');
|
||||
|
||||
const ControlBar = ({
|
||||
className,
|
||||
paused,
|
||||
time,
|
||||
duration,
|
||||
buffered,
|
||||
volume,
|
||||
muted,
|
||||
playbackSpeed,
|
||||
subtitlesTracks,
|
||||
audioTracks,
|
||||
metaItem,
|
||||
nextVideo,
|
||||
stream,
|
||||
statistics,
|
||||
onPlayRequested,
|
||||
onPauseRequested,
|
||||
video,
|
||||
player,
|
||||
streamingServer,
|
||||
onPlayPauseRequested,
|
||||
onNextVideoRequested,
|
||||
onMuteRequested,
|
||||
onUnmuteRequested,
|
||||
onVolumeChangeRequested,
|
||||
onSeekRequested,
|
||||
onToggleSubtitlesMenu,
|
||||
onToggleInfoMenu,
|
||||
onToggleSpeedMenu,
|
||||
onToggleVideosMenu,
|
||||
onToggleOptionsMenu,
|
||||
onToggleStatisticsMenu,
|
||||
onPlaybackSpeedChangeRequested,
|
||||
onSubtitlesTrackSelected,
|
||||
onExtraSubtitlesTrackSelected,
|
||||
onAudioTrackSelected,
|
||||
onSubtitlesOffsetChanged,
|
||||
onSubtitlesSizeChanged,
|
||||
onExtraSubtitlesDelayChanged,
|
||||
onMenuChange,
|
||||
...props
|
||||
}) => {
|
||||
const { chromecast } = useServices();
|
||||
const statistics = useStatistics(player, streamingServer);
|
||||
|
||||
const [chromecastServiceActive, setChromecastServiceActive] = React.useState(() => chromecast.active);
|
||||
const [buttonsMenuOpen, , , toogleButtonsMenu] = useBinaryState(false);
|
||||
const onSubtitlesButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.subtitlesMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onInfoButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.infoMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onSpeedButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.speedMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onVideosButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.videosMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onOptionsButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.optionsMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onStatisticsButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.statisticsMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onPlayPauseButtonClick = React.useCallback(() => {
|
||||
if (paused) {
|
||||
if (typeof onPlayRequested === 'function') {
|
||||
onPlayRequested();
|
||||
}
|
||||
} else {
|
||||
if (typeof onPauseRequested === 'function') {
|
||||
onPauseRequested();
|
||||
}
|
||||
}
|
||||
}, [paused, onPlayRequested, onPauseRequested]);
|
||||
const onNextVideoButtonClick = React.useCallback(() => {
|
||||
if (nextVideo !== null && typeof onNextVideoRequested === 'function') {
|
||||
onNextVideoRequested();
|
||||
}
|
||||
}, [nextVideo, onNextVideoRequested]);
|
||||
const [mobileMenuOpen, , , toogleMobileMenu] = useBinaryState(false);
|
||||
|
||||
const {
|
||||
subtitlesTracks,
|
||||
extraSubtitlesTracks,
|
||||
audioTracks,
|
||||
selectedAudioTrackId,
|
||||
selectedSubtitlesTrackId,
|
||||
selectedExtraSubtitlesTrackId,
|
||||
subtitlesOffset,
|
||||
subtitlesSize,
|
||||
extraSubtitlesOffset,
|
||||
extraSubtitlesDelay,
|
||||
extraSubtitlesSize,
|
||||
} = video.state;
|
||||
|
||||
const { paused, buffered, muted, volume, time, duration, playbackSpeed } = video.state;
|
||||
const { seriesInfo, nextVideo, addon } = player;
|
||||
|
||||
const tracks = React.useMemo(() => {
|
||||
return subtitlesTracks && extraSubtitlesTracks && audioTracks ? subtitlesTracks.concat(extraSubtitlesTracks).concat(audioTracks): [];
|
||||
}, [subtitlesTracks, extraSubtitlesTracks, audioTracks]);
|
||||
|
||||
const metaItem = React.useMemo(() => {
|
||||
return player.metaItem !== null && player.metaItem.type === 'Ready' ? player.metaItem.content : null;
|
||||
}, [player]);
|
||||
|
||||
const stream = React.useMemo(() => {
|
||||
return player.selected !== null ? player.selected.stream : null;
|
||||
}, [player]);
|
||||
|
||||
const playbackDevices = React.useMemo(() => {
|
||||
return streamingServer.playbackDevices !== null && streamingServer.playbackDevices.type === 'Ready' ? streamingServer.playbackDevices.content : [];
|
||||
}, [streamingServer]);
|
||||
|
||||
const onMuteButtonClick = React.useCallback(() => {
|
||||
if (muted) {
|
||||
if (typeof onUnmuteRequested === 'function') {
|
||||
onUnmuteRequested();
|
||||
}
|
||||
} else {
|
||||
if (typeof onMuteRequested === 'function') {
|
||||
onMuteRequested();
|
||||
}
|
||||
}
|
||||
muted ? onUnmuteRequested() : onMuteRequested();
|
||||
}, [muted, onMuteRequested, onUnmuteRequested]);
|
||||
|
||||
const onChromecastButtonClick = React.useCallback(() => {
|
||||
chromecast.transport.requestSession();
|
||||
}, []);
|
||||
|
||||
const volumeIcon = React.useMemo(() => {
|
||||
return (typeof muted === 'boolean' && muted) ? 'volume-mute' :
|
||||
(volume === null || isNaN(volume)) ? 'volume-off' :
|
||||
volume < 30 ? 'volume-low' :
|
||||
volume < 70 ? 'volume-medium' :
|
||||
'volume-high';
|
||||
}, [muted, volume]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const onStateChanged = () => {
|
||||
setChromecastServiceActive(chromecast.active);
|
||||
};
|
||||
const onStateChanged = () => setChromecastServiceActive(chromecast.active);
|
||||
chromecast.on('stateChanged', onStateChanged);
|
||||
return () => {
|
||||
chromecast.off('stateChanged', onStateChanged);
|
||||
};
|
||||
return () => chromecast.off('stateChanged', onStateChanged);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div {...props} className={classnames(className, styles['control-bar-container'])}>
|
||||
<SeekBar
|
||||
|
|
@ -112,65 +105,98 @@ const ControlBar = ({
|
|||
onSeekRequested={onSeekRequested}
|
||||
/>
|
||||
<div className={styles['control-bar-buttons-container']}>
|
||||
<Button className={classnames(styles['control-bar-button'], { 'disabled': typeof paused !== 'boolean' })} title={paused ? t('PLAYER_PLAY') : t('PLAYER_PAUSE')} tabIndex={-1} onClick={onPlayPauseButtonClick}>
|
||||
<Icon className={styles['icon']} name={typeof paused !== 'boolean' || paused ? 'play' : 'pause'} />
|
||||
</Button>
|
||||
<Control
|
||||
icon={typeof paused !== 'boolean' || paused ? 'play' : 'pause'}
|
||||
title={paused ? t('PLAYER_PLAY') : t('PLAYER_PAUSE')}
|
||||
disabled={typeof paused !== 'boolean'}
|
||||
onClick={onPlayPauseRequested}
|
||||
/>
|
||||
{
|
||||
nextVideo !== null ?
|
||||
<Button className={classnames(styles['control-bar-button'])} title={t('PLAYER_NEXT_VIDEO')} tabIndex={-1} onClick={onNextVideoButtonClick}>
|
||||
<Icon className={styles['icon']} name={'next'} />
|
||||
</Button>
|
||||
nextVideo ?
|
||||
<Control
|
||||
disabled={typeof paused !== 'boolean'}
|
||||
icon={'next'}
|
||||
title={t('PLAYER_NEXT_VIDEO')}
|
||||
onClick={onNextVideoRequested}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
<Button className={classnames(styles['control-bar-button'], { 'disabled': typeof muted !== 'boolean' })} title={muted ? t('PLAYER_UNMUTE') : t('PLAYER_MUTE')} tabIndex={-1} onClick={onMuteButtonClick}>
|
||||
<Icon
|
||||
className={styles['icon']}
|
||||
name={
|
||||
(typeof muted === 'boolean' && muted) ? 'volume-mute' :
|
||||
(volume === null || isNaN(volume)) ? 'volume-off' :
|
||||
volume < 30 ? 'volume-low' :
|
||||
volume < 70 ? 'volume-medium' :
|
||||
'volume-high'
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
<Control
|
||||
disabled={typeof muted !== 'boolean'}
|
||||
icon={volumeIcon}
|
||||
title={muted ? t('PLAYER_UNMUTE') : t('PLAYER_MUTE')}
|
||||
onClick={onMuteButtonClick}
|
||||
/>
|
||||
<VolumeSlider
|
||||
className={styles['volume-slider']}
|
||||
volume={volume}
|
||||
onVolumeChangeRequested={onVolumeChangeRequested}
|
||||
/>
|
||||
<div className={styles['spacing']} />
|
||||
<Button className={styles['control-bar-buttons-menu-button']} onClick={toogleButtonsMenu}>
|
||||
<Icon className={styles['icon']} name={'more-vertical'} />
|
||||
</Button>
|
||||
<div className={classnames(styles['control-bar-buttons-menu-container'], { 'open': buttonsMenuOpen })}>
|
||||
<Button className={classnames(styles['control-bar-button'], { 'disabled': statistics === null || statistics.type === 'Err' || stream === null || typeof stream.infoHash !== 'string' || typeof stream.fileIdx !== 'number' })} tabIndex={-1} onMouseDown={onStatisticsButtonMouseDown} onClick={onToggleStatisticsMenu}>
|
||||
<Icon className={styles['icon']} name={'network'} />
|
||||
</Button>
|
||||
<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>
|
||||
<Button className={classnames(styles['control-bar-button'], { 'disabled': (!Array.isArray(subtitlesTracks) || subtitlesTracks.length === 0) && (!Array.isArray(audioTracks) || audioTracks.length === 0) })} tabIndex={-1} onMouseDown={onSubtitlesButtonMouseDown} onClick={onToggleSubtitlesMenu}>
|
||||
<Icon className={styles['icon']} name={'subtitles'} />
|
||||
</Button>
|
||||
<Control
|
||||
className={styles['menu-button']}
|
||||
icon={'more-vertical'}
|
||||
onClick={toogleMobileMenu}
|
||||
/>
|
||||
<div className={classnames(styles['controls-menu-container'], { 'open': mobileMenuOpen })}>
|
||||
<Control icon={'network'} disabled={!statistics || statistics.infoHash === null || !stream} shortcut={'KeyD'} onMenuChange={onMenuChange}>
|
||||
<StatisticsMenu {...statistics} />
|
||||
</Control>
|
||||
<Control icon={'speed'} disabled={!playbackSpeed} shortcut={'KeyR'} onMenuChange={onMenuChange}>
|
||||
<SpeedMenu
|
||||
playbackSpeed={playbackSpeed}
|
||||
onChange={onPlaybackSpeedChangeRequested}
|
||||
/>
|
||||
</Control>
|
||||
<Control icon={'about'} disabled={!metaItem || !stream} shortcut={'KeyI'} onMenuChange={onMenuChange}>
|
||||
<InfoMenu
|
||||
stream={stream}
|
||||
addon={addon}
|
||||
metaItem={metaItem}
|
||||
/>
|
||||
</Control>
|
||||
<Control icon={'cast'} disabled={!chromecastServiceActive} onClick={onChromecastButtonClick} />
|
||||
<Control icon={'subtitles'} disabled={tracks.length === 0} shortcut={'KeyS'} onMenuChange={onMenuChange}>
|
||||
<SubtitlesMenu
|
||||
audioTracks={audioTracks}
|
||||
selectedAudioTrackId={selectedAudioTrackId}
|
||||
subtitlesTracks={subtitlesTracks}
|
||||
selectedSubtitlesTrackId={selectedSubtitlesTrackId}
|
||||
subtitlesOffset={subtitlesOffset}
|
||||
subtitlesSize={subtitlesSize}
|
||||
extraSubtitlesTracks={extraSubtitlesTracks}
|
||||
selectedExtraSubtitlesTrackId={selectedExtraSubtitlesTrackId}
|
||||
extraSubtitlesOffset={extraSubtitlesOffset}
|
||||
extraSubtitlesDelay={extraSubtitlesDelay}
|
||||
extraSubtitlesSize={extraSubtitlesSize}
|
||||
onSubtitlesTrackSelected={onSubtitlesTrackSelected}
|
||||
onExtraSubtitlesTrackSelected={onExtraSubtitlesTrackSelected}
|
||||
onAudioTrackSelected={onAudioTrackSelected}
|
||||
onSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
|
||||
onSubtitlesSizeChanged={onSubtitlesSizeChanged}
|
||||
onExtraSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
|
||||
onExtraSubtitlesDelayChanged={onExtraSubtitlesDelayChanged}
|
||||
onExtraSubtitlesSizeChanged={onSubtitlesSizeChanged}
|
||||
/>
|
||||
</Control>
|
||||
{
|
||||
metaItem?.content?.videos?.length > 0 ?
|
||||
<Button className={styles['control-bar-button']} tabIndex={-1} onMouseDown={onVideosButtonMouseDown} onClick={onToggleVideosMenu}>
|
||||
<Icon className={styles['icon']} name={'episodes'} />
|
||||
</Button>
|
||||
metaItem?.videos?.length > 0 ?
|
||||
<Control icon={'episodes'} shortcut={'KeyV'} onMenuChange={onMenuChange}>
|
||||
<VideosMenu
|
||||
metaItem={metaItem}
|
||||
seriesInfo={seriesInfo}
|
||||
/>
|
||||
</Control>
|
||||
:
|
||||
null
|
||||
}
|
||||
<Button className={styles['control-bar-button']} tabIndex={-1} onMouseDown={onOptionsButtonMouseDown} onClick={onToggleOptionsMenu}>
|
||||
<Icon className={styles['icon']} name={'more-horizontal'} />
|
||||
</Button>
|
||||
<Control icon={'more-horizontal'} onMenuChange={onMenuChange}>
|
||||
<OptionsMenu
|
||||
stream={stream}
|
||||
playbackDevices={playbackDevices}
|
||||
/>
|
||||
</Control>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -179,32 +205,23 @@ const ControlBar = ({
|
|||
|
||||
ControlBar.propTypes = {
|
||||
className: PropTypes.string,
|
||||
paused: PropTypes.bool,
|
||||
time: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
buffered: PropTypes.number,
|
||||
volume: PropTypes.number,
|
||||
muted: PropTypes.bool,
|
||||
playbackSpeed: PropTypes.number,
|
||||
subtitlesTracks: PropTypes.array,
|
||||
audioTracks: PropTypes.array,
|
||||
metaItem: PropTypes.object,
|
||||
nextVideo: PropTypes.object,
|
||||
stream: PropTypes.object,
|
||||
statistics: PropTypes.object,
|
||||
onPlayRequested: PropTypes.func,
|
||||
onPauseRequested: PropTypes.func,
|
||||
video: PropTypes.object,
|
||||
player: PropTypes.object,
|
||||
streamingServer: PropTypes.object,
|
||||
onPlayPauseRequested: PropTypes.func,
|
||||
onNextVideoRequested: PropTypes.func,
|
||||
onMuteRequested: PropTypes.func,
|
||||
onUnmuteRequested: PropTypes.func,
|
||||
onVolumeChangeRequested: PropTypes.func,
|
||||
onSeekRequested: PropTypes.func,
|
||||
onToggleSubtitlesMenu: PropTypes.func,
|
||||
onToggleInfoMenu: PropTypes.func,
|
||||
onToggleSpeedMenu: PropTypes.func,
|
||||
onToggleVideosMenu: PropTypes.func,
|
||||
onToggleOptionsMenu: PropTypes.func,
|
||||
onToggleStatisticsMenu: PropTypes.func,
|
||||
onPlaybackSpeedChangeRequested: PropTypes.func,
|
||||
onSubtitlesTrackSelected: PropTypes.func,
|
||||
onExtraSubtitlesTrackSelected: PropTypes.func,
|
||||
onAudioTrackSelected: PropTypes.func,
|
||||
onSubtitlesOffsetChanged: PropTypes.func,
|
||||
onSubtitlesSizeChanged: PropTypes.func,
|
||||
onExtraSubtitlesDelayChanged: PropTypes.func,
|
||||
onMenuChange: PropTypes.func,
|
||||
};
|
||||
|
||||
module.exports = ControlBar;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const classnames = require('classnames');
|
|||
const { MetaPreview, CONSTANTS } = require('stremio/common');
|
||||
const styles = require('./styles');
|
||||
|
||||
const InfoMenu = ({ className, ...props }) => {
|
||||
const InfoMenu = ({ ...props }) => {
|
||||
const metaItem = React.useMemo(() => {
|
||||
return props.metaItem !== null ?
|
||||
{
|
||||
|
|
@ -22,7 +22,7 @@ const InfoMenu = ({ className, ...props }) => {
|
|||
event.nativeEvent.infoMenuClosePrevented = true;
|
||||
}, []);
|
||||
return (
|
||||
<div className={classnames(className, styles['info-menu-container'])} onMouseDown={onMouseDown}>
|
||||
<div className={classnames(styles['info-menu-container'])} onMouseDown={onMouseDown}>
|
||||
{
|
||||
metaItem !== null ?
|
||||
<MetaPreview
|
||||
|
|
@ -68,7 +68,6 @@ const InfoMenu = ({ className, ...props }) => {
|
|||
};
|
||||
|
||||
InfoMenu.propTypes = {
|
||||
className: PropTypes.string,
|
||||
metaItem: PropTypes.object,
|
||||
addon: PropTypes.object,
|
||||
stream: PropTypes.object
|
||||
|
|
@ -9,16 +9,19 @@ const styles = require('./styles');
|
|||
|
||||
const RATES = Array.from(Array(8).keys(), (n) => n * 0.25 + 0.25).reverse();
|
||||
|
||||
const SpeedMenu = ({ className, playbackSpeed, onPlaybackSpeedChanged }) => {
|
||||
const SpeedMenu = ({ className, playbackSpeed, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.speedMenuClosePrevented = true;
|
||||
}, []);
|
||||
|
||||
const onOptionSelect = React.useCallback((value) => {
|
||||
if (typeof onPlaybackSpeedChanged === 'function') {
|
||||
onPlaybackSpeedChanged(value);
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(value);
|
||||
}
|
||||
}, [onPlaybackSpeedChanged]);
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<div className={classnames(className, styles['speed-menu-container'])} onMouseDown={onMouseDown}>
|
||||
<div className={styles['title']}>
|
||||
|
|
@ -44,7 +47,7 @@ const SpeedMenu = ({ className, playbackSpeed, onPlaybackSpeedChanged }) => {
|
|||
SpeedMenu.propTypes = {
|
||||
className: PropTypes.string,
|
||||
playbackSpeed: PropTypes.number,
|
||||
onPlaybackSpeedChanged: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
module.exports = SpeedMenu;
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const Video = require('../../MetaDetails/VideosList/Video');
|
||||
const Video = require('../../../../MetaDetails/VideosList/Video');
|
||||
const styles = require('./styles');
|
||||
|
||||
const VideosMenu = ({ className, metaItem, seriesInfo }) => {
|
||||
17
src/routes/Player/ControlBar/Menus/index.js
Normal file
17
src/routes/Player/ControlBar/Menus/index.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
const InfoMenu = require('./InfoMenu');
|
||||
const OptionsMenu = require('./OptionsMenu');
|
||||
const SpeedMenu = require('./SpeedMenu');
|
||||
const StatisticsMenu = require('./StatisticsMenu');
|
||||
const SubtitlesMenu = require('./SubtitlesMenu');
|
||||
const VideosMenu = require('./VideosMenu');
|
||||
|
||||
module.exports = {
|
||||
InfoMenu,
|
||||
OptionsMenu,
|
||||
SpeedMenu,
|
||||
StatisticsMenu,
|
||||
SubtitlesMenu,
|
||||
VideosMenu,
|
||||
};
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
@import (reference) '~stremio/common/screen-sizes.less';
|
||||
|
||||
.control-bar-container {
|
||||
|
|
@ -17,28 +16,7 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.control-bar-button {
|
||||
flex: none;
|
||||
width: 4rem;
|
||||
height: 5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:global(.disabled) {
|
||||
.icon {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: none;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
overflow: visible;
|
||||
|
||||
.volume-slider {
|
||||
--track-size: 0.35rem;
|
||||
|
|
@ -53,26 +31,15 @@
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.control-bar-buttons-menu-button {
|
||||
flex: none;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
.menu-button {
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
flex: none;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
.control-bar-buttons-menu-container {
|
||||
.controls-menu-container {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -94,11 +61,11 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.control-bar-buttons-menu-button {
|
||||
.menu-button {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.control-bar-buttons-menu-container {
|
||||
.controls-menu-container {
|
||||
position: absolute;
|
||||
right: 0.15rem;
|
||||
bottom: 4.5rem;
|
||||
|
|
@ -106,8 +73,7 @@
|
|||
padding: 0.5rem;
|
||||
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;
|
||||
box-shadow: 0 1.35rem 2.7rem rgba(0, 0, 0, 0.4), 0 1.1rem 0.85rem rgba(0, 0, 0, 0.2);
|
||||
|
||||
&:not(:global(.open)) {
|
||||
display: none;
|
||||
|
|
|
|||
|
|
@ -6,23 +6,15 @@ const classnames = require('classnames');
|
|||
const debounce = require('lodash.debounce');
|
||||
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, useFullscreen, useBinaryState, useToast, useStreamingServer, useKeyboardEvent, useMouseEvent, 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 SpeedMenu = require('./SpeedMenu');
|
||||
const usePlayer = require('./usePlayer');
|
||||
const useSettings = require('./useSettings');
|
||||
const useStatistics = require('./useStatistics');
|
||||
const useVideo = require('./useVideo');
|
||||
const styles = require('./styles');
|
||||
const Video = require('./Video');
|
||||
|
|
@ -37,9 +29,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
const [player, videoParamsChanged, timeChanged, pausedChanged, ended, nextVideo] = usePlayer(urlParams);
|
||||
const [settings, updateSettings] = useSettings();
|
||||
const streamingServer = useStreamingServer();
|
||||
const statistics = useStatistics(player, streamingServer);
|
||||
const video = useVideo();
|
||||
const routeFocused = useRouteFocused();
|
||||
const toast = useToast();
|
||||
|
||||
const [casting, setCasting] = React.useState(() => {
|
||||
|
|
@ -48,32 +38,18 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
|
||||
const [immersed, setImmersed] = React.useState(true);
|
||||
const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
|
||||
const [, , , toggleFullscreen] = useFullscreen();
|
||||
const [,,, toggleFullscreen] = useFullscreen();
|
||||
|
||||
const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false);
|
||||
const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = 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 [areMenusOpen, setMenusState] = React.useState(false);
|
||||
const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
|
||||
|
||||
const menusOpen = React.useMemo(() => {
|
||||
return optionsMenuOpen || subtitlesMenuOpen || infoMenuOpen || speedMenuOpen || videosMenuOpen || statisticsMenuOpen;
|
||||
}, [optionsMenuOpen, subtitlesMenuOpen, infoMenuOpen, speedMenuOpen, videosMenuOpen, statisticsMenuOpen]);
|
||||
|
||||
const closeMenus = React.useCallback(() => {
|
||||
closeOptionsMenu();
|
||||
closeSubtitlesMenu();
|
||||
closeInfoMenu();
|
||||
closeSpeedMenu();
|
||||
closeVideosMenu();
|
||||
closeStatisticsMenu();
|
||||
}, []);
|
||||
const ignoreShortcuts = React.useMemo(() => {
|
||||
return areMenusOpen || nextVideoPopupOpen;
|
||||
}, [areMenusOpen, nextVideoPopupOpen]);
|
||||
|
||||
const overlayHidden = React.useMemo(() => {
|
||||
return immersed && !casting && video.state.paused !== null && !video.state.paused && !menusOpen && !nextVideoPopupOpen;
|
||||
}, [immersed, casting, video.state.paused, menusOpen, nextVideoPopupOpen]);
|
||||
return immersed && !casting && video.state.paused !== null && !video.state.paused && !areMenusOpen && !nextVideoPopupOpen;
|
||||
}, [immersed, casting, video.state.paused, areMenusOpen, nextVideoPopupOpen]);
|
||||
|
||||
const nextVideoPopupDismissed = React.useRef(false);
|
||||
const defaultSubtitlesSelected = React.useRef(false);
|
||||
|
|
@ -145,6 +121,11 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
}, []);
|
||||
|
||||
const onPauseRequestedDebounced = React.useCallback(debounce(onPauseRequested, 200), []);
|
||||
|
||||
const onPlayPauseRequested = React.useCallback(() => {
|
||||
video.state.paused ? onPlayRequested() : onPauseRequested();
|
||||
}, [video.state.paused]);
|
||||
|
||||
const onMuteRequested = React.useCallback(() => {
|
||||
video.setProp('muted', true);
|
||||
}, []);
|
||||
|
|
@ -157,11 +138,29 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
video.setProp('volume', volume);
|
||||
}, []);
|
||||
|
||||
const onVolumeIncreaseRequested = React.useCallback(() => {
|
||||
video.state.volume !== null && onVolumeChangeRequested(video.state.volume + 5);
|
||||
}, [video.state.volume]);
|
||||
|
||||
const onVolumeDereaseRequested = React.useCallback(() => {
|
||||
video.state.volume !== null && onVolumeChangeRequested(video.state.volume - 5);
|
||||
}, [video.state.volume]);
|
||||
|
||||
const onSeekRequested = React.useCallback((time) => {
|
||||
video.setProp('time', time);
|
||||
}, []);
|
||||
|
||||
const onPlaybackSpeedChanged = React.useCallback((rate) => {
|
||||
const onSeekForwardRequested = React.useCallback((short) => {
|
||||
const duration = short ? settings.seekShortTimeDuration : settings.seekTimeDuration;
|
||||
video.state.time !== null && onSeekRequested(video.state.time + duration);
|
||||
}, [video.state.time]);
|
||||
|
||||
const onSeekBackwardRequested = React.useCallback((short) => {
|
||||
const duration = short ? settings.seekShortTimeDuration : settings.seekTimeDuration;
|
||||
video.state.time !== null && onSeekRequested(video.state.time - duration);
|
||||
}, [video.state.time]);
|
||||
|
||||
const onPlaybackSpeedChangeRequested = React.useCallback((rate) => {
|
||||
video.setProp('playbackSpeed', rate);
|
||||
}, []);
|
||||
|
||||
|
|
@ -226,27 +225,6 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
toggleFullscreen();
|
||||
}, [toggleFullscreen]);
|
||||
|
||||
const onContainerMouseDown = React.useCallback((event) => {
|
||||
if (!event.nativeEvent.optionsMenuClosePrevented) {
|
||||
closeOptionsMenu();
|
||||
}
|
||||
if (!event.nativeEvent.subtitlesMenuClosePrevented) {
|
||||
closeSubtitlesMenu();
|
||||
}
|
||||
if (!event.nativeEvent.infoMenuClosePrevented) {
|
||||
closeInfoMenu();
|
||||
}
|
||||
if (!event.nativeEvent.speedMenuClosePrevented) {
|
||||
closeSpeedMenu();
|
||||
}
|
||||
if (!event.nativeEvent.videosMenuClosePrevented) {
|
||||
closeVideosMenu();
|
||||
}
|
||||
if (!event.nativeEvent.statisticsMenuClosePrevented) {
|
||||
closeStatisticsMenu();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onContainerMouseMove = React.useCallback((event) => {
|
||||
setImmersed(false);
|
||||
if (!event.nativeEvent.immersePrevented) {
|
||||
|
|
@ -403,27 +381,6 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
nextVideoPopupDismissed.current = false;
|
||||
}, [video.state.stream]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if ((!Array.isArray(video.state.subtitlesTracks) || video.state.subtitlesTracks.length === 0) &&
|
||||
(!Array.isArray(video.state.extraSubtitlesTracks) || video.state.extraSubtitlesTracks.length === 0) &&
|
||||
(!Array.isArray(video.state.audioTracks) || video.state.audioTracks.length === 0)) {
|
||||
closeSubtitlesMenu();
|
||||
}
|
||||
}, [video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (player.metaItem === null || player.metaItem.type !== 'Ready') {
|
||||
closeInfoMenu();
|
||||
closeVideosMenu();
|
||||
}
|
||||
}, [player.metaItem]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (video.state.playbackSpeed === null) {
|
||||
closeSpeedMenu();
|
||||
}
|
||||
}, [video.state.playbackSpeed]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const toastFilter = (item) => item?.dataset?.type === 'CoreEvent';
|
||||
toast.addFilter(toastFilter);
|
||||
|
|
@ -460,118 +417,13 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const onKeyDown = (event) => {
|
||||
switch (event.code) {
|
||||
case 'Space': {
|
||||
if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) {
|
||||
if (video.state.paused) {
|
||||
onPlayRequested();
|
||||
} else {
|
||||
onPauseRequested();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'ArrowRight': {
|
||||
if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
|
||||
const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
|
||||
onSeekRequested(video.state.time + seekDuration);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'ArrowLeft': {
|
||||
if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
|
||||
const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
|
||||
onSeekRequested(video.state.time - seekDuration);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
|
||||
onVolumeChangeRequested(video.state.volume + 5);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'ArrowDown': {
|
||||
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
|
||||
onVolumeChangeRequested(video.state.volume - 5);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyS': {
|
||||
closeMenus();
|
||||
if ((Array.isArray(video.state.subtitlesTracks) && video.state.subtitlesTracks.length > 0) ||
|
||||
(Array.isArray(video.state.extraSubtitlesTracks) && video.state.extraSubtitlesTracks.length > 0) ||
|
||||
(Array.isArray(video.state.audioTracks) && video.state.audioTracks.length > 0)) {
|
||||
toggleSubtitlesMenu();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyI': {
|
||||
closeMenus();
|
||||
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
|
||||
toggleInfoMenu();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyR': {
|
||||
closeMenus();
|
||||
if (video.state.playbackSpeed !== null) {
|
||||
toggleSpeedMenu();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyV': {
|
||||
closeMenus();
|
||||
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
|
||||
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') {
|
||||
toggleStatisticsMenu();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'Escape': {
|
||||
closeMenus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
const onWheel = ({ deltaY }) => {
|
||||
if (deltaY > 0) {
|
||||
if (!menusOpen && video.state.volume !== null) {
|
||||
onVolumeChangeRequested(video.state.volume - 5);
|
||||
}
|
||||
} else {
|
||||
if (!menusOpen && video.state.volume !== null) {
|
||||
onVolumeChangeRequested(video.state.volume + 5);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (routeFocused) {
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
window.addEventListener('wheel', onWheel);
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
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]);
|
||||
useKeyboardEvent('Space', onPlayPauseRequested, ignoreShortcuts);
|
||||
useKeyboardEvent('ArrowRight', onSeekForwardRequested, ignoreShortcuts);
|
||||
useKeyboardEvent('ArrowLeft', onSeekBackwardRequested, ignoreShortcuts);
|
||||
useKeyboardEvent('ArrowUp', onVolumeIncreaseRequested, ignoreShortcuts);
|
||||
useKeyboardEvent('ArrowDown', onVolumeDereaseRequested, ignoreShortcuts);
|
||||
useMouseEvent('ScrollUp', onVolumeIncreaseRequested, ignoreShortcuts);
|
||||
useMouseEvent('ScrollDown', onVolumeDereaseRequested, ignoreShortcuts);
|
||||
|
||||
React.useEffect(() => {
|
||||
video.events.on('error', onError);
|
||||
|
|
@ -599,10 +451,10 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
|
||||
return (
|
||||
<div className={classnames(styles['player-container'], { [styles['overlayHidden']]: overlayHidden })}
|
||||
onMouseDown={onContainerMouseDown}
|
||||
onMouseMove={onContainerMouseMove}
|
||||
onMouseOver={onContainerMouseMove}
|
||||
onMouseLeave={onContainerMouseLeave}>
|
||||
onMouseLeave={onContainerMouseLeave}
|
||||
>
|
||||
<Video
|
||||
ref={video.containerElement}
|
||||
className={styles['layer']}
|
||||
|
|
@ -626,7 +478,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
null
|
||||
}
|
||||
{
|
||||
menusOpen ?
|
||||
areMenusOpen ?
|
||||
<div className={styles['layer']} />
|
||||
:
|
||||
null
|
||||
|
|
@ -636,6 +488,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
<VolumeChangeIndicator
|
||||
muted={video.state.muted}
|
||||
volume={video.state.volume}
|
||||
onVolumeChangeRequested={onVolumeChangeRequested}
|
||||
/>
|
||||
:
|
||||
null
|
||||
|
|
@ -650,32 +503,23 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
/>
|
||||
<ControlBar
|
||||
className={classnames(styles['layer'], styles['control-bar-layer'])}
|
||||
paused={video.state.paused}
|
||||
time={video.state.time}
|
||||
duration={video.state.duration}
|
||||
buffered={video.state.buffered}
|
||||
volume={video.state.volume}
|
||||
muted={video.state.muted}
|
||||
playbackSpeed={video.state.playbackSpeed}
|
||||
subtitlesTracks={video.state.subtitlesTracks.concat(video.state.extraSubtitlesTracks)}
|
||||
audioTracks={video.state.audioTracks}
|
||||
metaItem={player.metaItem}
|
||||
nextVideo={player.nextVideo}
|
||||
stream={player.selected !== null ? player.selected.stream : null}
|
||||
statistics={statistics}
|
||||
onPlayRequested={onPlayRequested}
|
||||
onPauseRequested={onPauseRequested}
|
||||
video={video}
|
||||
player={player}
|
||||
streamingServer={streamingServer}
|
||||
onPlayPauseRequested={onPlayPauseRequested}
|
||||
onNextVideoRequested={onNextVideoRequested}
|
||||
onMuteRequested={onMuteRequested}
|
||||
onUnmuteRequested={onUnmuteRequested}
|
||||
onVolumeChangeRequested={onVolumeChangeRequested}
|
||||
onSeekRequested={onSeekRequested}
|
||||
onToggleOptionsMenu={toggleOptionsMenu}
|
||||
onToggleSubtitlesMenu={toggleSubtitlesMenu}
|
||||
onToggleInfoMenu={toggleInfoMenu}
|
||||
onToggleSpeedMenu={toggleSpeedMenu}
|
||||
onToggleVideosMenu={toggleVideosMenu}
|
||||
onToggleStatisticsMenu={toggleStatisticsMenu}
|
||||
onPlaybackSpeedChangeRequested={onPlaybackSpeedChangeRequested}
|
||||
onSubtitlesTrackSelected={onSubtitlesTrackSelected}
|
||||
onExtraSubtitlesTrackSelected={onExtraSubtitlesTrackSelected}
|
||||
onAudioTrackSelected={onAudioTrackSelected}
|
||||
onSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
|
||||
onSubtitlesSizeChanged={onSubtitlesSizeChanged}
|
||||
onExtraSubtitlesDelayChanged={onExtraSubtitlesDelayChanged}
|
||||
onMenuChange={setMenusState}
|
||||
onMouseMove={onBarMouseMove}
|
||||
onMouseOver={onBarMouseMove}
|
||||
/>
|
||||
|
|
@ -691,83 +535,6 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
statisticsMenuOpen ?
|
||||
<StatisticsMenu
|
||||
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||
{...statistics}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
subtitlesMenuOpen ?
|
||||
<SubtitlesMenu
|
||||
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||
audioTracks={video.state.audioTracks}
|
||||
selectedAudioTrackId={video.state.selectedAudioTrackId}
|
||||
subtitlesTracks={video.state.subtitlesTracks}
|
||||
selectedSubtitlesTrackId={video.state.selectedSubtitlesTrackId}
|
||||
subtitlesOffset={video.state.subtitlesOffset}
|
||||
subtitlesSize={video.state.subtitlesSize}
|
||||
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
|
||||
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
|
||||
extraSubtitlesOffset={video.state.extraSubtitlesOffset}
|
||||
extraSubtitlesDelay={video.state.extraSubtitlesDelay}
|
||||
extraSubtitlesSize={video.state.extraSubtitlesSize}
|
||||
onSubtitlesTrackSelected={onSubtitlesTrackSelected}
|
||||
onExtraSubtitlesTrackSelected={onExtraSubtitlesTrackSelected}
|
||||
onAudioTrackSelected={onAudioTrackSelected}
|
||||
onSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
|
||||
onSubtitlesSizeChanged={onSubtitlesSizeChanged}
|
||||
onExtraSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
|
||||
onExtraSubtitlesDelayChanged={onExtraSubtitlesDelayChanged}
|
||||
onExtraSubtitlesSizeChanged={onSubtitlesSizeChanged}
|
||||
/>
|
||||
:
|
||||
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
|
||||
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||
playbackSpeed={video.state.playbackSpeed}
|
||||
onPlaybackSpeedChanged={onPlaybackSpeedChanged}
|
||||
/>
|
||||
:
|
||||
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
|
||||
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||
stream={player.selected.stream}
|
||||
playbackDevices={streamingServer.playbackDevices !== null && streamingServer.playbackDevices.type === 'Ready' ? streamingServer.playbackDevices.content : []}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -84,20 +84,5 @@ html:not(.active-slider-within) {
|
|||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
&.menu-layer {
|
||||
top: initial;
|
||||
left: initial;
|
||||
right: 2rem;
|
||||
bottom: 8rem;
|
||||
max-height: calc(100% - 13.5rem);
|
||||
max-width: calc(100% - 4rem);
|
||||
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;
|
||||
backdrop-filter: blur(15px);
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/types/models/Player.d.ts
vendored
10
src/types/models/Player.d.ts
vendored
|
|
@ -2,7 +2,7 @@ type LibraryItemPlayer = Pick<LibraryItem, '_id'> & {
|
|||
state: Pick<LibraryItemState, 'timeOffset' | 'video_id'>,
|
||||
};
|
||||
|
||||
type VideoPlayer = Video & {
|
||||
type PlayerVideo = Video & {
|
||||
upcomming: boolean,
|
||||
watched: boolean,
|
||||
progress: boolean | null,
|
||||
|
|
@ -10,8 +10,8 @@ type VideoPlayer = Video & {
|
|||
deepLinks: VideoDeepLinks,
|
||||
};
|
||||
|
||||
type MetaItemPlayer = MetaItemPreview & {
|
||||
videos: VideoPlayer[],
|
||||
type PlayerMetaItem = MetaItemPreview & {
|
||||
videos: PlayerVideo[],
|
||||
};
|
||||
|
||||
type SelectedStream = Stream & {
|
||||
|
|
@ -28,8 +28,8 @@ type Subtitle = {
|
|||
type Player = {
|
||||
addon: Addon | null,
|
||||
libraryItem: LibraryItemPlayer | null,
|
||||
metaItem: Loadable<MetaItemPlayer> | null,
|
||||
nextVideo: VideoPlayer | null,
|
||||
metaItem: Loadable<PlayerMetaItem> | null,
|
||||
nextVideo: PlayerVideo | null,
|
||||
selected: {
|
||||
stream: SelectedStream,
|
||||
metaRequest: ResourceRequest,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
const fs = require('fs');
|
||||
const readdirp = require('readdirp');
|
||||
|
||||
const COPYRIGHT_HEADER = /^\/\/ Copyright \(C\) 2017-2023 Smart code 203358507.*/;
|
||||
const COPYRIGHT_HEADER = /^\/\/ Copyright \(C\) 2017-\d{4} Smart code 203358507.*/;
|
||||
|
||||
describe('copyright', () => {
|
||||
test('js', async () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable"],
|
||||
"jsx": "preserve",
|
||||
"jsx": "react",
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": ".",
|
||||
|
|
@ -9,9 +9,10 @@
|
|||
"stremio/*": ["src/*"],
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"noEmit": true,
|
||||
"noEmit": false,
|
||||
"strict": false
|
||||
},
|
||||
"include": [
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ module.exports = (env, argv) => ({
|
|||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(ts|tsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: 'ts-loader',
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
|
|
@ -142,7 +147,7 @@ module.exports = (env, argv) => ({
|
|||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.json', '.less', '.wasm'],
|
||||
extensions: ['.tsx', '.ts', '.js', '.json', '.less', '.wasm'],
|
||||
alias: {
|
||||
'stremio': path.join(__dirname, 'src'),
|
||||
'stremio-router': path.join(__dirname, 'src', 'router')
|
||||
|
|
|
|||
Loading…
Reference in a new issue