mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
Merge branch 'development' of github.com:Stremio/stremio-web into server-notification
This commit is contained in:
commit
59a2b7e3ca
11 changed files with 153 additions and 18 deletions
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -8117,9 +8117,9 @@
|
|||
"integrity": "sha512-yT3No1gIWKLV2BhQIeSgG94EzXxmEqXJLulO+pFpziqWNUbmmEKeE+nRvW5wtoIK4SLy+v0bLd0b6HBH3KFfWw=="
|
||||
},
|
||||
"@stremio/stremio-core-web": {
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.22.0.tgz",
|
||||
"integrity": "sha512-PqvIXKwYmRYwzh0BFCEb9orZ3533OVLmLNbV1mCf9dmHG+BX+mREv9IB5+/yJZal2GMLnEBPfgeNxvIDYU6sww==",
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.23.0.tgz",
|
||||
"integrity": "sha512-eZyBEWuB90y6lTG47Y4lKUnlM4bpVhmKh96DUHMPtMKOsyHRutOkuKXYjAwP0WjCbe57/vbalI04Fout7SZwWg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "7.10.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
"@babel/runtime": "7.12.5",
|
||||
"@sentry/browser": "5.11.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-video": "0.0.7",
|
||||
"a-color-picker": "1.2.1",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
const CHROMECAST_RECEIVER_APP_ID = '1634F54B';
|
||||
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 SEEK_TIME_DURATIONS = [5000, 10000, 15000, 20000, 25000, 30000];
|
||||
const CATALOG_PREVIEW_SIZE = 10;
|
||||
const CATALOG_PAGE_SIZE = 100;
|
||||
const NONE_EXTRA_VALUE = 'None';
|
||||
|
|
@ -28,6 +29,7 @@ module.exports = {
|
|||
CHROMECAST_RECEIVER_APP_ID,
|
||||
SUBTITLES_SIZES,
|
||||
SUBTITLES_FONTS,
|
||||
SEEK_TIME_DURATIONS,
|
||||
CATALOG_PREVIEW_SIZE,
|
||||
CATALOG_PAGE_SIZE,
|
||||
NONE_EXTRA_VALUE,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const ALLOWED_LINK_REDIRECTS = [
|
|||
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 linksGroups = React.useMemo(() => {
|
||||
return Array.isArray(links) ?
|
||||
|
|
@ -70,6 +70,21 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
|
|||
:
|
||||
new Map();
|
||||
}, [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(() => {
|
||||
if (!Array.isArray(trailerStreams) || trailerStreams.length === 0) {
|
||||
return null;
|
||||
|
|
@ -195,7 +210,19 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
|
|||
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>
|
||||
<ActionButton
|
||||
className={styles['action-button']}
|
||||
|
|
@ -236,6 +263,11 @@ MetaPreview.propTypes = {
|
|||
releaseInfo: PropTypes.string,
|
||||
released: PropTypes.instanceOf(Date),
|
||||
description: PropTypes.string,
|
||||
deepLinks: PropTypes.shape({
|
||||
metaDetailsVideos: PropTypes.string,
|
||||
metaDetailsStreams: PropTypes.string,
|
||||
player: PropTypes.string
|
||||
}),
|
||||
links: PropTypes.arrayOf(PropTypes.shape({
|
||||
category: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ const Addons = ({ urlParams, queryParams }) => {
|
|||
className={styles['addon-url-input']}
|
||||
type={'text'}
|
||||
placeholder={'Paste addon URL'}
|
||||
autoFocus={true}
|
||||
onSubmit={addAddonOnSubmit}
|
||||
/>
|
||||
</ModalDialog>
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ const Discover = ({ urlParams, queryParams }) => {
|
|||
releaseInfo={selectedMetaItem.releaseInfo}
|
||||
released={selectedMetaItem.released}
|
||||
description={selectedMetaItem.description}
|
||||
deepLinks={selectedMetaItem.deepLinks}
|
||||
trailerStreams={selectedMetaItem.trailerStreams}
|
||||
inLibrary={selectedMetaItem.inLibrary}
|
||||
toggleInLibrary={selectedMetaItem.inLibrary ? removeFromLibrary : addToLibrary}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,15 @@ const classnames = require('classnames');
|
|||
const debounce = require('lodash.debounce');
|
||||
const { useRouteFocused } = require('stremio-router');
|
||||
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 ControlBar = require('./ControlBar');
|
||||
const InfoMenu = require('./InfoMenu');
|
||||
const SubtitlesMenu = require('./SubtitlesMenu');
|
||||
const Video = require('./Video');
|
||||
const usePlayer = require('./usePlayer');
|
||||
const usePlaylist = require('./usePlaylist');
|
||||
const useSettings = require('./useSettings');
|
||||
const styles = require('./styles');
|
||||
|
||||
|
|
@ -22,6 +24,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
return queryParams.has('forceTranscoding');
|
||||
}, [queryParams]);
|
||||
const [player, updateLibraryItemState, pushToLibrary] = usePlayer(urlParams);
|
||||
const playlist = usePlaylist(player);
|
||||
const [settings, updateSettings] = useSettings();
|
||||
const streamingServer = useStreamingServer();
|
||||
const routeFocused = useRouteFocused();
|
||||
|
|
@ -233,7 +236,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
streamingServer.selected.transportUrl
|
||||
:
|
||||
null,
|
||||
chromecastTransport: chromecast.transport
|
||||
chromecastTransport: chromecast.active ? chromecast.transport : null
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -331,14 +334,16 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
case 'ArrowRight': {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) {
|
||||
onSeekRequested(videoState.time + 15000);
|
||||
const seekTimeMultiplier = event.shiftKey ? 3 : 1;
|
||||
onSeekRequested(videoState.time + (settings.seekTimeDuration * seekTimeMultiplier));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'ArrowLeft': {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) {
|
||||
onSeekRequested(videoState.time - 15000);
|
||||
const seekTimeMultiplier = event.shiftKey ? 3 : 1;
|
||||
onSeekRequested(videoState.time - (settings.seekTimeDuration * seekTimeMultiplier));
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -386,7 +391,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
return () => {
|
||||
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(() => {
|
||||
return () => {
|
||||
setImmersedDebounced.cancel();
|
||||
|
|
@ -415,18 +420,30 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
videoState.buffering ?
|
||||
<BufferingLoader className={styles['layer']} />
|
||||
:
|
||||
error !== null ?
|
||||
<div className={classnames(styles['layer'], styles['error-layer'])}>
|
||||
<div className={styles['error-label']}>{error.message}</div>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
null
|
||||
}
|
||||
<div
|
||||
className={styles['layer']}
|
||||
onClick={onVideoClick}
|
||||
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 ?
|
||||
<div className={styles['layer']} />
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ html:not(.active-slider-within) {
|
|||
|
||||
&.error-layer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: @color-background-dark5;
|
||||
|
|
@ -53,6 +53,39 @@ html:not(.active-slider-within) {
|
|||
color: @color-surface-light5-90;
|
||||
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 {
|
||||
|
|
|
|||
16
src/routes/Player/usePlaylist.js
Normal file
16
src/routes/Player/usePlaylist.js
Normal 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;
|
||||
|
|
@ -27,6 +27,7 @@ const Settings = () => {
|
|||
subtitlesTextColorInput,
|
||||
subtitlesBackgroundColorInput,
|
||||
subtitlesOutlineColorInput,
|
||||
seekTimeDurationSelect,
|
||||
bingeWatchingCheckbox,
|
||||
playInBackgroundCheckbox,
|
||||
playInExternalPlayerCheckbox,
|
||||
|
|
@ -303,6 +304,15 @@ const Settings = () => {
|
|||
{...subtitlesOutlineColorInput}
|
||||
/>
|
||||
</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-name-container']}>
|
||||
<div className={styles['label']}>Auto-play next episode</div>
|
||||
|
|
|
|||
|
|
@ -110,6 +110,28 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [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(() => ({
|
||||
checked: profile.settings.bingeWatching,
|
||||
onClick: () => {
|
||||
|
|
@ -192,6 +214,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
subtitlesTextColorInput,
|
||||
subtitlesBackgroundColorInput,
|
||||
subtitlesOutlineColorInput,
|
||||
seekTimeDurationSelect,
|
||||
bingeWatchingCheckbox,
|
||||
playInBackgroundCheckbox,
|
||||
playInExternalPlayerCheckbox,
|
||||
|
|
|
|||
Loading…
Reference in a new issue