mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
Merge branch 'development' of https://github.com/Stremio/stremio-web into interface-language
This commit is contained in:
commit
2275f40eaa
12 changed files with 305 additions and 63 deletions
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -12,7 +12,7 @@
|
|||
"@babel/runtime": "7.16.0",
|
||||
"@sentry/browser": "6.13.3",
|
||||
"@stremio/stremio-colors": "5.0.1",
|
||||
"@stremio/stremio-core-web": "0.44.7",
|
||||
"@stremio/stremio-core-web": "0.44.11",
|
||||
"@stremio/stremio-icons": "4.0.0",
|
||||
"@stremio/stremio-video": "0.0.24",
|
||||
"a-color-picker": "1.2.1",
|
||||
|
|
@ -2702,9 +2702,9 @@
|
|||
"integrity": "sha512-Dt3PYmy1DZ473QNs99KYXVWQPHtpIl37VUY0+gCEvvuCqE1fRrZIJtZ9KbysUKonvO7WwdQDztgcW0iGoc1dEA=="
|
||||
},
|
||||
"node_modules/@stremio/stremio-core-web": {
|
||||
"version": "0.44.7",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.7.tgz",
|
||||
"integrity": "sha512-hkeYLfL1On4TMBHn87Onrp93aeRuTh4YXMKdDR1Vz5YikPOiPEq/JRoLLmmSSsFEdifs6Egu+A0qiggTttepOA==",
|
||||
"version": "0.44.11",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.11.tgz",
|
||||
"integrity": "sha512-KkNEZenQ+94ylYEMl/AqvL1Md4p+xwqg/02FKuVxjFBEoKyXEDXCgOKOI7wbbXdZJe8tnvk+XFq39jzAiTinSw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.16.0"
|
||||
}
|
||||
|
|
@ -16795,9 +16795,9 @@
|
|||
"integrity": "sha512-Dt3PYmy1DZ473QNs99KYXVWQPHtpIl37VUY0+gCEvvuCqE1fRrZIJtZ9KbysUKonvO7WwdQDztgcW0iGoc1dEA=="
|
||||
},
|
||||
"@stremio/stremio-core-web": {
|
||||
"version": "0.44.7",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.7.tgz",
|
||||
"integrity": "sha512-hkeYLfL1On4TMBHn87Onrp93aeRuTh4YXMKdDR1Vz5YikPOiPEq/JRoLLmmSSsFEdifs6Egu+A0qiggTttepOA==",
|
||||
"version": "0.44.11",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.11.tgz",
|
||||
"integrity": "sha512-KkNEZenQ+94ylYEMl/AqvL1Md4p+xwqg/02FKuVxjFBEoKyXEDXCgOKOI7wbbXdZJe8tnvk+XFq39jzAiTinSw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "7.16.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"@babel/runtime": "7.16.0",
|
||||
"@sentry/browser": "6.13.3",
|
||||
"@stremio/stremio-colors": "5.0.1",
|
||||
"@stremio/stremio-core-web": "0.44.7",
|
||||
"@stremio/stremio-core-web": "0.44.11",
|
||||
"@stremio/stremio-icons": "4.0.0",
|
||||
"@stremio/stremio-video": "0.0.24",
|
||||
"a-color-picker": "1.2.1",
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ const App = () => {
|
|||
i18n.changeLanguage(state.profile.settings.interfaceLanguage);
|
||||
}
|
||||
};
|
||||
if (services.core.active) {
|
||||
const onWindowFocus = () => {
|
||||
services.core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
|
|
@ -126,6 +126,10 @@ const App = () => {
|
|||
action: 'SyncLibraryWithAPI'
|
||||
}
|
||||
});
|
||||
};
|
||||
if (services.core.active) {
|
||||
onWindowFocus();
|
||||
window.addEventListener('focus', onWindowFocus);
|
||||
services.core.transport.on('CoreEvent', onCoreEvent);
|
||||
services.core.transport
|
||||
.getState('ctx')
|
||||
|
|
@ -133,6 +137,7 @@ const App = () => {
|
|||
.catch((e) => console.error(e));
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('focus', onWindowFocus);
|
||||
services.core.transport.off('CoreEvent', onCoreEvent);
|
||||
};
|
||||
}, [initialized]);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ const ServicesToaster = () => {
|
|||
break;
|
||||
}
|
||||
|
||||
if (args.error.type === 'Other' && args.error.code === 3 && args.source.event === 'AddonInstalled' && args.source.args.transport_url.startsWith('https://www.strem.io/trakt/addon')) {
|
||||
break;
|
||||
}
|
||||
|
||||
toast.show({
|
||||
type: 'error',
|
||||
title: args.source.event,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const ControlBar = ({
|
|||
onToggleInfoMenu,
|
||||
onToggleSpeedMenu,
|
||||
onToggleVideosMenu,
|
||||
onToggleOptionsMenu,
|
||||
...props
|
||||
}) => {
|
||||
const { chromecast } = useServices();
|
||||
|
|
@ -51,6 +52,9 @@ const ControlBar = ({
|
|||
const onVideosButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.videosMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onOptionsButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.optionsMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onPlayPauseButtonClick = React.useCallback(() => {
|
||||
if (paused) {
|
||||
if (typeof onPlayRequested === 'function') {
|
||||
|
|
@ -102,6 +106,11 @@ const ControlBar = ({
|
|||
onToggleVideosMenu();
|
||||
}
|
||||
}, [onToggleVideosMenu]);
|
||||
const onOptionsButtonClick = React.useCallback(() => {
|
||||
if (typeof onToggleOptionsMenu === 'function') {
|
||||
onToggleOptionsMenu();
|
||||
}
|
||||
}, [onToggleOptionsMenu]);
|
||||
const onChromecastButtonClick = React.useCallback(() => {
|
||||
chromecast.transport.requestSession();
|
||||
}, []);
|
||||
|
|
@ -179,6 +188,9 @@ const ControlBar = ({
|
|||
:
|
||||
null
|
||||
}
|
||||
<Button className={styles['control-bar-button']} tabIndex={-1} onMouseDown={onOptionsButtonMouseDown} onClick={onOptionsButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_more'} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -206,7 +218,8 @@ ControlBar.propTypes = {
|
|||
onToggleSubtitlesMenu: PropTypes.func,
|
||||
onToggleInfoMenu: PropTypes.func,
|
||||
onToggleSpeedMenu: PropTypes.func,
|
||||
onToggleVideosMenu: PropTypes.func
|
||||
onToggleVideosMenu: PropTypes.func,
|
||||
onToggleOptionsMenu: PropTypes.func,
|
||||
};
|
||||
|
||||
module.exports = ControlBar;
|
||||
|
|
|
|||
92
src/routes/Player/OptionsMenu/OptionsMenu.js
Normal file
92
src/routes/Player/OptionsMenu/OptionsMenu.js
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const Icon = require('@stremio/stremio-icons/dom');
|
||||
const { Button, useToast } = require('stremio/common');
|
||||
// const { useServices } = require('stremio/services');
|
||||
const styles = require('./styles');
|
||||
|
||||
const OptionsMenu = ({ className, stream }) => {
|
||||
// const { core } = useServices();
|
||||
const toast = useToast();
|
||||
const streamUrl = React.useMemo(() => {
|
||||
return stream !== null ?
|
||||
stream.deepLinks &&
|
||||
stream.deepLinks.externalPlayer &&
|
||||
typeof stream.deepLinks.externalPlayer.download === 'string' ?
|
||||
stream.deepLinks.externalPlayer.download
|
||||
:
|
||||
null
|
||||
:
|
||||
null;
|
||||
}, [stream]);
|
||||
const onCopyStreamButtonClick = React.useCallback(() => {
|
||||
if (streamUrl !== null) {
|
||||
navigator.clipboard.writeText(streamUrl)
|
||||
.then(() => {
|
||||
toast.show({
|
||||
type: 'success',
|
||||
title: 'Copied',
|
||||
message: 'Stream link was copied to your clipboard',
|
||||
timeout: 3000
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
toast.show({
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: `Failed to copy stream link: ${streamUrl}`,
|
||||
timeout: 3000
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [streamUrl]);
|
||||
const onDownloadVideoButtonClick = React.useCallback(() => {
|
||||
if (streamUrl !== null) {
|
||||
window.open(streamUrl);
|
||||
}
|
||||
}, [streamUrl]);
|
||||
// const onExternalPlayerButtonClick = React.useCallback(() => {
|
||||
// if (streamUrl !== null) {
|
||||
// core.transport.dispatch({
|
||||
// action: 'StreamingServer',
|
||||
// args: {
|
||||
// action: 'PlayOnDevice',
|
||||
// args: {
|
||||
// device: 'vlc',
|
||||
// source: streamUrl,
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }, [streamUrl]);
|
||||
const onMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.optionsMenuClosePrevented = true;
|
||||
}, []);
|
||||
return (
|
||||
<div className={classnames(className, styles['options-menu-container'])} onMouseDown={onMouseDown}>
|
||||
<Button className={classnames(styles['option-container'], { 'disabled': stream === null })} disabled={stream === null} onClick={onCopyStreamButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_link'} />
|
||||
<div className={styles['label']}>Copy Stream Link</div>
|
||||
</Button>
|
||||
<Button className={classnames(styles['option-container'], { 'disabled': stream === null })} disabled={stream === null}onClick={onDownloadVideoButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_downloads'} />
|
||||
<div className={styles['label']}>Download Video</div>
|
||||
</Button>
|
||||
{/* <Button className={classnames(styles['option-container'], { 'disabled': stream === null })} disabled={stream === null} onClick={onExternalPlayerButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_vlc'} />
|
||||
<div className={styles['label']}>Play in External Player</div>
|
||||
</Button> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
OptionsMenu.propTypes = {
|
||||
className: PropTypes.string,
|
||||
stream: PropTypes.object
|
||||
};
|
||||
|
||||
module.exports = OptionsMenu;
|
||||
5
src/routes/Player/OptionsMenu/index.js
Normal file
5
src/routes/Player/OptionsMenu/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const OptionsMenu = require('./OptionsMenu');
|
||||
|
||||
module.exports = OptionsMenu;
|
||||
37
src/routes/Player/OptionsMenu/styles.less
Normal file
37
src/routes/Player/OptionsMenu/styles.less
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
|
||||
.options-menu-container {
|
||||
width: 15rem;
|
||||
|
||||
.option-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 4rem;
|
||||
|
||||
.icon {
|
||||
flex: none;
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
margin: 1.3rem;
|
||||
fill: @color-surface-light5-90;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
max-height: 2.4em;
|
||||
font-weight: 400;
|
||||
color: @color-surface-light5-90;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: @color-background-light2;
|
||||
}
|
||||
|
||||
&:global(.disabled) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ const BufferingLoader = require('./BufferingLoader');
|
|||
const ControlBar = require('./ControlBar');
|
||||
const NextVideoPopup = require('./NextVideoPopup');
|
||||
const InfoMenu = require('./InfoMenu');
|
||||
const OptionsMenu = require('./OptionsMenu');
|
||||
const VideosMenu = require('./VideosMenu');
|
||||
const SubtitlesMenu = require('./SubtitlesMenu');
|
||||
const SpeedMenu = require('./SpeedMenu');
|
||||
|
|
@ -29,26 +30,6 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
queryParams.has('maxAudioChannels') ? parseInt(queryParams.get('maxAudioChannels'), 10) : null
|
||||
];
|
||||
}, [queryParams]);
|
||||
const [player, timeChanged, pausedChanged, ended, pushToLibrary] = usePlayer(urlParams);
|
||||
const [settings, updateSettings] = useSettings();
|
||||
const streamingServer = useStreamingServer();
|
||||
const routeFocused = useRouteFocused();
|
||||
const toast = useToast();
|
||||
const [, , , toggleFullscreen] = useFullscreen();
|
||||
const [casting, setCasting] = React.useState(() => {
|
||||
return chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED;
|
||||
});
|
||||
const [immersed, setImmersed] = React.useState(true);
|
||||
const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
|
||||
const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
|
||||
const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
|
||||
const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
|
||||
const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false);
|
||||
const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
|
||||
const nextVideoPopupDismissed = React.useRef(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 }),
|
||||
{
|
||||
|
|
@ -61,6 +42,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
volume: null,
|
||||
muted: null,
|
||||
playbackSpeed: null,
|
||||
videoParams: null,
|
||||
audioTracks: [],
|
||||
selectedAudioTrackId: null,
|
||||
subtitlesTracks: [],
|
||||
|
|
@ -80,6 +62,27 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
extraSubtitlesOutlineColor: null
|
||||
}
|
||||
);
|
||||
const [player, timeChanged, pausedChanged, ended, pushToLibrary] = usePlayer(urlParams, videoState.videoParams);
|
||||
const [settings, updateSettings] = useSettings();
|
||||
const streamingServer = useStreamingServer();
|
||||
const routeFocused = useRouteFocused();
|
||||
const toast = useToast();
|
||||
const [, , , toggleFullscreen] = useFullscreen();
|
||||
const [casting, setCasting] = React.useState(() => {
|
||||
return chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED;
|
||||
});
|
||||
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 [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
|
||||
const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false);
|
||||
const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
|
||||
const nextVideoPopupDismissed = React.useRef(false);
|
||||
const defaultSubtitlesSelected = React.useRef(false);
|
||||
const defaultAudioTrackSelected = React.useRef(false);
|
||||
const [error, setError] = React.useState(null);
|
||||
const videoRef = React.useRef(null);
|
||||
const dispatch = React.useCallback((action, options) => {
|
||||
if (videoRef.current !== null) {
|
||||
|
|
@ -215,6 +218,9 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
toggleFullscreen();
|
||||
}, [toggleFullscreen]);
|
||||
const onContainerMouseDown = React.useCallback((event) => {
|
||||
if (!event.nativeEvent.optionsMenuClosePrevented) {
|
||||
closeOptionsMenu();
|
||||
}
|
||||
if (!event.nativeEvent.subtitlesMenuClosePrevented) {
|
||||
closeSubtitlesMenu();
|
||||
}
|
||||
|
|
@ -439,7 +445,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
const onKeyDown = (event) => {
|
||||
switch (event.code) {
|
||||
case 'Space': {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen&& videoState.paused !== null) {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && videoState.paused !== null) {
|
||||
if (videoState.paused) {
|
||||
onPlayRequested();
|
||||
} else {
|
||||
|
|
@ -450,7 +456,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
break;
|
||||
}
|
||||
case 'ArrowRight': {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.time !== null) {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && videoState.time !== null) {
|
||||
const seekTimeMultiplier = event.shiftKey ? 3 : 1;
|
||||
onSeekRequested(videoState.time + (settings.seekTimeDuration * seekTimeMultiplier));
|
||||
}
|
||||
|
|
@ -458,7 +464,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
break;
|
||||
}
|
||||
case 'ArrowLeft': {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.time !== null) {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && videoState.time !== null) {
|
||||
const seekTimeMultiplier = event.shiftKey ? 3 : 1;
|
||||
onSeekRequested(videoState.time - (settings.seekTimeDuration * seekTimeMultiplier));
|
||||
}
|
||||
|
|
@ -466,20 +472,21 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
break;
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.volume !== null) {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && videoState.volume !== null) {
|
||||
onVolumeChangeRequested(videoState.volume + 5);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'ArrowDown': {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.volume !== null) {
|
||||
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && videoState.volume !== null) {
|
||||
onVolumeChangeRequested(videoState.volume - 5);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyS': {
|
||||
closeOptionsMenu();
|
||||
closeInfoMenu();
|
||||
closeSpeedMenu();
|
||||
closeVideosMenu();
|
||||
|
|
@ -492,6 +499,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
break;
|
||||
}
|
||||
case 'KeyI': {
|
||||
closeOptionsMenu();
|
||||
closeSubtitlesMenu();
|
||||
closeSpeedMenu();
|
||||
closeVideosMenu();
|
||||
|
|
@ -502,6 +510,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
break;
|
||||
}
|
||||
case 'KeyR': {
|
||||
closeOptionsMenu();
|
||||
closeInfoMenu();
|
||||
closeSubtitlesMenu();
|
||||
closeVideosMenu();
|
||||
|
|
@ -512,6 +521,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
break;
|
||||
}
|
||||
case 'KeyV': {
|
||||
closeOptionsMenu();
|
||||
closeInfoMenu();
|
||||
closeSubtitlesMenu();
|
||||
closeSpeedMenu();
|
||||
|
|
@ -522,6 +532,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
break;
|
||||
}
|
||||
case 'Escape': {
|
||||
closeOptionsMenu();
|
||||
closeSubtitlesMenu();
|
||||
closeInfoMenu();
|
||||
closeSpeedMenu();
|
||||
|
|
@ -537,7 +548,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [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]);
|
||||
}, [player.metaItem, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, videosMenuOpen, speedMenuOpen, optionsMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, videoState.playbackSpeed, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu]);
|
||||
React.useLayoutEffect(() => {
|
||||
return () => {
|
||||
setImmersedDebounced.cancel();
|
||||
|
|
@ -546,7 +557,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen && !speedMenuOpen && !videosMenuOpen && !nextVideoPopupOpen })}
|
||||
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen && !speedMenuOpen && !videosMenuOpen && !nextVideoPopupOpen && !optionsMenuOpen })}
|
||||
onMouseDown={onContainerMouseDown}
|
||||
onMouseMove={onContainerMouseMove}
|
||||
onMouseOver={onContainerMouseMove}
|
||||
|
|
@ -579,7 +590,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
<div className={styles['error-label']} title={error.message}>{error.message}</div>
|
||||
{
|
||||
player.selected !== null ?
|
||||
<Button {...player.selected.stream.deepLinks.externalPlayer} className={styles['playlist-button']} title={'Open in external player'} target={'_blank'}>
|
||||
<Button className={styles['playlist-button']} title={'Open in external player'} href={player.selected.stream.deepLinks.externalPlayer.href} download={player.selected.stream.deepLinks.externalPlayer.fileName} target={'_blank'}>
|
||||
<Icon className={styles['icon']} icon={'ic_downloads'} />
|
||||
<div className={styles['label']}>Open in external player</div>
|
||||
</Button>
|
||||
|
|
@ -591,7 +602,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
null
|
||||
}
|
||||
{
|
||||
subtitlesMenuOpen || infoMenuOpen || videosMenuOpen || speedMenuOpen ?
|
||||
subtitlesMenuOpen || infoMenuOpen || videosMenuOpen || speedMenuOpen || optionsMenuOpen ?
|
||||
<div className={styles['layer']} />
|
||||
:
|
||||
null
|
||||
|
|
@ -622,6 +633,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
onUnmuteRequested={onUnmuteRequested}
|
||||
onVolumeChangeRequested={onVolumeChangeRequested}
|
||||
onSeekRequested={onSeekRequested}
|
||||
onToggleOptionsMenu={toggleOptionsMenu}
|
||||
onToggleSubtitlesMenu={toggleSubtitlesMenu}
|
||||
onToggleInfoMenu={toggleInfoMenu}
|
||||
onToggleSpeedMenu={toggleSpeedMenu}
|
||||
|
|
@ -699,6 +711,15 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
optionsMenuOpen ?
|
||||
<OptionsMenu
|
||||
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||
stream={player.selected.stream}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const map = (player) => ({
|
|||
player.metaItem,
|
||||
});
|
||||
|
||||
const usePlayer = (urlParams) => {
|
||||
const usePlayer = (urlParams, videoParams) => {
|
||||
const { core } = useServices();
|
||||
const { decodeStream } = useCoreSuspender();
|
||||
const stream = decodeStream(urlParams.stream);
|
||||
|
|
@ -44,6 +44,7 @@ const usePlayer = (urlParams) => {
|
|||
model: 'Player',
|
||||
args: {
|
||||
stream,
|
||||
videoParams,
|
||||
streamRequest: typeof urlParams.streamTransportUrl === 'string' && typeof urlParams.type === 'string' && typeof urlParams.videoId === 'string' ?
|
||||
{
|
||||
base: urlParams.streamTransportUrl,
|
||||
|
|
@ -85,7 +86,7 @@ const usePlayer = (urlParams) => {
|
|||
action: 'Unload'
|
||||
};
|
||||
}
|
||||
}, [urlParams]);
|
||||
}, [urlParams, videoParams]);
|
||||
const timeChanged = React.useCallback((time, duration, device) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Player',
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ const { useTranslation } = require('react-i18next');
|
|||
const Icon = require('@stremio/stremio-icons/dom');
|
||||
const { useRouteFocused } = require('stremio-router');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { Button, Checkbox, MainNavBars, Multiselect, ColorInput, TextInput, ModalDialog, useProfile, useStreamingServer, useBinaryState, withCoreSuspender } = require('stremio/common');
|
||||
const { Button, Checkbox, MainNavBars, Multiselect, ColorInput, TextInput, ModalDialog, useProfile, useStreamingServer, useBinaryState, withCoreSuspender, useToast } = require('stremio/common');
|
||||
const useProfileSettingsInputs = require('./useProfileSettingsInputs');
|
||||
const useStreamingServerSettingsInputs = require('./useStreamingServerSettingsInputs');
|
||||
const useDataExport = require('./useDataExport');
|
||||
const styles = require('./styles');
|
||||
|
||||
const GENERAL_SECTION = 'general';
|
||||
|
|
@ -22,7 +23,9 @@ const Settings = () => {
|
|||
const { core } = useServices();
|
||||
const { routeFocused } = useRouteFocused();
|
||||
const profile = useProfile();
|
||||
const [dataExport, loadDataExport] = useDataExport();
|
||||
const streamingServer = useStreamingServer();
|
||||
const toast = useToast();
|
||||
const {
|
||||
interfaceLanguageSelect,
|
||||
subtitlesLanguageSelect,
|
||||
|
|
@ -49,6 +52,11 @@ const Settings = () => {
|
|||
streamingServerUrlInput.onChange(configureServerUrlInputRef.current.value);
|
||||
closeConfigureServerUrlModal();
|
||||
}, [streamingServerUrlInput]);
|
||||
const [traktAuthStarted, setTraktAuthStarted] = React.useState(false);
|
||||
const isTraktAuthenticated = React.useMemo(() => {
|
||||
return profile.auth !== null && profile.auth.user !== null && profile.auth.user.trakt !== null &&
|
||||
(Date.now() / 1000) < (profile.auth.user.trakt.created_at + profile.auth.user.trakt.expires_in);
|
||||
}, [profile.auth]);
|
||||
const configureServerUrlModalButtons = React.useMemo(() => {
|
||||
return [
|
||||
{
|
||||
|
|
@ -74,17 +82,31 @@ const Settings = () => {
|
|||
}
|
||||
});
|
||||
}, []);
|
||||
const authenticateTraktOnClick = React.useCallback(() => {
|
||||
// TODO
|
||||
}, []);
|
||||
const importFacebookOnClick = React.useCallback(() => {
|
||||
// TODO
|
||||
}, []);
|
||||
const toggleTraktOnClick = React.useCallback(() => {
|
||||
if (!isTraktAuthenticated && profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user._id === 'string') {
|
||||
window.open(`https://www.strem.io/trakt/auth/${profile.auth.user._id}`);
|
||||
setTraktAuthStarted(true);
|
||||
} else {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'LogoutTrakt'
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [isTraktAuthenticated, profile.auth]);
|
||||
const subscribeCalendarOnClick = React.useCallback(() => {
|
||||
// TODO
|
||||
const url = `webcal://www.strem.io/calendar/${profile.auth.user._id}.ics`;
|
||||
window.open(url);
|
||||
toast.show({
|
||||
type: 'success',
|
||||
title: 'Calendar has been added to your default caldendar app',
|
||||
timeout: 25000
|
||||
});
|
||||
//Stremio 4 emits not documented event subscribeCalendar
|
||||
}, []);
|
||||
const exportDataOnClick = React.useCallback(() => {
|
||||
// TODO
|
||||
loadDataExport();
|
||||
}, []);
|
||||
const reloadStreamingServer = React.useCallback(() => {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -130,6 +152,22 @@ const Settings = () => {
|
|||
const sectionsContainerOnScorll = React.useCallback(throttle(() => {
|
||||
updateSelectedSectionId();
|
||||
}, 50), []);
|
||||
React.useEffect(() => {
|
||||
if (isTraktAuthenticated && traktAuthStarted) {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'InstallTraktAddon'
|
||||
}
|
||||
});
|
||||
setTraktAuthStarted(false);
|
||||
}
|
||||
}, [isTraktAuthenticated, traktAuthStarted]);
|
||||
React.useEffect(() => {
|
||||
if (dataExport.exportUrl !== null && typeof dataExport.exportUrl === 'string') {
|
||||
window.open(dataExport.exportUrl);
|
||||
}
|
||||
}, [dataExport.exportUrl]);
|
||||
React.useLayoutEffect(() => {
|
||||
if (routeFocused) {
|
||||
updateSelectedSectionId();
|
||||
|
|
@ -217,31 +255,24 @@ const Settings = () => {
|
|||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Trakt Scrobbling</div>
|
||||
</div>
|
||||
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={'Authenticate'} disabled={true} tabIndex={-1} onClick={authenticateTraktOnClick}>
|
||||
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={'Authenticate'} disabled={profile.auth === null} tabIndex={-1} onClick={toggleTraktOnClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_trakt'} />
|
||||
<div className={styles['label']}>{ t('SETTINGS_TRAKT_AUTHENTICATE') }</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Facebook</div>
|
||||
</div>
|
||||
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={'Import'} disabled={true} tabIndex={-1} onClick={importFacebookOnClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_facebook'} />
|
||||
<div className={styles['label']}>{ t('SETTINGS_FACEBOOK_IMPORT') }</div>
|
||||
<div className={styles['label']}>
|
||||
{ profile.auth !== null && profile.auth.user !== null && profile.auth.user.trakt !== null ? t('LOG_OUT') : t('SETTINGS_TRAKT_AUTHENTICATE') }
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('Calendar') }</div>
|
||||
</div>
|
||||
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={'Subscribe'} disabled={true} tabIndex={-1} onClick={subscribeCalendarOnClick}>
|
||||
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={'Subscribe'} disabled={!(profile.auth && profile.auth.user && profile.auth.user._id)} tabIndex={-1} onClick={subscribeCalendarOnClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_calendar'} />
|
||||
<div className={styles['label']}>{ t('SETTINGS_CALENDAR_SUBSCRIBE') }</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<Button className={classnames(styles['option-input-container'], styles['link-container'])} title={t('SETTINGS_DATA_EXPORT')} disabled={true} tabIndex={-1} onClick={exportDataOnClick}>
|
||||
<Button className={classnames(styles['option-input-container'], styles['link-container'])} title={t('SETTINGS_DATA_EXPORT')} tabIndex={-1} onClick={exportDataOnClick}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_DATA_EXPORT') }</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -564,6 +595,7 @@ const Settings = () => {
|
|||
onCloseRequest={closeConfigureServerUrlModal}>
|
||||
<TextInput
|
||||
ref={configureServerUrlInputRef}
|
||||
autoFocus={true}
|
||||
className={styles['server-url-input']}
|
||||
type={'text'}
|
||||
defaultValue={streamingServerUrlInput.value}
|
||||
|
|
|
|||
32
src/routes/Settings/useDataExport.js
Normal file
32
src/routes/Settings/useDataExport.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { useModelState } = require('stremio/common');
|
||||
|
||||
const map = (dataExport) => ({
|
||||
...dataExport,
|
||||
exportUrl: dataExport !== null && dataExport.exportUrl !== null && dataExport.exportUrl.type === 'Ready' ?
|
||||
dataExport.exportUrl.content
|
||||
:
|
||||
null
|
||||
});
|
||||
|
||||
const useDataExport = () => {
|
||||
const { core } = useServices();
|
||||
const loadDataExport = React.useCallback(() => {
|
||||
core.transport.dispatch({
|
||||
action: 'Load',
|
||||
args: {
|
||||
model: 'DataExport',
|
||||
}
|
||||
}, 'data_export');
|
||||
}, []);
|
||||
const dataExport = useModelState({ model: 'data_export', map });
|
||||
return [
|
||||
dataExport,
|
||||
loadDataExport
|
||||
];
|
||||
};
|
||||
|
||||
module.exports = useDataExport;
|
||||
Loading…
Reference in a new issue