mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
Merge branch 'development' of https://github.com/Stremio/stremio-web into feat/player-playback-speed
This commit is contained in:
commit
b3fa52d5cc
19 changed files with 522 additions and 94 deletions
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -12,7 +12,7 @@
|
|||
"@babel/runtime": "7.16.0",
|
||||
"@sentry/browser": "6.13.3",
|
||||
"@stremio/stremio-colors": "4.0.1",
|
||||
"@stremio/stremio-core-web": "0.44.5",
|
||||
"@stremio/stremio-core-web": "0.44.6",
|
||||
"@stremio/stremio-icons": "4.0.0",
|
||||
"@stremio/stremio-video": "0.0.23",
|
||||
"a-color-picker": "1.2.1",
|
||||
|
|
@ -2617,9 +2617,9 @@
|
|||
"integrity": "sha512-yT3No1gIWKLV2BhQIeSgG94EzXxmEqXJLulO+pFpziqWNUbmmEKeE+nRvW5wtoIK4SLy+v0bLd0b6HBH3KFfWw=="
|
||||
},
|
||||
"node_modules/@stremio/stremio-core-web": {
|
||||
"version": "0.44.5",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.5.tgz",
|
||||
"integrity": "sha512-egKYHD7h8Q5CcybT5RcCGpcJDT13TVbW3fyUjcwv4McvOGWRW3iO8RMaZt/ZWbf10tXSaGA2rbmJ52tjwGWqyA==",
|
||||
"version": "0.44.6",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.6.tgz",
|
||||
"integrity": "sha512-Mxc6oRKgTuXU80JEacJIe4TphccZUJkyHTMUZnUx9sotVetGX+EJsyvr+HLKNMDGJHx5xcwGT/BUikdyQR/Lpw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.16.0"
|
||||
}
|
||||
|
|
@ -15757,9 +15757,9 @@
|
|||
"integrity": "sha512-yT3No1gIWKLV2BhQIeSgG94EzXxmEqXJLulO+pFpziqWNUbmmEKeE+nRvW5wtoIK4SLy+v0bLd0b6HBH3KFfWw=="
|
||||
},
|
||||
"@stremio/stremio-core-web": {
|
||||
"version": "0.44.5",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.5.tgz",
|
||||
"integrity": "sha512-egKYHD7h8Q5CcybT5RcCGpcJDT13TVbW3fyUjcwv4McvOGWRW3iO8RMaZt/ZWbf10tXSaGA2rbmJ52tjwGWqyA==",
|
||||
"version": "0.44.6",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.6.tgz",
|
||||
"integrity": "sha512-Mxc6oRKgTuXU80JEacJIe4TphccZUJkyHTMUZnUx9sotVetGX+EJsyvr+HLKNMDGJHx5xcwGT/BUikdyQR/Lpw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "7.16.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"@babel/runtime": "7.16.0",
|
||||
"@sentry/browser": "6.13.3",
|
||||
"@stremio/stremio-colors": "4.0.1",
|
||||
"@stremio/stremio-core-web": "0.44.5",
|
||||
"@stremio/stremio-core-web": "0.44.6",
|
||||
"@stremio/stremio-icons": "4.0.0",
|
||||
"@stremio/stremio-video": "0.0.23",
|
||||
"a-color-picker": "1.2.1",
|
||||
|
|
|
|||
|
|
@ -103,6 +103,12 @@ const App = () => {
|
|||
action: 'PullUserFromAPI'
|
||||
}
|
||||
});
|
||||
services.core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'SyncLibraryWithAPI'
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [initialized]);
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -15,11 +15,18 @@ const ServicesToaster = () => {
|
|||
break;
|
||||
}
|
||||
|
||||
if (args.source.event === 'LibrarySyncWithAPIPlanned' && args.source.args.uid === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
toast.show({
|
||||
type: 'error',
|
||||
title: args.source.event,
|
||||
message: args.error.message,
|
||||
timeout: 4000
|
||||
timeout: 4000,
|
||||
dataset: {
|
||||
type: 'CoreEvent'
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@ const { useServices } = require('stremio/services');
|
|||
const Button = require('stremio/common/Button');
|
||||
const useFullscreen = require('stremio/common/useFullscreen');
|
||||
const useProfile = require('stremio/common/useProfile');
|
||||
const useTorrent = require('stremio/common/useTorrent');
|
||||
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
|
||||
const styles = require('./styles');
|
||||
|
||||
const NavMenuContent = ({ onClick }) => {
|
||||
const { core } = useServices();
|
||||
const profile = useProfile();
|
||||
const { createTorrentFromMagnet } = useTorrent();
|
||||
const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen();
|
||||
const logoutButtonOnClick = React.useCallback(() => {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -23,6 +25,14 @@ const NavMenuContent = ({ onClick }) => {
|
|||
}
|
||||
});
|
||||
}, []);
|
||||
const onPlayMagnetLinkClick = React.useCallback(async () => {
|
||||
try {
|
||||
const clipboardText = await navigator.clipboard.readText();
|
||||
createTorrentFromMagnet(clipboardText);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<div className={classnames(styles['nav-menu-container'], 'animation-fade-in')} onClick={onClick}>
|
||||
<div className={styles['user-info-container']}>
|
||||
|
|
@ -57,11 +67,7 @@ const NavMenuContent = ({ onClick }) => {
|
|||
<Icon className={styles['icon']} icon={'ic_addons'} />
|
||||
<div className={styles['nav-menu-option-label']}>Addons</div>
|
||||
</Button>
|
||||
<Button className={styles['nav-menu-option-container']} title={'Remote Control'} disabled={true}>
|
||||
<Icon className={styles['icon']} icon={'ic_remote'} />
|
||||
<div className={styles['nav-menu-option-label']}>Remote Control</div>
|
||||
</Button>
|
||||
<Button className={styles['nav-menu-option-container']} title={'Play Magnet Link'} disabled={true}>
|
||||
<Button className={styles['nav-menu-option-container']} title={'Play Magnet Link'} onClick={onPlayMagnetLinkClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_magnet'} />
|
||||
<div className={styles['nav-menu-option-label']}>Play Magnet Link</div>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@
|
|||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const magnet = require('magnet-uri');
|
||||
const { useServices } = require('stremio/services');
|
||||
const Icon = require('@stremio/stremio-icons/dom');
|
||||
const { useRouteFocused } = require('stremio-router');
|
||||
const Button = require('stremio/common/Button');
|
||||
const TextInput = require('stremio/common/TextInput');
|
||||
const useTorrent = require('stremio/common/useTorrent');
|
||||
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
|
||||
const styles = require('./styles');
|
||||
|
||||
const SearchBar = ({ className, query, active }) => {
|
||||
const { core } = useServices();
|
||||
const routeFocused = useRouteFocused();
|
||||
const { createTorrentFromMagnet } = useTorrent();
|
||||
const searchInputRef = React.useRef(null);
|
||||
const searchBarOnClick = React.useCallback(() => {
|
||||
if (!active) {
|
||||
|
|
@ -22,17 +22,7 @@ const SearchBar = ({ className, query, active }) => {
|
|||
}, [active]);
|
||||
const queryInputOnChange = React.useCallback(() => {
|
||||
try {
|
||||
const parsed = magnet.decode(searchInputRef.current.value);
|
||||
if (parsed && typeof parsed.infoHash === 'string') {
|
||||
core.transport.dispatch({
|
||||
action: 'StreamingServer',
|
||||
args: {
|
||||
action: 'CreateTorrent',
|
||||
args: searchInputRef.current.value
|
||||
}
|
||||
});
|
||||
searchInputRef.current.value = '';
|
||||
}
|
||||
createTorrentFromMagnet(searchInputRef.current.value);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch { }
|
||||
}, []);
|
||||
|
|
@ -80,4 +70,17 @@ SearchBar.propTypes = {
|
|||
active: PropTypes.bool
|
||||
};
|
||||
|
||||
module.exports = SearchBar;
|
||||
const SearchBarFallback = ({ className }) => (
|
||||
<label className={classnames(className, styles['search-bar-container'])}>
|
||||
<div className={styles['search-input']}>
|
||||
<div className={styles['placeholder-label']}>Search or paste link</div>
|
||||
</div>
|
||||
<Button className={styles['submit-button-container']} tabIndex={-1}>
|
||||
<Icon className={styles['icon']} icon={'ic_search_link'} />
|
||||
</Button>
|
||||
</label>
|
||||
);
|
||||
|
||||
SearchBarFallback.propTypes = SearchBar.propTypes;
|
||||
|
||||
module.exports = withCoreSuspender(SearchBar, SearchBarFallback);
|
||||
|
|
|
|||
|
|
@ -28,32 +28,48 @@ const ToastProvider = ({ className, children }) => {
|
|||
clearTimeout(event.dataset.id);
|
||||
dispatch({ type: 'remove', id: event.dataset.id });
|
||||
}, []);
|
||||
const toast = React.useMemo(() => ({
|
||||
show: (item) => {
|
||||
const timeout = typeof item.timeout === 'number' && !isNaN(item.timeout) ?
|
||||
item.timeout
|
||||
:
|
||||
DEFAULT_TIMEOUT;
|
||||
const id = setTimeout(() => {
|
||||
dispatch({ type: 'remove', id });
|
||||
}, timeout);
|
||||
dispatch({
|
||||
type: 'add',
|
||||
item: {
|
||||
...item,
|
||||
id,
|
||||
dataset: {
|
||||
...item.dataset,
|
||||
id
|
||||
},
|
||||
onClose: itemOnClose
|
||||
const toast = React.useMemo(() => {
|
||||
const filters = [];
|
||||
return {
|
||||
addFilter: (filter) => {
|
||||
filters.push(filter);
|
||||
},
|
||||
removeFilter: (filter) => {
|
||||
const index = filters.indexOf(filter);
|
||||
if (index > -1) {
|
||||
filters.splice(index, 1);
|
||||
}
|
||||
});
|
||||
},
|
||||
clear: () => {
|
||||
dispatch({ type: 'clear' });
|
||||
}
|
||||
}), []);
|
||||
},
|
||||
show: (item) => {
|
||||
if (filters.some((filter) => filter(item))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = typeof item.timeout === 'number' && !isNaN(item.timeout) ?
|
||||
item.timeout
|
||||
:
|
||||
DEFAULT_TIMEOUT;
|
||||
const id = setTimeout(() => {
|
||||
dispatch({ type: 'remove', id });
|
||||
}, timeout);
|
||||
dispatch({
|
||||
type: 'add',
|
||||
item: {
|
||||
...item,
|
||||
id,
|
||||
dataset: {
|
||||
...item.dataset,
|
||||
id
|
||||
},
|
||||
onClose: itemOnClose
|
||||
}
|
||||
});
|
||||
},
|
||||
clear: () => {
|
||||
dispatch({ type: 'clear' });
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<ToastContext.Provider value={toast}>
|
||||
{container instanceof HTMLElement ? children : null}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ const useModelState = require('./useModelState');
|
|||
const useOnScrollToBottom = require('./useOnScrollToBottom');
|
||||
const useProfile = require('./useProfile');
|
||||
const useStreamingServer = require('./useStreamingServer');
|
||||
const useTorrent = require('./useTorrent');
|
||||
|
||||
module.exports = {
|
||||
AddonDetailsModal,
|
||||
|
|
@ -80,5 +81,6 @@ module.exports = {
|
|||
useModelState,
|
||||
useOnScrollToBottom,
|
||||
useProfile,
|
||||
useStreamingServer
|
||||
useStreamingServer,
|
||||
useTorrent
|
||||
};
|
||||
|
|
|
|||
50
src/common/useTorrent.js
Normal file
50
src/common/useTorrent.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const magnet = require('magnet-uri');
|
||||
const { useServices } = require('stremio/services');
|
||||
const useToast = require('stremio/common/Toast/useToast');
|
||||
const useStreamingServer = require('stremio/common/useStreamingServer');
|
||||
|
||||
const useTorrent = () => {
|
||||
const { core } = useServices();
|
||||
const streamingServer = useStreamingServer();
|
||||
const toast = useToast();
|
||||
const createTorrentTimeout = React.useRef(null);
|
||||
const createTorrentFromMagnet = React.useCallback((text) => {
|
||||
const parsed = magnet.decode(text);
|
||||
if (parsed && typeof parsed.infoHash === 'string') {
|
||||
core.transport.dispatch({
|
||||
action: 'StreamingServer',
|
||||
args: {
|
||||
action: 'CreateTorrent',
|
||||
args: text
|
||||
}
|
||||
});
|
||||
clearTimeout(createTorrentTimeout.current);
|
||||
createTorrentTimeout.current = setTimeout(() => {
|
||||
toast.show({
|
||||
type: 'error',
|
||||
title: 'It\'s taking a long time to get metadata from the torrent.',
|
||||
timeout: 10000
|
||||
});
|
||||
}, 10000);
|
||||
}
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
if (streamingServer.torrent !== null) {
|
||||
const [, { type }] = streamingServer.torrent;
|
||||
if (type === 'Ready') {
|
||||
clearTimeout(createTorrentTimeout.current);
|
||||
}
|
||||
}
|
||||
}, [streamingServer.torrent]);
|
||||
React.useEffect(() => {
|
||||
return () => clearTimeout(createTorrentTimeout.current);
|
||||
}, []);
|
||||
return {
|
||||
createTorrentFromMagnet
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = useTorrent;
|
||||
|
|
@ -4,32 +4,70 @@ const React = require('react');
|
|||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const Icon = require('@stremio/stremio-icons/dom');
|
||||
const { Button, Image } = require('stremio/common');
|
||||
const { Button, Image, Multiselect } = require('stremio/common');
|
||||
const { useServices } = require('stremio/services');
|
||||
const Stream = require('./Stream');
|
||||
const styles = require('./styles');
|
||||
|
||||
const ALL_ADDONS_KEY = 'ALL';
|
||||
|
||||
const StreamsList = ({ className, ...props }) => {
|
||||
const { core } = useServices();
|
||||
const streams = React.useMemo(() => {
|
||||
const [selectedAddon, setSelectedAddon] = React.useState(ALL_ADDONS_KEY);
|
||||
const onAddonSelected = React.useCallback((event) => {
|
||||
setSelectedAddon(event.value);
|
||||
}, []);
|
||||
const streamsByAddon = React.useMemo(() => {
|
||||
return props.streams
|
||||
.filter((streams) => streams.content.type === 'Ready')
|
||||
.map((streams) => {
|
||||
return streams.content.content.map((stream) => ({
|
||||
...stream,
|
||||
onClick: () => {
|
||||
core.transport.analytics({
|
||||
event: 'StreamClicked',
|
||||
args: {
|
||||
stream
|
||||
}
|
||||
});
|
||||
},
|
||||
addonName: streams.addon.manifest.name
|
||||
}));
|
||||
})
|
||||
.flat(1);
|
||||
.reduce((streamsByAddon, streams) => {
|
||||
streamsByAddon[streams.addon.transportUrl] = {
|
||||
addon: streams.addon,
|
||||
streams: streams.content.content.map((stream) => ({
|
||||
...stream,
|
||||
onClick: () => {
|
||||
core.transport.analytics({
|
||||
event: 'StreamClicked',
|
||||
args: {
|
||||
stream
|
||||
}
|
||||
});
|
||||
},
|
||||
addonName: streams.addon.manifest.name
|
||||
}))
|
||||
};
|
||||
|
||||
return streamsByAddon;
|
||||
}, {});
|
||||
}, [props.streams]);
|
||||
const filteredStreams = React.useMemo(() => {
|
||||
return selectedAddon === ALL_ADDONS_KEY ?
|
||||
Object.values(streamsByAddon).map(({ streams }) => streams).flat(1)
|
||||
:
|
||||
streamsByAddon[selectedAddon] ?
|
||||
streamsByAddon[selectedAddon].streams
|
||||
:
|
||||
[];
|
||||
}, [streamsByAddon, selectedAddon]);
|
||||
const selectableOptions = React.useMemo(() => {
|
||||
return {
|
||||
title: 'Select Addon',
|
||||
options: [
|
||||
{
|
||||
value: ALL_ADDONS_KEY,
|
||||
label: 'All',
|
||||
title: 'All'
|
||||
},
|
||||
...Object.keys(streamsByAddon).map((transportUrl) => ({
|
||||
value: transportUrl,
|
||||
label: streamsByAddon[transportUrl].addon.manifest.name,
|
||||
title: streamsByAddon[transportUrl].addon.manifest.name,
|
||||
}))
|
||||
],
|
||||
selected: [selectedAddon],
|
||||
onSelect: onAddonSelected
|
||||
};
|
||||
}, [streamsByAddon, selectedAddon]);
|
||||
return (
|
||||
<div className={classnames(className, styles['streams-list-container'])}>
|
||||
{
|
||||
|
|
@ -45,26 +83,37 @@ const StreamsList = ({ className, ...props }) => {
|
|||
<div className={styles['label']}>No streams were found!</div>
|
||||
</div>
|
||||
:
|
||||
streams.length === 0 ?
|
||||
filteredStreams.length === 0 ?
|
||||
<div className={styles['streams-container']}>
|
||||
<Stream.Placeholder />
|
||||
<Stream.Placeholder />
|
||||
</div>
|
||||
:
|
||||
<div className={styles['streams-container']}>
|
||||
{streams.map((stream, index) => (
|
||||
<Stream
|
||||
key={index}
|
||||
addonName={stream.addonName}
|
||||
name={stream.name}
|
||||
description={stream.description}
|
||||
thumbnail={stream.thumbnail}
|
||||
progress={stream.progress}
|
||||
deepLinks={stream.deepLinks}
|
||||
onClick={stream.onClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<React.Fragment>
|
||||
{
|
||||
Object.keys(streamsByAddon).length > 1 ?
|
||||
<Multiselect
|
||||
{...selectableOptions}
|
||||
className={styles['select-input-container']}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles['streams-container']}>
|
||||
{filteredStreams.map((stream, index) => (
|
||||
<Stream
|
||||
key={index}
|
||||
addonName={stream.addonName}
|
||||
name={stream.name}
|
||||
description={stream.description}
|
||||
thumbnail={stream.thumbnail}
|
||||
progress={stream.progress}
|
||||
deepLinks={stream.deepLinks}
|
||||
onClick={stream.onClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
<Button className={styles['install-button-container']} title={'Install Addons'} href={'#/addons'}>
|
||||
<Icon className={styles['icon']} icon={'ic_addons'} />
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@
|
|||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
@import (reference) '~stremio/common/screen-sizes.less';
|
||||
|
||||
:import('~stremio/common/Multiselect/styles.less') {
|
||||
multiselect-menu-container: menu-container;
|
||||
multiselect-label: label;
|
||||
multiselect-icon: icon;
|
||||
}
|
||||
|
||||
.streams-list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -35,6 +41,30 @@
|
|||
}
|
||||
}
|
||||
|
||||
.select-input-container {
|
||||
flex: 0 0 auto;
|
||||
height: 3.5rem;
|
||||
margin: 1em 1em 0 1em;
|
||||
background: none;
|
||||
|
||||
&:hover, &:focus, &:global(.active) {
|
||||
background-color: @color-background;
|
||||
}
|
||||
|
||||
& >.multiselect-label {
|
||||
color: @color-surface-light5-90;
|
||||
}
|
||||
|
||||
& >.multiselect-icon {
|
||||
fill: @color-surface-light5-90;
|
||||
}
|
||||
|
||||
.multiselect-menu-container {
|
||||
max-height: calc(3.2rem * 7);
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.streams-container {
|
||||
flex: 0 1 auto;
|
||||
align-self: stretch;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w
|
|||
core.transport.dispatch({
|
||||
action: 'MetaDetails',
|
||||
args: {
|
||||
action: 'MarkAsWatched',
|
||||
action: 'MarkVideoAsWatched',
|
||||
args: [id, !watched]
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const ControlBar = ({
|
|||
subtitlesTracks,
|
||||
audioTracks,
|
||||
metaItem,
|
||||
nextVideo,
|
||||
onPlayRequested,
|
||||
onPauseRequested,
|
||||
onMuteRequested,
|
||||
|
|
@ -31,6 +32,7 @@ const ControlBar = ({
|
|||
onToggleSubtitlesMenu,
|
||||
onToggleInfoMenu,
|
||||
onToggleSpeedMenu,
|
||||
onToggleVideosMenu,
|
||||
...props
|
||||
}) => {
|
||||
const { chromecast } = useServices();
|
||||
|
|
@ -45,6 +47,9 @@ const ControlBar = ({
|
|||
const onSpeedButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.speedMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onVideosButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.videosMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onPlayPauseButtonClick = React.useCallback(() => {
|
||||
if (paused) {
|
||||
if (typeof onPlayRequested === 'function') {
|
||||
|
|
@ -56,6 +61,15 @@ const ControlBar = ({
|
|||
}
|
||||
}
|
||||
}, [paused, onPlayRequested, onPauseRequested]);
|
||||
const onNextVideoButtonClick = React.useCallback(() => {
|
||||
if (nextVideo !== null && typeof nextVideo.deepLinks === 'object') {
|
||||
if (nextVideo.deepLinks.player !== null) {
|
||||
window.location.replace(nextVideo.deepLinks.player);
|
||||
} else if (nextVideo.deepLinks.metaDetailsStreams !== null) {
|
||||
window.location.replace(nextVideo.deepLinks.metaDetailsStreams);
|
||||
}
|
||||
}
|
||||
}, [nextVideo]);
|
||||
const onMuteButtonClick = React.useCallback(() => {
|
||||
if (muted) {
|
||||
if (typeof onUnmuteRequested === 'function') {
|
||||
|
|
@ -82,6 +96,11 @@ const ControlBar = ({
|
|||
onToggleSpeedMenu();
|
||||
}
|
||||
}, [onToggleSpeedMenu]);
|
||||
const onVideosButtonClick = React.useCallback(() => {
|
||||
if (typeof onToggleVideosMenu === 'function') {
|
||||
onToggleVideosMenu();
|
||||
}
|
||||
}, [onToggleVideosMenu]);
|
||||
const onChromecastButtonClick = React.useCallback(() => {
|
||||
chromecast.transport.requestSession();
|
||||
}, []);
|
||||
|
|
@ -106,6 +125,14 @@ const ControlBar = ({
|
|||
<Button className={classnames(styles['control-bar-button'], { 'disabled': typeof paused !== 'boolean' })} title={paused ? 'Play' : 'Pause'} tabIndex={-1} onClick={onPlayPauseButtonClick}>
|
||||
<Icon className={styles['icon']} icon={typeof paused !== 'boolean' || paused ? 'ic_play' : 'ic_pause'} />
|
||||
</Button>
|
||||
{
|
||||
nextVideo !== null ?
|
||||
<Button className={classnames(styles['control-bar-button'])} title={'Next Video'} tabIndex={-1} onClick={onNextVideoButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_play_next'} />
|
||||
</Button>
|
||||
:
|
||||
null
|
||||
}
|
||||
<Button className={classnames(styles['control-bar-button'], { 'disabled': typeof muted !== 'boolean' })} title={muted ? 'Unmute' : 'Mute'} tabIndex={-1} onClick={onMuteButtonClick}>
|
||||
<Icon
|
||||
className={styles['icon']}
|
||||
|
|
@ -143,9 +170,14 @@ const ControlBar = ({
|
|||
<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={onSubtitlesButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_sub'} />
|
||||
</Button>
|
||||
<Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}>
|
||||
<Icon className={styles['icon']} icon={'ic_videos'} />
|
||||
</Button>
|
||||
{
|
||||
metaItem?.content?.videos?.length > 0 ?
|
||||
<Button className={styles['control-bar-button']} tabIndex={-1} onMouseDown={onVideosButtonMouseDown} onClick={onVideosButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_videos'} />
|
||||
</Button>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -163,6 +195,7 @@ ControlBar.propTypes = {
|
|||
subtitlesTracks: PropTypes.array,
|
||||
audioTracks: PropTypes.array,
|
||||
metaItem: PropTypes.object,
|
||||
nextVideo: PropTypes.object,
|
||||
onPlayRequested: PropTypes.func,
|
||||
onPauseRequested: PropTypes.func,
|
||||
onMuteRequested: PropTypes.func,
|
||||
|
|
@ -171,7 +204,8 @@ ControlBar.propTypes = {
|
|||
onSeekRequested: PropTypes.func,
|
||||
onToggleSubtitlesMenu: PropTypes.func,
|
||||
onToggleInfoMenu: PropTypes.func,
|
||||
onToggleSpeedMenu: PropTypes.func
|
||||
onToggleSpeedMenu: PropTypes.func,
|
||||
onToggleVideosMenu: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports = ControlBar;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const Icon = require('@stremio/stremio-icons/dom');
|
|||
const BufferingLoader = require('./BufferingLoader');
|
||||
const ControlBar = require('./ControlBar');
|
||||
const InfoMenu = require('./InfoMenu');
|
||||
const VideosMenu = require('./VideosMenu');
|
||||
const SubtitlesMenu = require('./SubtitlesMenu');
|
||||
const SpeedMenu = require('./SpeedMenu');
|
||||
const Video = require('./Video');
|
||||
|
|
@ -40,6 +41,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
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 [error, setError] = React.useState(null);
|
||||
const [videoState, setVideoState] = React.useReducer(
|
||||
(videoState, nextVideoState) => ({ ...videoState, ...nextVideoState }),
|
||||
|
|
@ -207,6 +209,9 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
if (!event.nativeEvent.speedMenuClosePrevented) {
|
||||
closeSpeedMenu();
|
||||
}
|
||||
if (!event.nativeEvent.videosMenuClosePrevented) {
|
||||
closeVideosMenu();
|
||||
}
|
||||
}, []);
|
||||
const onContainerMouseMove = React.useCallback((event) => {
|
||||
setImmersed(false);
|
||||
|
|
@ -327,6 +332,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
React.useEffect(() => {
|
||||
if (player.metaItem === null || player.metaItem.type !== 'Ready') {
|
||||
closeInfoMenu();
|
||||
closeVideosMenu();
|
||||
}
|
||||
}, [player.metaItem]);
|
||||
React.useEffect(() => {
|
||||
|
|
@ -343,6 +349,8 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
};
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
const toastFilter = (item) => item?.dataset?.type === 'CoreEvent';
|
||||
toast.addFilter(toastFilter);
|
||||
const onCastStateChange = () => {
|
||||
setCasting(chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED);
|
||||
};
|
||||
|
|
@ -358,6 +366,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
chromecast.on('stateChanged', onChromecastServiceStateChange);
|
||||
onChromecastServiceStateChange();
|
||||
return () => {
|
||||
toast.removeFilter(toastFilter);
|
||||
chromecast.off('stateChanged', onChromecastServiceStateChange);
|
||||
if (chromecast.active) {
|
||||
chromecast.transport.off(
|
||||
|
|
@ -414,6 +423,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
case 'KeyS': {
|
||||
closeInfoMenu();
|
||||
closeSpeedMenu();
|
||||
closeVideosMenu();
|
||||
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)) {
|
||||
|
|
@ -425,6 +435,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
case 'KeyI': {
|
||||
closeSubtitlesMenu();
|
||||
closeSpeedMenu();
|
||||
closeVideosMenu();
|
||||
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
|
||||
toggleInfoMenu();
|
||||
}
|
||||
|
|
@ -440,10 +451,20 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
|
||||
break;
|
||||
}
|
||||
case 'KeyV': {
|
||||
closeInfoMenu();
|
||||
closeSubtitlesMenu();
|
||||
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
|
||||
toggleVideosMenu();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'Escape': {
|
||||
closeSubtitlesMenu();
|
||||
closeInfoMenu();
|
||||
closeSpeedMenu();
|
||||
closeVideosMenu();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -454,7 +475,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [player.metaItem, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, speedMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, toggleSubtitlesMenu, toggleInfoMenu]);
|
||||
}, [player.metaItem, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, speedMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu]);
|
||||
React.useLayoutEffect(() => {
|
||||
return () => {
|
||||
setImmersedDebounced.cancel();
|
||||
|
|
@ -463,7 +484,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen && !speedMenuOpen })}
|
||||
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen && !speedMenuOpen && !videosMenuOpen })}
|
||||
onMouseDown={onContainerMouseDown}
|
||||
onMouseMove={onContainerMouseMove}
|
||||
onMouseOver={onContainerMouseMove}
|
||||
|
|
@ -532,6 +553,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
subtitlesTracks={videoState.subtitlesTracks.concat(videoState.extraSubtitlesTracks)}
|
||||
audioTracks={videoState.audioTracks}
|
||||
metaItem={player.metaItem}
|
||||
nextVideo={player.nextVideo}
|
||||
onPlayRequested={onPlayRequested}
|
||||
onPauseRequested={onPauseRequested}
|
||||
onMuteRequested={onMuteRequested}
|
||||
|
|
@ -541,6 +563,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
onToggleSubtitlesMenu={toggleSubtitlesMenu}
|
||||
onToggleInfoMenu={toggleInfoMenu}
|
||||
onToggleSpeedMenu={toggleSpeedMenu}
|
||||
onToggleVideosMenu={toggleVideosMenu}
|
||||
onMouseMove={onBarMouseMove}
|
||||
onMouseOver={onBarMouseMove}
|
||||
/>
|
||||
|
|
@ -592,6 +615,16 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
videosMenuOpen ?
|
||||
<VideosMenu
|
||||
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||
metaItem={player.metaItem !== null && player.metaItem.type === 'Ready' ? player.metaItem.content : null}
|
||||
seriesInfo={player.seriesInfo}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
51
src/routes/Player/VideosMenu/VideosMenu.js
Normal file
51
src/routes/Player/VideosMenu/VideosMenu.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const Video = require('../../MetaDetails/VideosList/Video');
|
||||
const styles = require('./styles');
|
||||
|
||||
const VideosMenu = ({ className, metaItem, seriesInfo }) => {
|
||||
const onMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.videosMenuClosePrevented = true;
|
||||
}, []);
|
||||
const videos = React.useMemo(() => {
|
||||
return seriesInfo && typeof seriesInfo.season === 'number' && Array.isArray(metaItem.videos) ?
|
||||
metaItem.videos.filter(({ season }) => season === seriesInfo.season)
|
||||
:
|
||||
metaItem.videos;
|
||||
}, [metaItem, seriesInfo]);
|
||||
return (
|
||||
<div className={classnames(className, styles['videos-menu-container'])} onMouseDown={onMouseDown}>
|
||||
{
|
||||
videos.map((video, index) => (
|
||||
<Video
|
||||
key={index}
|
||||
id={video.id}
|
||||
title={video.title}
|
||||
thumbnail={video.thumbnail}
|
||||
episode={video.episode}
|
||||
released={video.released}
|
||||
upcoming={video.upcoming}
|
||||
watched={video.watched}
|
||||
progress={video.progress}
|
||||
deepLinks={video.deepLinks}
|
||||
scheduled={video.scheduled}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
VideosMenu.propTypes = {
|
||||
className: PropTypes.string,
|
||||
metaItem: PropTypes.object,
|
||||
seriesInfo: PropTypes.shape({
|
||||
season: PropTypes.number,
|
||||
episode: PropTypes.number,
|
||||
}),
|
||||
};
|
||||
|
||||
module.exports = VideosMenu;
|
||||
5
src/routes/Player/VideosMenu/index.js
Normal file
5
src/routes/Player/VideosMenu/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const VideosMenu = require('./VideosMenu');
|
||||
|
||||
module.exports = VideosMenu;
|
||||
5
src/routes/Player/VideosMenu/styles.less
Normal file
5
src/routes/Player/VideosMenu/styles.less
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
.videos-menu-container {
|
||||
width: 30rem;
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ const styles = require('./styles');
|
|||
const GENERAL_SECTION = 'general';
|
||||
const PLAYER_SECTION = 'player';
|
||||
const STREAMING_SECTION = 'streaming';
|
||||
const SHORTCUTS_SECTION = 'shortcuts';
|
||||
|
||||
const Settings = () => {
|
||||
const { core } = useServices();
|
||||
|
|
@ -93,10 +94,12 @@ const Settings = () => {
|
|||
const generalSectionRef = React.useRef(null);
|
||||
const playerSectionRef = React.useRef(null);
|
||||
const streamingServerSectionRef = React.useRef(null);
|
||||
const shortcutsSectionRef = React.useRef(null);
|
||||
const sections = React.useMemo(() => ([
|
||||
{ ref: generalSectionRef, id: GENERAL_SECTION },
|
||||
{ ref: playerSectionRef, id: PLAYER_SECTION },
|
||||
{ ref: streamingServerSectionRef, id: STREAMING_SECTION },
|
||||
{ ref: shortcutsSectionRef, id: SHORTCUTS_SECTION },
|
||||
]), []);
|
||||
const [selectedSectionId, setSelectedSectionId] = React.useState(GENERAL_SECTION);
|
||||
const updateSelectedSectionId = React.useCallback(() => {
|
||||
|
|
@ -142,6 +145,9 @@ const Settings = () => {
|
|||
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === STREAMING_SECTION })} title={'Streaming server'} data-section={STREAMING_SECTION} onClick={sideMenuButtonOnClick}>
|
||||
Streaming server
|
||||
</Button>
|
||||
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === SHORTCUTS_SECTION })} title={'Shortcuts'} data-section={SHORTCUTS_SECTION} onClick={sideMenuButtonOnClick}>
|
||||
Shortcuts
|
||||
</Button>
|
||||
<div className={styles['spacing']} />
|
||||
<div className={styles['version-info-label']} title={process.env.VERSION}>App Version: {process.env.VERSION}</div>
|
||||
{
|
||||
|
|
@ -424,6 +430,107 @@ const Settings = () => {
|
|||
null
|
||||
}
|
||||
</div>
|
||||
<div ref={shortcutsSectionRef} className={styles['section-container']}>
|
||||
<div className={styles['section-title']}>Shortcuts</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Play / Pause</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>Space</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Seek Forward</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>→</kbd>
|
||||
<div className={styles['label']}>or</div>
|
||||
<kbd>⇧ Shift</kbd>
|
||||
<div className={styles['label']}>+</div>
|
||||
<kbd>→</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Seek Backward</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>←</kbd>
|
||||
<div className={styles['label']}>or</div>
|
||||
<kbd>⇧ Shift</kbd>
|
||||
<div className={styles['label']}>+</div>
|
||||
<kbd>←</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Volume Up</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>↑</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Volume Down</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>↓</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Toggle Subtitles Menu</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>S</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Toggle Info Menu</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>I</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Toggle Fullscreen</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>F</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Navigate Between Menus</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>1</kbd>
|
||||
<div className={styles['label']}>to</div>
|
||||
<kbd>5</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Go to Search</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>0</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Close Menu or Modal</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>Esc</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -315,6 +315,30 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.shortcut-container {
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
|
||||
kbd {
|
||||
flex: 0 1 auto;
|
||||
height: 2.5rem;
|
||||
min-width: 2.5rem;
|
||||
line-height: 2.5rem;
|
||||
padding: 0 1rem;
|
||||
font-weight: 500;
|
||||
color: @color-secondaryvariant1-90;
|
||||
border-radius: 0.25em;
|
||||
box-shadow: 0 4px 0 1px @color-background-40;
|
||||
background-color: @color-background;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 0 1rem;
|
||||
color: @color-secondaryvariant1-90;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue