mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-20 02:22:09 +00:00
Merge branch 'development' of https://github.com/Stremio/stremio-web into feat/player-options-menu
This commit is contained in:
commit
c5d84bfe6b
39 changed files with 2591 additions and 201 deletions
BIN
favicons/favicon.ico
Normal file
BIN
favicons/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
images/icon_x192.png
Normal file
BIN
images/icon_x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
images/icon_x512.png
Normal file
BIN
images/icon_x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
images/maskable_icon_x192.png
Normal file
BIN
images/maskable_icon_x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5 KiB |
BIN
images/maskable_icon_x512.png
Normal file
BIN
images/maskable_icon_x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
40
manifest.json
Normal file
40
manifest.json
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"short_name": "Stremio",
|
||||
"name": "Stremio Web",
|
||||
"description": "Freedom To Stream",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicons/favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "images/icon_x192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "images/icon_x512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
},
|
||||
{
|
||||
"src": "images/maskable_icon_x192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "images/maskable_icon_x512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"start_url": "https://web.stremio.com",
|
||||
"scope": "https://web.stremio.com",
|
||||
"display": "standalone",
|
||||
"orientation": "natural",
|
||||
"theme_color": "#2a2843",
|
||||
"background_color": "#161523"
|
||||
}
|
||||
1706
package-lock.json
generated
1706
package-lock.json
generated
File diff suppressed because it is too large
Load diff
14
package.json
14
package.json
|
|
@ -14,16 +14,19 @@
|
|||
"dependencies": {
|
||||
"@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-icons": "3.0.5",
|
||||
"@stremio/stremio-video": "0.0.23",
|
||||
"@stremio/stremio-colors": "5.0.1",
|
||||
"@stremio/stremio-core-web": "0.44.6",
|
||||
"@stremio/stremio-icons": "4.0.0",
|
||||
"@stremio/stremio-video": "0.0.24",
|
||||
"a-color-picker": "1.2.1",
|
||||
"bowser": "2.11.0",
|
||||
"buffer": "6.0.3",
|
||||
"classnames": "2.3.1",
|
||||
"eventemitter3": "4.0.7",
|
||||
"filter-invalid-dom-props": "2.1.0",
|
||||
"langs": "^2.0.0",
|
||||
"hat": "0.0.3",
|
||||
"langs": "^2.0.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.intersection": "4.4.0",
|
||||
"lodash.isequal": "4.5.0",
|
||||
|
|
@ -61,6 +64,7 @@
|
|||
"terser-webpack-plugin": "5.2.4",
|
||||
"webpack": "5.61.0",
|
||||
"webpack-cli": "4.9.1",
|
||||
"webpack-dev-server": "4.7.4"
|
||||
"webpack-dev-server": "4.7.4",
|
||||
"workbox-webpack-plugin": "^6.5.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ const ServicesToaster = () => {
|
|||
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}
|
||||
|
|
|
|||
|
|
@ -31,13 +31,13 @@ const languageNames = require('./languageNames');
|
|||
const routesRegexp = require('./routesRegexp');
|
||||
const useAnimationFrame = require('./useAnimationFrame');
|
||||
const useBinaryState = require('./useBinaryState');
|
||||
const useDeepEqualMemo = require('./useDeepEqualMemo');
|
||||
const useFullscreen = require('./useFullscreen');
|
||||
const useLiveRef = require('./useLiveRef');
|
||||
const useModelState = require('./useModelState');
|
||||
const useOnScrollToBottom = require('./useOnScrollToBottom');
|
||||
const useProfile = require('./useProfile');
|
||||
const useStreamingServer = require('./useStreamingServer');
|
||||
const useTorrent = require('./useTorrent');
|
||||
|
||||
module.exports = {
|
||||
AddonDetailsModal,
|
||||
|
|
@ -74,11 +74,11 @@ module.exports = {
|
|||
routesRegexp,
|
||||
useAnimationFrame,
|
||||
useBinaryState,
|
||||
useDeepEqualMemo,
|
||||
useFullscreen,
|
||||
useLiveRef,
|
||||
useModelState,
|
||||
useOnScrollToBottom,
|
||||
useProfile,
|
||||
useStreamingServer
|
||||
useStreamingServer,
|
||||
useTorrent
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const isEqual = require('lodash.isequal');
|
||||
|
||||
const useDeepEqualMemo = (cb, deps) => {
|
||||
const valueRef = React.useRef();
|
||||
const mountedRef = React.useRef(false);
|
||||
const prevDepsRef = React.useRef(deps);
|
||||
if (!mountedRef.current || !isEqual(prevDepsRef.current, deps)) {
|
||||
valueRef.current = cb();
|
||||
prevDepsRef.current = deps;
|
||||
}
|
||||
React.useLayoutEffect(() => {
|
||||
mountedRef.current = true;
|
||||
}, []);
|
||||
return valueRef.current;
|
||||
};
|
||||
|
||||
module.exports = useDeepEqualMemo;
|
||||
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;
|
||||
|
|
@ -7,8 +7,10 @@
|
|||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-title" content="Stremio">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="<%= htmlWebpackPlugin.options.faviconsPath %>/icon-96.png">
|
||||
<link rel="apple-touch-icon" href="<%= htmlWebpackPlugin.options.faviconsPath %>/icon-96.png" />
|
||||
<title>Stremio - All you can watch!</title>
|
||||
<link rel="manifest" href="<%= htmlWebpackPlugin.options.manifestPath %>" />
|
||||
<meta name="theme-color" content="<%= htmlWebpackPlugin.options.themeColor %>">
|
||||
<link rel="apple-touch-icon" href="<%= htmlWebpackPlugin.options.imagesPath %>/icon_x192.png">
|
||||
<title>Stremio - Freedom to Stream</title>
|
||||
<%= htmlWebpackPlugin.tags.headTags %>
|
||||
</head>
|
||||
|
||||
|
|
|
|||
|
|
@ -17,3 +17,12 @@ const App = require('./App');
|
|||
|
||||
const root = ReactDOM.createRoot(document.getElementById('app'));
|
||||
root.render(<App />);
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/service-worker.js')
|
||||
.catch((registrationError) => {
|
||||
console.error('SW registration failed: ', registrationError);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ const ControlBar = ({
|
|||
duration,
|
||||
volume,
|
||||
muted,
|
||||
playbackSpeed,
|
||||
subtitlesTracks,
|
||||
audioTracks,
|
||||
metaItem,
|
||||
nextVideo,
|
||||
onPlayRequested,
|
||||
onPauseRequested,
|
||||
onMuteRequested,
|
||||
|
|
@ -29,6 +31,8 @@ const ControlBar = ({
|
|||
onSeekRequested,
|
||||
onToggleSubtitlesMenu,
|
||||
onToggleInfoMenu,
|
||||
onToggleSpeedMenu,
|
||||
onToggleVideosMenu,
|
||||
onToggleOptionsMenu,
|
||||
...props
|
||||
}) => {
|
||||
|
|
@ -41,6 +45,12 @@ const ControlBar = ({
|
|||
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;
|
||||
}, []);
|
||||
|
|
@ -55,6 +65,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') {
|
||||
|
|
@ -76,6 +95,16 @@ const ControlBar = ({
|
|||
onToggleInfoMenu();
|
||||
}
|
||||
}, [onToggleInfoMenu]);
|
||||
const onSpeedButtonClick = React.useCallback(() => {
|
||||
if (typeof onToggleSpeedMenu === 'function') {
|
||||
onToggleSpeedMenu();
|
||||
}
|
||||
}, [onToggleSpeedMenu]);
|
||||
const onVideosButtonClick = React.useCallback(() => {
|
||||
if (typeof onToggleVideosMenu === 'function') {
|
||||
onToggleVideosMenu();
|
||||
}
|
||||
}, [onToggleVideosMenu]);
|
||||
const onOptionsButtonClick = React.useCallback(() => {
|
||||
if (typeof onToggleOptionsMenu === 'function') {
|
||||
onToggleOptionsMenu();
|
||||
|
|
@ -105,6 +134,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']}
|
||||
|
|
@ -127,6 +164,9 @@ const ControlBar = ({
|
|||
<Icon className={styles['icon']} icon={'ic_more'} />
|
||||
</Button>
|
||||
<div className={classnames(styles['control-bar-buttons-menu-container'], { 'open': buttonsMenuOpen })}>
|
||||
<Button className={classnames(styles['control-bar-button'], { 'disabled': playbackSpeed === null })} tabIndex={-1} onMouseDown={onSpeedButtonMouseDown} onClick={onSpeedButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_speedometer'} />
|
||||
</Button>
|
||||
<Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}>
|
||||
<Icon className={styles['icon']} icon={'ic_network'} />
|
||||
</Button>
|
||||
|
|
@ -139,9 +179,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
|
||||
}
|
||||
<Button className={styles['control-bar-button']} tabIndex={-1} onMouseDown={onOptionsButtonMouseDown} onClick={onOptionsButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_more'} />
|
||||
</Button>
|
||||
|
|
@ -158,9 +203,11 @@ ControlBar.propTypes = {
|
|||
duration: PropTypes.number,
|
||||
volume: PropTypes.number,
|
||||
muted: PropTypes.bool,
|
||||
playbackSpeed: PropTypes.number,
|
||||
subtitlesTracks: PropTypes.array,
|
||||
audioTracks: PropTypes.array,
|
||||
metaItem: PropTypes.object,
|
||||
nextVideo: PropTypes.object,
|
||||
onPlayRequested: PropTypes.func,
|
||||
onPauseRequested: PropTypes.func,
|
||||
onMuteRequested: PropTypes.func,
|
||||
|
|
@ -169,7 +216,9 @@ ControlBar.propTypes = {
|
|||
onSeekRequested: PropTypes.func,
|
||||
onToggleSubtitlesMenu: PropTypes.func,
|
||||
onToggleInfoMenu: PropTypes.func,
|
||||
onToggleOptionsMenu: PropTypes.func
|
||||
onToggleSpeedMenu: PropTypes.func,
|
||||
onToggleVideosMenu: PropTypes.func,
|
||||
onToggleOptionsMenu: PropTypes.func,
|
||||
};
|
||||
|
||||
module.exports = ControlBar;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ const React = require('react');
|
|||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const debounce = require('lodash.debounce');
|
||||
const langs = require('langs');
|
||||
const { useRouteFocused } = require('stremio-router');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { HorizontalNavBar, Button, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common');
|
||||
|
|
@ -12,7 +13,9 @@ const BufferingLoader = require('./BufferingLoader');
|
|||
const ControlBar = require('./ControlBar');
|
||||
const InfoMenu = require('./InfoMenu');
|
||||
const OptionsMenu = require('./OptionsMenu');
|
||||
const VideosMenu = require('./VideosMenu');
|
||||
const SubtitlesMenu = require('./SubtitlesMenu');
|
||||
const SpeedMenu = require('./SpeedMenu');
|
||||
const Video = require('./Video');
|
||||
const usePlayer = require('./usePlayer');
|
||||
const useSettings = require('./useSettings');
|
||||
|
|
@ -37,9 +40,13 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
});
|
||||
const [immersed, setImmersed] = React.useState(true);
|
||||
const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
|
||||
const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false);
|
||||
const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
|
||||
const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
|
||||
const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false);
|
||||
const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
|
||||
const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false);
|
||||
const defaultSubtitlesSelected = React.useRef(false);
|
||||
const defaultAudioTrackSelected = React.useRef(false);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [videoState, setVideoState] = React.useReducer(
|
||||
(videoState, nextVideoState) => ({ ...videoState, ...nextVideoState }),
|
||||
|
|
@ -52,6 +59,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
buffering: null,
|
||||
volume: null,
|
||||
muted: null,
|
||||
playbackSpeed: null,
|
||||
audioTracks: [],
|
||||
selectedAudioTrackId: null,
|
||||
subtitlesTracks: [],
|
||||
|
|
@ -159,6 +167,9 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
const onSeekRequested = React.useCallback((time) => {
|
||||
dispatch({ type: 'setProp', propName: 'time', propValue: time });
|
||||
}, []);
|
||||
const onPlaybackSpeedChanged = React.useCallback((rate) => {
|
||||
dispatch({ type: 'setProp', propName: 'playbackSpeed', propValue: rate });
|
||||
}, []);
|
||||
const onSubtitlesTrackSelected = React.useCallback((id) => {
|
||||
dispatch({ type: 'setProp', propName: 'selectedSubtitlesTrackId', propValue: id });
|
||||
dispatch({ type: 'setProp', propName: 'selectedExtraSubtitlesTrackId', propValue: null });
|
||||
|
|
@ -194,14 +205,20 @@ 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.optionsMenuClosePrevented) {
|
||||
closeOptionsMenu();
|
||||
if (!event.nativeEvent.speedMenuClosePrevented) {
|
||||
closeSpeedMenu();
|
||||
}
|
||||
if (!event.nativeEvent.videosMenuClosePrevented) {
|
||||
closeVideosMenu();
|
||||
}
|
||||
}, []);
|
||||
const onContainerMouseMove = React.useCallback((event) => {
|
||||
|
|
@ -240,7 +257,10 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
[]
|
||||
},
|
||||
autoplay: true,
|
||||
time: player.libraryItem !== null && player.selected.streamRequest !== null && player.libraryItem.state.video_id === player.selected.streamRequest.id ?
|
||||
time: player.libraryItem !== null &&
|
||||
player.selected.streamRequest !== null &&
|
||||
player.selected.streamRequest.path !== null &&
|
||||
player.libraryItem.state.video_id === player.selected.streamRequest.path.id ?
|
||||
player.libraryItem.state.timeOffset
|
||||
:
|
||||
0,
|
||||
|
|
@ -313,6 +333,37 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
pausedChanged(videoState.paused);
|
||||
}
|
||||
}, [videoState.paused]);
|
||||
React.useEffect(() => {
|
||||
if (!defaultSubtitlesSelected.current) {
|
||||
const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
|
||||
|
||||
const subtitlesTrack = findTrackByLang(videoState.subtitlesTracks, settings.subtitlesLanguage);
|
||||
const extraSubtitlesTrack = findTrackByLang(videoState.extraSubtitlesTracks, settings.subtitlesLanguage);
|
||||
|
||||
if (subtitlesTrack && subtitlesTrack.id) {
|
||||
onSubtitlesTrackSelected(subtitlesTrack.id);
|
||||
defaultSubtitlesSelected.current = true;
|
||||
} else if (extraSubtitlesTrack && extraSubtitlesTrack.id) {
|
||||
onExtraSubtitlesTrackSelected(extraSubtitlesTrack.id);
|
||||
defaultSubtitlesSelected.current = true;
|
||||
}
|
||||
}
|
||||
}, [videoState.subtitlesTracks, videoState.extraSubtitlesTracks]);
|
||||
React.useEffect(() => {
|
||||
if (!defaultAudioTrackSelected.current) {
|
||||
const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
|
||||
const audioTrack = findTrackByLang(videoState.audioTracks, settings.audioLanguage);
|
||||
|
||||
if (audioTrack && audioTrack.id) {
|
||||
onAudioTrackSelected(audioTrack.id);
|
||||
defaultAudioTrackSelected.current = true;
|
||||
}
|
||||
}
|
||||
}, [videoState.audioTracks]);
|
||||
React.useEffect(() => {
|
||||
defaultSubtitlesSelected.current = false;
|
||||
defaultAudioTrackSelected.current = false;
|
||||
}, [videoState.stream]);
|
||||
React.useEffect(() => {
|
||||
if ((!Array.isArray(videoState.subtitlesTracks) || videoState.subtitlesTracks.length === 0) &&
|
||||
(!Array.isArray(videoState.extraSubtitlesTracks) || videoState.extraSubtitlesTracks.length === 0) &&
|
||||
|
|
@ -323,8 +374,14 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
React.useEffect(() => {
|
||||
if (player.metaItem === null || player.metaItem.type !== 'Ready') {
|
||||
closeInfoMenu();
|
||||
closeVideosMenu();
|
||||
}
|
||||
}, [player.metaItem]);
|
||||
React.useEffect(() => {
|
||||
if (videoState.playbackSpeed === null) {
|
||||
closeSpeedMenu();
|
||||
}
|
||||
}, [videoState.playbackSpeed]);
|
||||
React.useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
pushToLibrary();
|
||||
|
|
@ -334,6 +391,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);
|
||||
};
|
||||
|
|
@ -349,6 +408,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
chromecast.on('stateChanged', onChromecastServiceStateChange);
|
||||
onChromecastServiceStateChange();
|
||||
return () => {
|
||||
toast.removeFilter(toastFilter);
|
||||
chromecast.off('stateChanged', onChromecastServiceStateChange);
|
||||
if (chromecast.active) {
|
||||
chromecast.transport.off(
|
||||
|
|
@ -362,7 +422,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
const onKeyDown = (event) => {
|
||||
switch (event.code) {
|
||||
case 'Space': {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && videoState.paused !== null) {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen&& videoState.paused !== null) {
|
||||
if (videoState.paused) {
|
||||
onPlayRequested();
|
||||
} else {
|
||||
|
|
@ -373,7 +433,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
break;
|
||||
}
|
||||
case 'ArrowRight': {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.time !== null) {
|
||||
const seekTimeMultiplier = event.shiftKey ? 3 : 1;
|
||||
onSeekRequested(videoState.time + (settings.seekTimeDuration * seekTimeMultiplier));
|
||||
}
|
||||
|
|
@ -381,7 +441,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
break;
|
||||
}
|
||||
case 'ArrowLeft': {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.time !== null) {
|
||||
const seekTimeMultiplier = event.shiftKey ? 3 : 1;
|
||||
onSeekRequested(videoState.time - (settings.seekTimeDuration * seekTimeMultiplier));
|
||||
}
|
||||
|
|
@ -389,22 +449,24 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
break;
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && videoState.volume !== null) {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.volume !== null) {
|
||||
onVolumeChangeRequested(videoState.volume + 5);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'ArrowDown': {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && videoState.volume !== null) {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.volume !== null) {
|
||||
onVolumeChangeRequested(videoState.volume - 5);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyS': {
|
||||
closeInfoMenu();
|
||||
closeOptionsMenu();
|
||||
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)) {
|
||||
|
|
@ -414,18 +476,44 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
break;
|
||||
}
|
||||
case 'KeyI': {
|
||||
closeSubtitlesMenu();
|
||||
closeOptionsMenu();
|
||||
closeSubtitlesMenu();
|
||||
closeSpeedMenu();
|
||||
closeVideosMenu();
|
||||
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
|
||||
toggleInfoMenu();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyR': {
|
||||
closeOptionsMenu();
|
||||
closeInfoMenu();
|
||||
closeSubtitlesMenu();
|
||||
closeVideosMenu();
|
||||
if (videoState.playbackSpeed !== null) {
|
||||
toggleSpeedMenu();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyV': {
|
||||
closeOptionsMenu();
|
||||
closeInfoMenu();
|
||||
closeSubtitlesMenu();
|
||||
closeSpeedMenu();
|
||||
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
|
||||
toggleVideosMenu();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'Escape': {
|
||||
closeOptionsMenu();
|
||||
closeSubtitlesMenu();
|
||||
closeInfoMenu();
|
||||
closeOptionsMenu();
|
||||
closeSpeedMenu();
|
||||
closeVideosMenu();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -436,7 +524,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [player.metaItem, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, toggleSubtitlesMenu, toggleInfoMenu]);
|
||||
}, [player.metaItem, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, videosMenuOpen, speedMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, videoState.playbackSpeed, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu]);
|
||||
React.useLayoutEffect(() => {
|
||||
return () => {
|
||||
setImmersedDebounced.cancel();
|
||||
|
|
@ -445,7 +533,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen && !optionsMenuOpen })}
|
||||
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen && !speedMenuOpen && !videosMenuOpen && !optionsMenuOpen })}
|
||||
onMouseDown={onContainerMouseDown}
|
||||
onMouseMove={onContainerMouseMove}
|
||||
onMouseOver={onContainerMouseMove}
|
||||
|
|
@ -490,7 +578,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
null
|
||||
}
|
||||
{
|
||||
subtitlesMenuOpen || infoMenuOpen ?
|
||||
subtitlesMenuOpen || infoMenuOpen || videosMenuOpen || speedMenuOpen ?
|
||||
<div className={styles['layer']} />
|
||||
:
|
||||
null
|
||||
|
|
@ -510,18 +598,22 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
duration={videoState.duration}
|
||||
volume={videoState.volume}
|
||||
muted={videoState.muted}
|
||||
playbackSpeed={videoState.playbackSpeed}
|
||||
subtitlesTracks={videoState.subtitlesTracks.concat(videoState.extraSubtitlesTracks)}
|
||||
audioTracks={videoState.audioTracks}
|
||||
metaItem={player.metaItem}
|
||||
nextVideo={player.nextVideo}
|
||||
onPlayRequested={onPlayRequested}
|
||||
onPauseRequested={onPauseRequested}
|
||||
onMuteRequested={onMuteRequested}
|
||||
onUnmuteRequested={onUnmuteRequested}
|
||||
onVolumeChangeRequested={onVolumeChangeRequested}
|
||||
onSeekRequested={onSeekRequested}
|
||||
onToggleOptionsMenu={toggleOptionsMenu}
|
||||
onToggleSubtitlesMenu={toggleSubtitlesMenu}
|
||||
onToggleInfoMenu={toggleInfoMenu}
|
||||
onToggleOptionsMenu={toggleOptionsMenu}
|
||||
onToggleSpeedMenu={toggleSpeedMenu}
|
||||
onToggleVideosMenu={toggleVideosMenu}
|
||||
onMouseMove={onBarMouseMove}
|
||||
onMouseOver={onBarMouseMove}
|
||||
/>
|
||||
|
|
@ -563,6 +655,26 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
speedMenuOpen ?
|
||||
<SpeedMenu
|
||||
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||
playbackSpeed={videoState.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
|
||||
|
|
|
|||
33
src/routes/Player/SpeedMenu/Option/Option.js
Normal file
33
src/routes/Player/SpeedMenu/Option/Option.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const { Button } = require('stremio/common');
|
||||
const styles = require('./styles');
|
||||
|
||||
const OptionButton = ({ className, value, selected, onSelect }) => {
|
||||
const onClick = React.useCallback(() => {
|
||||
if (typeof onSelect === 'function') {
|
||||
onSelect(value);
|
||||
}
|
||||
}, [onSelect, value]);
|
||||
return (
|
||||
<Button
|
||||
className={classnames(className, styles['option'], { 'selected': selected })}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className={styles['label']}>{ value }x</div>
|
||||
<div className={styles['icon']} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
OptionButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.number,
|
||||
selected: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
module.exports = OptionButton;
|
||||
5
src/routes/Player/SpeedMenu/Option/index.js
Normal file
5
src/routes/Player/SpeedMenu/Option/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const Option = require('./Option');
|
||||
|
||||
module.exports = Option;
|
||||
38
src/routes/Player/SpeedMenu/Option/styles.less
Normal file
38
src/routes/Player/SpeedMenu/Option/styles.less
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 1.5em;
|
||||
|
||||
&:global(.selected) {
|
||||
background-color: @color-background;
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: @color-background-light2;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
font-weight: 400;
|
||||
color: @color-surface-light5-90;
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: none;
|
||||
display: none;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 100%;
|
||||
margin-left: 1rem;
|
||||
background-color: @color-accent3-90;
|
||||
}
|
||||
}
|
||||
48
src/routes/Player/SpeedMenu/SpeedMenu.js
Normal file
48
src/routes/Player/SpeedMenu/SpeedMenu.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const Option = require('./Option');
|
||||
const styles = require('./styles');
|
||||
|
||||
const RATES = Array.from(Array(8).keys(), (n) => n * 0.25 + 0.25).reverse();
|
||||
|
||||
const SpeedMenu = ({ className, playbackSpeed, onPlaybackSpeedChanged }) => {
|
||||
const onMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.speedMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onOptionSelect = React.useCallback((value) => {
|
||||
if (typeof onPlaybackSpeedChanged === 'function') {
|
||||
onPlaybackSpeedChanged(value);
|
||||
}
|
||||
}, [onPlaybackSpeedChanged]);
|
||||
return (
|
||||
<div className={classnames(className, styles['speed-menu-container'])} onMouseDown={onMouseDown}>
|
||||
<div className={styles['title']}>
|
||||
Playback Speed
|
||||
</div>
|
||||
<div className={styles['options-container']}>
|
||||
{
|
||||
RATES.map((rate) => (
|
||||
<Option
|
||||
className={styles['option']}
|
||||
key={rate}
|
||||
value={rate}
|
||||
selected={rate === playbackSpeed}
|
||||
onSelect={onOptionSelect}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SpeedMenu.propTypes = {
|
||||
className: PropTypes.string,
|
||||
playbackSpeed: PropTypes.number,
|
||||
onPlaybackSpeedChanged: PropTypes.func,
|
||||
};
|
||||
|
||||
module.exports = SpeedMenu;
|
||||
5
src/routes/Player/SpeedMenu/index.js
Normal file
5
src/routes/Player/SpeedMenu/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const SpeedMenu = require('./SpeedMenu');
|
||||
|
||||
module.exports = SpeedMenu;
|
||||
27
src/routes/Player/SpeedMenu/styles.less
Normal file
27
src/routes/Player/SpeedMenu/styles.less
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
|
||||
.speed-menu-container {
|
||||
width: 12rem;
|
||||
overflow: visible !important;
|
||||
|
||||
.title {
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
max-height: 2.4em;
|
||||
font-weight: 600;
|
||||
color: @color-surface-light5-90;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
flex: 0 1 auto;
|
||||
max-height: calc(3.2rem * 8);
|
||||
overflow-y: auto;
|
||||
|
||||
.option {
|
||||
height: 3.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
|
|||
const classnames = require('classnames');
|
||||
const debounce = require('lodash.debounce');
|
||||
const Icon = require('@stremio/stremio-icons/dom');
|
||||
const { Image, MainNavBars, MetaRow, MetaItem, useDeepEqualMemo, withCoreSuspender, getVisibleChildrenRange } = require('stremio/common');
|
||||
const { Image, MainNavBars, MetaRow, MetaItem, withCoreSuspender, getVisibleChildrenRange } = require('stremio/common');
|
||||
const useSearch = require('./useSearch');
|
||||
const styles = require('./styles');
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ const THRESHOLD = 100;
|
|||
|
||||
const Search = ({ queryParams }) => {
|
||||
const [search, loadSearchRows] = useSearch(queryParams);
|
||||
const query = useDeepEqualMemo(() => {
|
||||
const query = React.useMemo(() => {
|
||||
return search.selected !== null ?
|
||||
search.selected.extra.reduceRight((query, [name, value]) => {
|
||||
if (name === 'search') {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -27,6 +28,7 @@ const Settings = () => {
|
|||
subtitlesTextColorInput,
|
||||
subtitlesBackgroundColorInput,
|
||||
subtitlesOutlineColorInput,
|
||||
audioLanguageSelect,
|
||||
seekTimeDurationSelect,
|
||||
bingeWatchingCheckbox,
|
||||
playInBackgroundCheckbox,
|
||||
|
|
@ -93,10 +95,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 +146,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>
|
||||
{
|
||||
|
|
@ -304,6 +311,15 @@ const Settings = () => {
|
|||
{...subtitlesOutlineColorInput}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Audio Language</div>
|
||||
</div>
|
||||
<Multiselect
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...audioLanguageSelect}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Rewind & Fast-forward duration</div>
|
||||
|
|
@ -424,6 +440,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,31 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.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;
|
||||
white-space: nowrap;
|
||||
color: @color-secondaryvariant1-90;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { CONSTANTS, languageNames, useDeepEqualMemo } = require('stremio/common');
|
||||
const { CONSTANTS, languageNames } = require('stremio/common');
|
||||
|
||||
const useProfileSettingsInputs = (profile) => {
|
||||
const { core } = useServices();
|
||||
// TODO combine those useDeepEqualMemo in one
|
||||
const interfaceLanguageSelect = useDeepEqualMemo(() => ({
|
||||
// TODO combine those useMemo in one
|
||||
const interfaceLanguageSelect = React.useMemo(() => ({
|
||||
options: Object.keys(languageNames).map((code) => ({
|
||||
value: code,
|
||||
label: languageNames[code]
|
||||
|
|
@ -25,7 +26,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const subtitlesLanguageSelect = useDeepEqualMemo(() => ({
|
||||
const subtitlesLanguageSelect = React.useMemo(() => ({
|
||||
options: Object.keys(languageNames).map((code) => ({
|
||||
value: code,
|
||||
label: languageNames[code]
|
||||
|
|
@ -44,7 +45,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const subtitlesSizeSelect = useDeepEqualMemo(() => ({
|
||||
const subtitlesSizeSelect = React.useMemo(() => ({
|
||||
options: CONSTANTS.SUBTITLES_SIZES.map((size) => ({
|
||||
value: `${size}`,
|
||||
label: `${size}%`
|
||||
|
|
@ -66,7 +67,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const subtitlesTextColorInput = useDeepEqualMemo(() => ({
|
||||
const subtitlesTextColorInput = React.useMemo(() => ({
|
||||
value: profile.settings.subtitlesTextColor,
|
||||
onChange: (event) => {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -81,7 +82,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const subtitlesBackgroundColorInput = useDeepEqualMemo(() => ({
|
||||
const subtitlesBackgroundColorInput = React.useMemo(() => ({
|
||||
value: profile.settings.subtitlesBackgroundColor,
|
||||
onChange: (event) => {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -96,7 +97,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const subtitlesOutlineColorInput = useDeepEqualMemo(() => ({
|
||||
const subtitlesOutlineColorInput = React.useMemo(() => ({
|
||||
value: profile.settings.subtitlesOutlineColor,
|
||||
onChange: (event) => {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -111,7 +112,26 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const seekTimeDurationSelect = useDeepEqualMemo(() => ({
|
||||
const audioLanguageSelect = React.useMemo(() => ({
|
||||
options: Object.keys(languageNames).map((code) => ({
|
||||
value: code,
|
||||
label: languageNames[code]
|
||||
})),
|
||||
selected: [profile.settings.audioLanguage],
|
||||
onSelect: (event) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'UpdateSettings',
|
||||
args: {
|
||||
...profile.settings,
|
||||
audioLanguage: event.value
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const seekTimeDurationSelect = React.useMemo(() => ({
|
||||
options: CONSTANTS.SEEK_TIME_DURATIONS.map((size) => ({
|
||||
value: `${size}`,
|
||||
label: `${size / 1000} seconds`
|
||||
|
|
@ -133,7 +153,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const bingeWatchingCheckbox = useDeepEqualMemo(() => ({
|
||||
const bingeWatchingCheckbox = React.useMemo(() => ({
|
||||
checked: profile.settings.bingeWatching,
|
||||
onClick: () => {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -148,7 +168,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const playInBackgroundCheckbox = useDeepEqualMemo(() => ({
|
||||
const playInBackgroundCheckbox = React.useMemo(() => ({
|
||||
checked: profile.settings.playInBackground,
|
||||
onClick: () => {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -163,7 +183,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const playInExternalPlayerCheckbox = useDeepEqualMemo(() => ({
|
||||
const playInExternalPlayerCheckbox = React.useMemo(() => ({
|
||||
checked: profile.settings.playInExternalPlayer,
|
||||
onClick: () => {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -178,7 +198,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const hardwareDecodingCheckbox = useDeepEqualMemo(() => ({
|
||||
const hardwareDecodingCheckbox = React.useMemo(() => ({
|
||||
checked: profile.settings.hardwareDecoding,
|
||||
onClick: () => {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -193,7 +213,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const streamingServerUrlInput = useDeepEqualMemo(() => ({
|
||||
const streamingServerUrlInput = React.useMemo(() => ({
|
||||
value: profile.settings.streamingServerUrl,
|
||||
onChange: (value) => {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -215,6 +235,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
subtitlesTextColorInput,
|
||||
subtitlesBackgroundColorInput,
|
||||
subtitlesOutlineColorInput,
|
||||
audioLanguageSelect,
|
||||
seekTimeDurationSelect,
|
||||
bingeWatchingCheckbox,
|
||||
playInBackgroundCheckbox,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const isEqual = require('lodash.isequal');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { useDeepEqualMemo } = require('stremio/common');
|
||||
|
||||
const CACHE_SIZES = [0, 2147483648, 5368709120, 10737418240, null];
|
||||
|
||||
|
|
@ -53,8 +53,8 @@ const TORRENT_PROFILES = {
|
|||
|
||||
const useStreamingServerSettingsInputs = (streamingServer) => {
|
||||
const { core } = useServices();
|
||||
// TODO combine those useDeepEqualMemo in one
|
||||
const cacheSizeSelect = useDeepEqualMemo(() => {
|
||||
// TODO combine those useMemo in one
|
||||
const cacheSizeSelect = React.useMemo(() => {
|
||||
if (streamingServer.settings === null || streamingServer.settings.type !== 'Ready') {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -82,7 +82,7 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
|
|||
}
|
||||
};
|
||||
}, [streamingServer.settings]);
|
||||
const torrentProfileSelect = useDeepEqualMemo(() => {
|
||||
const torrentProfileSelect = React.useMemo(() => {
|
||||
if (streamingServer.settings === null || streamingServer.settings.type !== 'Ready') {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ function Chromecast() {
|
|||
function onTransportInitError(args) {
|
||||
console.error(args);
|
||||
active = false;
|
||||
error = new Error('Google Cast API not available');
|
||||
error = new Error('Google Cast API not available', { cause: args });
|
||||
starting = false;
|
||||
onStateChanged();
|
||||
transport = null;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const EventEmitter = require('eventemitter3');
|
||||
const hat = require('hat');
|
||||
|
||||
const MESSAGE_NAMESPACE = 'urn:x-cast:com.stremio';
|
||||
const CHUNK_SIZE = 20000;
|
||||
|
|
@ -33,7 +34,7 @@ const initialize = () => {
|
|||
|
||||
function ChromecastTransport() {
|
||||
const events = new EventEmitter();
|
||||
const chunks = [];
|
||||
const messages = {};
|
||||
|
||||
initialize()
|
||||
.then(() => {
|
||||
|
|
@ -59,26 +60,17 @@ function ChromecastTransport() {
|
|||
|
||||
function onMessage(_, message) {
|
||||
try {
|
||||
const { chunk, last } = JSON.parse(message);
|
||||
chunks.push(chunk);
|
||||
if (!last) {
|
||||
return;
|
||||
const { id, chunk, index, length } = JSON.parse(message);
|
||||
messages[id] = messages[id] || [];
|
||||
messages[id][index] = chunk;
|
||||
if (Object.keys(messages[id]).length === length) {
|
||||
const parsedMessage = JSON.parse(messages[id].join(''));
|
||||
delete messages[id];
|
||||
events.emit('message', parsedMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
chunks.splice(0, chunks.length);
|
||||
events.emit('message-error', error);
|
||||
return;
|
||||
}
|
||||
|
||||
let parsedMessage;
|
||||
try {
|
||||
parsedMessage = JSON.parse(chunks.splice(0, chunks.length).join(''));
|
||||
} catch (error) {
|
||||
events.emit('message-error', error);
|
||||
return;
|
||||
}
|
||||
|
||||
events.emit('message', parsedMessage);
|
||||
}
|
||||
function onApplicationStatusChanged(event) {
|
||||
events.emit(cast.framework.CastSession.APPLICATION_STATUS_CHANGED, event);
|
||||
|
|
@ -165,11 +157,13 @@ function ChromecastTransport() {
|
|||
const chunk = serializedMessage.slice(start, start + CHUNK_SIZE);
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
const id = hat();
|
||||
return Promise.all(chunks.map((chunk, index) => {
|
||||
return castSession.sendMessage(MESSAGE_NAMESPACE, {
|
||||
id,
|
||||
chunk,
|
||||
last: index === chunks.length - 1,
|
||||
index,
|
||||
length: chunks.length
|
||||
});
|
||||
}));
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ function Core(args) {
|
|||
function onTransportError(args) {
|
||||
console.error(args);
|
||||
active = false;
|
||||
error = new Error('Stremio Core Transport initialization failed');
|
||||
error = new Error('Stremio Core Transport initialization failed', { cause: args });
|
||||
starting = false;
|
||||
onStateChanged();
|
||||
transport = null;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ const webpack = require('webpack');
|
|||
const HtmlWebPackPlugin = require('html-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const WorkboxPlugin = require('workbox-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const colors = require('@stremio/stremio-colors');
|
||||
const pachageJson = require('./package.json');
|
||||
|
||||
const COMMIT_HASH = execSync('git rev-parse HEAD').toString().trim();
|
||||
|
|
@ -187,8 +189,18 @@ module.exports = (env, argv) => ({
|
|||
new CleanWebpackPlugin({
|
||||
cleanOnceBeforeBuildPatterns: ['*']
|
||||
}),
|
||||
argv.mode === 'production' &&
|
||||
new WorkboxPlugin.GenerateSW({
|
||||
maximumFileSizeToCacheInBytes: 20000000,
|
||||
clientsClaim: true,
|
||||
skipWaiting: true
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [{ from: 'favicons', to: `${COMMIT_HASH}/favicons` }]
|
||||
patterns: [
|
||||
{ from: 'favicons', to: `${COMMIT_HASH}/favicons` },
|
||||
{ from: 'images', to: `${COMMIT_HASH}/images` },
|
||||
{ from: 'manifest.json', to: `${COMMIT_HASH}/manifest.json` },
|
||||
]
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: `${COMMIT_HASH}/styles/[name].css`
|
||||
|
|
@ -197,7 +209,10 @@ module.exports = (env, argv) => ({
|
|||
template: './src/index.html',
|
||||
inject: false,
|
||||
scriptLoading: 'blocking',
|
||||
faviconsPath: `${COMMIT_HASH}/favicons`
|
||||
themeColor: colors.background,
|
||||
faviconsPath: `${COMMIT_HASH}/favicons`,
|
||||
imagesPath: `${COMMIT_HASH}/images`,
|
||||
manifestPath: `${COMMIT_HASH}/manifest.json`,
|
||||
})
|
||||
]
|
||||
].filter(Boolean)
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue