mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
feat(Player): add options menu
This commit is contained in:
parent
92fa3d53a5
commit
3ff7a67fe5
5 changed files with 161 additions and 2 deletions
|
|
@ -29,6 +29,7 @@ const ControlBar = ({
|
|||
onSeekRequested,
|
||||
onToggleSubtitlesMenu,
|
||||
onToggleInfoMenu,
|
||||
onToggleOptionsMenu,
|
||||
...props
|
||||
}) => {
|
||||
const { chromecast } = useServices();
|
||||
|
|
@ -40,6 +41,9 @@ const ControlBar = ({
|
|||
const onInfoButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.infoMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onOptionsButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.optionsMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onPlayPauseButtonClick = React.useCallback(() => {
|
||||
if (paused) {
|
||||
if (typeof onPlayRequested === 'function') {
|
||||
|
|
@ -72,6 +76,11 @@ const ControlBar = ({
|
|||
onToggleInfoMenu();
|
||||
}
|
||||
}, [onToggleInfoMenu]);
|
||||
const onOptionsButtonClick = React.useCallback(() => {
|
||||
if (typeof onToggleOptionsMenu === 'function') {
|
||||
onToggleOptionsMenu();
|
||||
}
|
||||
}, [onToggleOptionsMenu]);
|
||||
const onChromecastButtonClick = React.useCallback(() => {
|
||||
chromecast.transport.requestSession();
|
||||
}, []);
|
||||
|
|
@ -133,6 +142,9 @@ const ControlBar = ({
|
|||
<Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}>
|
||||
<Icon className={styles['icon']} icon={'ic_videos'} />
|
||||
</Button>
|
||||
<Button className={styles['control-bar-button']} tabIndex={-1} onMouseDown={onOptionsButtonMouseDown} onClick={onOptionsButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_more'} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -156,7 +168,8 @@ ControlBar.propTypes = {
|
|||
onVolumeChangeRequested: PropTypes.func,
|
||||
onSeekRequested: PropTypes.func,
|
||||
onToggleSubtitlesMenu: PropTypes.func,
|
||||
onToggleInfoMenu: PropTypes.func
|
||||
onToggleInfoMenu: PropTypes.func,
|
||||
onToggleOptionsMenu: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports = ControlBar;
|
||||
|
|
|
|||
86
src/routes/Player/OptionsMenu/OptionsMenu.js
Normal file
86
src/routes/Player/OptionsMenu/OptionsMenu.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const Icon = require('@stremio/stremio-icons/dom');
|
||||
const { Button, useStreamingServer, useToast } = require('stremio/common');
|
||||
const styles = require('./styles');
|
||||
|
||||
const OptionsMenu = ({ className, stream }) => {
|
||||
const streamingServer = useStreamingServer();
|
||||
const toast = useToast();
|
||||
const streamUrl = React.useMemo(() => {
|
||||
return stream !== null ?
|
||||
typeof stream.url === 'string' ?
|
||||
stream.url
|
||||
:
|
||||
typeof stream.infoHash === 'string' &&
|
||||
typeof stream.fileIdx === 'number' &&
|
||||
typeof streamingServer.selected === 'object' &&
|
||||
typeof streamingServer.selected.transportUrl === 'string' ?
|
||||
`${streamingServer.selected.transportUrl}${stream.infoHash}/${stream.fileIdx}`
|
||||
:
|
||||
null
|
||||
:
|
||||
null;
|
||||
}, [stream, streamingServer]);
|
||||
const onCopyStreamButtonClick = React.useCallback(() => {
|
||||
if (streamUrl !== null) {
|
||||
navigator.clipboard.writeText(streamUrl)
|
||||
.then(() => {
|
||||
toast.show({
|
||||
type: 'success',
|
||||
title: 'Copied',
|
||||
message: 'Stream link was copied to your clipboard',
|
||||
timeout: 3000
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
toast.show({
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: 'Failed to copy stream link. Try to enable your browser permission.',
|
||||
timeout: 3000
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [streamUrl]);
|
||||
const onDownloadVideoButtonClick = React.useCallback(() => {
|
||||
if (streamUrl !== null) {
|
||||
window.open(streamUrl);
|
||||
}
|
||||
}, [streamUrl]);
|
||||
const onExternalPlayerButtonClick = React.useCallback(() => {
|
||||
if (streamUrl !== null) {
|
||||
window.open(`vlc://${encodeURIComponent(streamUrl)}`);
|
||||
}
|
||||
}, [streamUrl]);
|
||||
const onMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.optionsMenuClosePrevented = true;
|
||||
}, []);
|
||||
return (
|
||||
<div className={classnames(className, styles['options-menu-container'])} onMouseDown={onMouseDown}>
|
||||
<Button className={classnames(styles['option-container'], { 'disabled': stream === null })} disabled={stream === null} onClick={onCopyStreamButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_link'} />
|
||||
<div className={styles['label']}>Copy Stream Link</div>
|
||||
</Button>
|
||||
<Button className={classnames(styles['option-container'], { 'disabled': stream === null })} disabled={stream === null}onClick={onDownloadVideoButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_downloads'} />
|
||||
<div className={styles['label']}>Download Video</div>
|
||||
</Button>
|
||||
<Button className={classnames(styles['option-container'], { 'disabled': stream === null })} disabled={stream === null} onClick={onExternalPlayerButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_vlc'} />
|
||||
<div className={styles['label']}>Play in External Player</div>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
OptionsMenu.propTypes = {
|
||||
className: PropTypes.string,
|
||||
stream: PropTypes.object
|
||||
};
|
||||
|
||||
module.exports = OptionsMenu;
|
||||
5
src/routes/Player/OptionsMenu/index.js
Normal file
5
src/routes/Player/OptionsMenu/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const OptionsMenu = require('./OptionsMenu');
|
||||
|
||||
module.exports = OptionsMenu;
|
||||
37
src/routes/Player/OptionsMenu/styles.less
Normal file
37
src/routes/Player/OptionsMenu/styles.less
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
|
||||
.options-menu-container {
|
||||
width: 15rem;
|
||||
|
||||
.option-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 4rem;
|
||||
|
||||
.icon {
|
||||
flex: none;
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
margin: 1.3rem;
|
||||
fill: @color-surface-light5-90;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
max-height: 2.4em;
|
||||
font-weight: 400;
|
||||
color: @color-surface-light5-90;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: @color-background-light2;
|
||||
}
|
||||
|
||||
&:global(.disabled) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ const Icon = require('@stremio/stremio-icons/dom');
|
|||
const BufferingLoader = require('./BufferingLoader');
|
||||
const ControlBar = require('./ControlBar');
|
||||
const InfoMenu = require('./InfoMenu');
|
||||
const OptionsMenu = require('./OptionsMenu');
|
||||
const SubtitlesMenu = require('./SubtitlesMenu');
|
||||
const Video = require('./Video');
|
||||
const usePlayer = require('./usePlayer');
|
||||
|
|
@ -38,6 +39,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
|
||||
const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
|
||||
const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
|
||||
const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [videoState, setVideoState] = React.useReducer(
|
||||
(videoState, nextVideoState) => ({ ...videoState, ...nextVideoState }),
|
||||
|
|
@ -198,6 +200,9 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
if (!event.nativeEvent.infoMenuClosePrevented) {
|
||||
closeInfoMenu();
|
||||
}
|
||||
if (!event.nativeEvent.optionsMenuClosePrevented) {
|
||||
closeOptionsMenu();
|
||||
}
|
||||
}, []);
|
||||
const onContainerMouseMove = React.useCallback((event) => {
|
||||
setImmersed(false);
|
||||
|
|
@ -399,6 +404,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
case 'KeyS': {
|
||||
closeInfoMenu();
|
||||
closeOptionsMenu();
|
||||
if ((Array.isArray(videoState.subtitlesTracks) && videoState.subtitlesTracks.length > 0) ||
|
||||
(Array.isArray(videoState.extraSubtitlesTracks) && videoState.extraSubtitlesTracks.length > 0) ||
|
||||
(Array.isArray(videoState.audioTracks) && videoState.audioTracks.length > 0)) {
|
||||
|
|
@ -409,6 +415,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
case 'KeyI': {
|
||||
closeSubtitlesMenu();
|
||||
closeOptionsMenu();
|
||||
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
|
||||
toggleInfoMenu();
|
||||
}
|
||||
|
|
@ -418,6 +425,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
case 'Escape': {
|
||||
closeSubtitlesMenu();
|
||||
closeInfoMenu();
|
||||
closeOptionsMenu();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -437,7 +445,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen })}
|
||||
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen && !optionsMenuOpen })}
|
||||
onMouseDown={onContainerMouseDown}
|
||||
onMouseMove={onContainerMouseMove}
|
||||
onMouseOver={onContainerMouseMove}
|
||||
|
|
@ -513,6 +521,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
onSeekRequested={onSeekRequested}
|
||||
onToggleSubtitlesMenu={toggleSubtitlesMenu}
|
||||
onToggleInfoMenu={toggleInfoMenu}
|
||||
onToggleOptionsMenu={toggleOptionsMenu}
|
||||
onMouseMove={onBarMouseMove}
|
||||
onMouseOver={onBarMouseMove}
|
||||
/>
|
||||
|
|
@ -554,6 +563,15 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
optionsMenuOpen ?
|
||||
<OptionsMenu
|
||||
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||
stream={videoState.stream}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue