Merge branch 'development' of github.com:Stremio/stremio-web into server-notification

This commit is contained in:
svetlagasheva 2021-08-04 10:26:38 +03:00
commit 59a2b7e3ca
11 changed files with 153 additions and 18 deletions

6
package-lock.json generated
View file

@ -8117,9 +8117,9 @@
"integrity": "sha512-yT3No1gIWKLV2BhQIeSgG94EzXxmEqXJLulO+pFpziqWNUbmmEKeE+nRvW5wtoIK4SLy+v0bLd0b6HBH3KFfWw==" "integrity": "sha512-yT3No1gIWKLV2BhQIeSgG94EzXxmEqXJLulO+pFpziqWNUbmmEKeE+nRvW5wtoIK4SLy+v0bLd0b6HBH3KFfWw=="
}, },
"@stremio/stremio-core-web": { "@stremio/stremio-core-web": {
"version": "0.22.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.22.0.tgz", "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.23.0.tgz",
"integrity": "sha512-PqvIXKwYmRYwzh0BFCEb9orZ3533OVLmLNbV1mCf9dmHG+BX+mREv9IB5+/yJZal2GMLnEBPfgeNxvIDYU6sww==", "integrity": "sha512-eZyBEWuB90y6lTG47Y4lKUnlM4bpVhmKh96DUHMPtMKOsyHRutOkuKXYjAwP0WjCbe57/vbalI04Fout7SZwWg==",
"requires": { "requires": {
"@babel/runtime": "7.10.0" "@babel/runtime": "7.10.0"
}, },

View file

@ -16,7 +16,7 @@
"@babel/runtime": "7.12.5", "@babel/runtime": "7.12.5",
"@sentry/browser": "5.11.1", "@sentry/browser": "5.11.1",
"@stremio/stremio-colors": "4.0.1", "@stremio/stremio-colors": "4.0.1",
"@stremio/stremio-core-web": "0.22.0", "@stremio/stremio-core-web": "0.23.0",
"@stremio/stremio-icons": "3.0.5", "@stremio/stremio-icons": "3.0.5",
"@stremio/stremio-video": "0.0.7", "@stremio/stremio-video": "0.0.7",
"a-color-picker": "1.2.1", "a-color-picker": "1.2.1",

View file

@ -3,6 +3,7 @@
const CHROMECAST_RECEIVER_APP_ID = '1634F54B'; const CHROMECAST_RECEIVER_APP_ID = '1634F54B';
const SUBTITLES_SIZES = [75, 100, 125, 150, 175, 200, 250]; const SUBTITLES_SIZES = [75, 100, 125, 150, 175, 200, 250];
const SUBTITLES_FONTS = ['Roboto', 'Arial', 'Halvetica', 'Times New Roman', 'Verdana', 'Courier', 'Lucida Console', 'sans-serif', 'serif', 'monospace']; const SUBTITLES_FONTS = ['Roboto', 'Arial', 'Halvetica', 'Times New Roman', 'Verdana', 'Courier', 'Lucida Console', 'sans-serif', 'serif', 'monospace'];
const SEEK_TIME_DURATIONS = [5000, 10000, 15000, 20000, 25000, 30000];
const CATALOG_PREVIEW_SIZE = 10; const CATALOG_PREVIEW_SIZE = 10;
const CATALOG_PAGE_SIZE = 100; const CATALOG_PAGE_SIZE = 100;
const NONE_EXTRA_VALUE = 'None'; const NONE_EXTRA_VALUE = 'None';
@ -28,6 +29,7 @@ module.exports = {
CHROMECAST_RECEIVER_APP_ID, CHROMECAST_RECEIVER_APP_ID,
SUBTITLES_SIZES, SUBTITLES_SIZES,
SUBTITLES_FONTS, SUBTITLES_FONTS,
SEEK_TIME_DURATIONS,
CATALOG_PREVIEW_SIZE, CATALOG_PREVIEW_SIZE,
CATALOG_PAGE_SIZE, CATALOG_PAGE_SIZE,
NONE_EXTRA_VALUE, NONE_EXTRA_VALUE,

View file

@ -23,7 +23,7 @@ const ALLOWED_LINK_REDIRECTS = [
routesRegexp.metadetails.regexp routesRegexp.metadetails.regexp
]; ];
const MetaPreview = ({ className, compact, name, logo, background, runtime, releaseInfo, released, description, links, trailerStreams, inLibrary, toggleInLibrary }) => { const MetaPreview = ({ className, compact, name, logo, background, runtime, releaseInfo, released, description, deepLinks, links, trailerStreams, inLibrary, toggleInLibrary }) => {
const [shareModalOpen, openShareModal, closeShareModal] = useBinaryState(false); const [shareModalOpen, openShareModal, closeShareModal] = useBinaryState(false);
const linksGroups = React.useMemo(() => { const linksGroups = React.useMemo(() => {
return Array.isArray(links) ? return Array.isArray(links) ?
@ -70,6 +70,21 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
: :
new Map(); new Map();
}, [links]); }, [links]);
const showHref = React.useMemo(() => {
return deepLinks ?
typeof deepLinks.player === 'string' ?
deepLinks.player
:
typeof deepLinks.metaDetailsStreams === 'string' ?
deepLinks.metaDetailsStreams
:
typeof deepLinks.metaDetailsVideos === 'string' ?
deepLinks.metaDetailsVideos
:
null
:
null;
}, [deepLinks]);
const trailerHref = React.useMemo(() => { const trailerHref = React.useMemo(() => {
if (!Array.isArray(trailerStreams) || trailerStreams.length === 0) { if (!Array.isArray(trailerStreams) || trailerStreams.length === 0) {
return null; return null;
@ -195,7 +210,19 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
null null
} }
{ {
linksGroups.has(CONSTANTS.SHARE_LINK_CATEGORY) ? typeof showHref === 'string' && compact ?
<ActionButton
className={styles['action-button']}
icon={'ic_play'}
label={'Show'}
tabIndex={compact ? -1 : 0}
href={showHref}
/>
:
null
}
{
linksGroups.has(CONSTANTS.SHARE_LINK_CATEGORY) && !compact ?
<React.Fragment> <React.Fragment>
<ActionButton <ActionButton
className={styles['action-button']} className={styles['action-button']}
@ -236,6 +263,11 @@ MetaPreview.propTypes = {
releaseInfo: PropTypes.string, releaseInfo: PropTypes.string,
released: PropTypes.instanceOf(Date), released: PropTypes.instanceOf(Date),
description: PropTypes.string, description: PropTypes.string,
deepLinks: PropTypes.shape({
metaDetailsVideos: PropTypes.string,
metaDetailsStreams: PropTypes.string,
player: PropTypes.string
}),
links: PropTypes.arrayOf(PropTypes.shape({ links: PropTypes.arrayOf(PropTypes.shape({
category: PropTypes.string, category: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,

View file

@ -182,6 +182,7 @@ const Addons = ({ urlParams, queryParams }) => {
className={styles['addon-url-input']} className={styles['addon-url-input']}
type={'text'} type={'text'}
placeholder={'Paste addon URL'} placeholder={'Paste addon URL'}
autoFocus={true}
onSubmit={addAddonOnSubmit} onSubmit={addAddonOnSubmit}
/> />
</ModalDialog> </ModalDialog>

View file

@ -165,6 +165,7 @@ const Discover = ({ urlParams, queryParams }) => {
releaseInfo={selectedMetaItem.releaseInfo} releaseInfo={selectedMetaItem.releaseInfo}
released={selectedMetaItem.released} released={selectedMetaItem.released}
description={selectedMetaItem.description} description={selectedMetaItem.description}
deepLinks={selectedMetaItem.deepLinks}
trailerStreams={selectedMetaItem.trailerStreams} trailerStreams={selectedMetaItem.trailerStreams}
inLibrary={selectedMetaItem.inLibrary} inLibrary={selectedMetaItem.inLibrary}
toggleInLibrary={selectedMetaItem.inLibrary ? removeFromLibrary : addToLibrary} toggleInLibrary={selectedMetaItem.inLibrary ? removeFromLibrary : addToLibrary}

View file

@ -6,13 +6,15 @@ const classnames = require('classnames');
const debounce = require('lodash.debounce'); const debounce = require('lodash.debounce');
const { useRouteFocused } = require('stremio-router'); const { useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services'); const { useServices } = require('stremio/services');
const { HorizontalNavBar, useDeepEqualEffect, useFullscreen, useBinaryState, useToast, useStreamingServer } = require('stremio/common'); const { HorizontalNavBar, Button, useDeepEqualEffect, useFullscreen, useBinaryState, useToast, useStreamingServer } = require('stremio/common');
const Icon = require('@stremio/stremio-icons/dom');
const BufferingLoader = require('./BufferingLoader'); const BufferingLoader = require('./BufferingLoader');
const ControlBar = require('./ControlBar'); const ControlBar = require('./ControlBar');
const InfoMenu = require('./InfoMenu'); const InfoMenu = require('./InfoMenu');
const SubtitlesMenu = require('./SubtitlesMenu'); const SubtitlesMenu = require('./SubtitlesMenu');
const Video = require('./Video'); const Video = require('./Video');
const usePlayer = require('./usePlayer'); const usePlayer = require('./usePlayer');
const usePlaylist = require('./usePlaylist');
const useSettings = require('./useSettings'); const useSettings = require('./useSettings');
const styles = require('./styles'); const styles = require('./styles');
@ -22,6 +24,7 @@ const Player = ({ urlParams, queryParams }) => {
return queryParams.has('forceTranscoding'); return queryParams.has('forceTranscoding');
}, [queryParams]); }, [queryParams]);
const [player, updateLibraryItemState, pushToLibrary] = usePlayer(urlParams); const [player, updateLibraryItemState, pushToLibrary] = usePlayer(urlParams);
const playlist = usePlaylist(player);
const [settings, updateSettings] = useSettings(); const [settings, updateSettings] = useSettings();
const streamingServer = useStreamingServer(); const streamingServer = useStreamingServer();
const routeFocused = useRouteFocused(); const routeFocused = useRouteFocused();
@ -233,7 +236,7 @@ const Player = ({ urlParams, queryParams }) => {
streamingServer.selected.transportUrl streamingServer.selected.transportUrl
: :
null, null,
chromecastTransport: chromecast.transport chromecastTransport: chromecast.active ? chromecast.transport : null
} }
}); });
} }
@ -331,14 +334,16 @@ const Player = ({ urlParams, queryParams }) => {
} }
case 'ArrowRight': { case 'ArrowRight': {
if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) { if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) {
onSeekRequested(videoState.time + 15000); const seekTimeMultiplier = event.shiftKey ? 3 : 1;
onSeekRequested(videoState.time + (settings.seekTimeDuration * seekTimeMultiplier));
} }
break; break;
} }
case 'ArrowLeft': { case 'ArrowLeft': {
if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) { if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) {
onSeekRequested(videoState.time - 15000); const seekTimeMultiplier = event.shiftKey ? 3 : 1;
onSeekRequested(videoState.time - (settings.seekTimeDuration * seekTimeMultiplier));
} }
break; break;
@ -386,7 +391,7 @@ const Player = ({ urlParams, queryParams }) => {
return () => { return () => {
window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keydown', onKeyDown);
}; };
}, [player, routeFocused, subtitlesMenuOpen, infoMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.subtitlesTracks, toggleSubtitlesMenu, toggleInfoMenu]); }, [player, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.subtitlesTracks, toggleSubtitlesMenu, toggleInfoMenu]);
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
return () => { return () => {
setImmersedDebounced.cancel(); setImmersedDebounced.cancel();
@ -415,18 +420,30 @@ const Player = ({ urlParams, queryParams }) => {
videoState.buffering ? videoState.buffering ?
<BufferingLoader className={styles['layer']} /> <BufferingLoader className={styles['layer']} />
: :
error !== null ? null
<div className={classnames(styles['layer'], styles['error-layer'])}>
<div className={styles['error-label']}>{error.message}</div>
</div>
:
null
} }
<div <div
className={styles['layer']} className={styles['layer']}
onClick={onVideoClick} onClick={onVideoClick}
onDoubleClick={onVideoDoubleClick} onDoubleClick={onVideoDoubleClick}
/> />
{
error !== null ?
<div className={classnames(styles['layer'], styles['error-layer'])}>
<div className={styles['error-label']} title={error.message}>{error.message}</div>
{
playlist ?
<Button className={styles['playlist-button']} title={'Download M3U Playlist'} href={playlist.href} download={playlist.name}>
<Icon className={styles['icon']} icon={'ic_downloads'} />
<div className={styles['label']}>Download Playlist</div>
</Button>
:
null
}
</div>
:
null
}
{ {
subtitlesMenuOpen || infoMenuOpen ? subtitlesMenuOpen || infoMenuOpen ?
<div className={styles['layer']} /> <div className={styles['layer']} />

View file

@ -40,7 +40,7 @@ html:not(.active-slider-within) {
&.error-layer { &.error-layer {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: @color-background-dark5; background-color: @color-background-dark5;
@ -53,6 +53,39 @@ html:not(.active-slider-within) {
color: @color-surface-light5-90; color: @color-surface-light5-90;
text-align: center; text-align: center;
} }
.playlist-button {
flex: none;
display: flex;
flex-direction: row;
align-items: center;
height: 3.5rem;
max-width: 16rem;
margin-top: 1.5rem;
padding: 0.5rem 1rem;
background-color: @color-accent3;
&:hover, &:focus {
background-color: @color-accent3-light1;
}
.icon {
flex: none;
width: 1.5rem;
height: 1.5rem;
margin-right: 1rem;
fill: @color-surface-light5-90;
}
.label {
flex: 1;
max-height: 2.4em;
font-size: 1.1rem;
font-weight: 500;
color: @color-surface-light5-90;
text-align: center;
}
}
} }
&.nav-bar-layer { &.nav-bar-layer {

View file

@ -0,0 +1,16 @@
const React = require('react');
const usePlaylist = (player) => {
return React.useMemo(() => {
if (player.selected === null || typeof player.selected.stream.url !== 'string') {
return null;
}
const name = `${player.title}.m3u`;
const m3u = `#EXTM3U\n\n#EXTINF:0,${encodeURIComponent(player.title)}\n${encodeURI(player.selected.stream.url)}`;
const href = `data:application/octet-stream;charset=utf-8;base64,${window.btoa(m3u)}`;
return { name, href };
}, [player]);
};
module.exports = usePlaylist;

View file

@ -27,6 +27,7 @@ const Settings = () => {
subtitlesTextColorInput, subtitlesTextColorInput,
subtitlesBackgroundColorInput, subtitlesBackgroundColorInput,
subtitlesOutlineColorInput, subtitlesOutlineColorInput,
seekTimeDurationSelect,
bingeWatchingCheckbox, bingeWatchingCheckbox,
playInBackgroundCheckbox, playInBackgroundCheckbox,
playInExternalPlayerCheckbox, playInExternalPlayerCheckbox,
@ -303,6 +304,15 @@ const Settings = () => {
{...subtitlesOutlineColorInput} {...subtitlesOutlineColorInput}
/> />
</div> </div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Rewind & Fast-forward duration</div>
</div>
<Multiselect
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
{...seekTimeDurationSelect}
/>
</div>
<div className={styles['option-container']}> <div className={styles['option-container']}>
<div className={styles['option-name-container']}> <div className={styles['option-name-container']}>
<div className={styles['label']}>Auto-play next episode</div> <div className={styles['label']}>Auto-play next episode</div>

View file

@ -110,6 +110,28 @@ const useProfileSettingsInputs = (profile) => {
}); });
} }
}), [profile.settings]); }), [profile.settings]);
const seekTimeDurationSelect = useDeepEqualMemo(() => ({
options: CONSTANTS.SEEK_TIME_DURATIONS.map((size) => ({
value: `${size}`,
label: `${size / 1000} seconds`
})),
selected: [`${profile.settings.seekTimeDuration}`],
renderLabelText: () => {
return `${profile.settings.seekTimeDuration / 1000} seconds`;
},
onSelect: (event) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
seekTimeDuration: parseInt(event.value, 10)
}
}
});
}
}), [profile.settings]);
const bingeWatchingCheckbox = useDeepEqualMemo(() => ({ const bingeWatchingCheckbox = useDeepEqualMemo(() => ({
checked: profile.settings.bingeWatching, checked: profile.settings.bingeWatching,
onClick: () => { onClick: () => {
@ -192,6 +214,7 @@ const useProfileSettingsInputs = (profile) => {
subtitlesTextColorInput, subtitlesTextColorInput,
subtitlesBackgroundColorInput, subtitlesBackgroundColorInput,
subtitlesOutlineColorInput, subtitlesOutlineColorInput,
seekTimeDurationSelect,
bingeWatchingCheckbox, bingeWatchingCheckbox,
playInBackgroundCheckbox, playInBackgroundCheckbox,
playInExternalPlayerCheckbox, playInExternalPlayerCheckbox,