mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-01-11 22:40:31 +00:00
Merge branch 'development' of https://github.com/Stremio/stremio-web into pwa
This commit is contained in:
commit
d7eccd9d0c
67 changed files with 3439 additions and 816 deletions
2174
package-lock.json
generated
2174
package-lock.json
generated
File diff suppressed because it is too large
Load diff
16
package.json
16
package.json
|
|
@ -15,9 +15,9 @@
|
|||
"@babel/runtime": "7.16.0",
|
||||
"@sentry/browser": "6.13.3",
|
||||
"@stremio/stremio-colors": "4.0.1",
|
||||
"@stremio/stremio-core-web": "0.43.0",
|
||||
"@stremio/stremio-core-web": "0.44.6",
|
||||
"@stremio/stremio-icons": "3.0.5",
|
||||
"@stremio/stremio-video": "0.0.20-rc.4",
|
||||
"@stremio/stremio-video": "0.0.23",
|
||||
"a-color-picker": "1.2.1",
|
||||
"bowser": "2.11.0",
|
||||
"buffer": "6.0.3",
|
||||
|
|
@ -25,13 +25,17 @@
|
|||
"eventemitter3": "4.0.7",
|
||||
"filter-invalid-dom-props": "2.1.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.intersection": "4.4.0",
|
||||
"lodash.isequal": "4.5.0",
|
||||
"lodash.throttle": "4.1.1",
|
||||
"magnet-uri": "6.2.0",
|
||||
"prop-types": "15.7.2",
|
||||
"react": "16.12.0",
|
||||
"react-dom": "16.12.0",
|
||||
"react-focus-lock": "2.2.1",
|
||||
"spatial-navigation-polyfill": "git+https://git@github.com/Stremio/spatial-navigation.git#64871b1422466f5f45d24ebc8bbd315b2ebab6a6"
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-focus-lock": "2.9.1",
|
||||
"react-is": "18.2.0",
|
||||
"spatial-navigation-polyfill": "git+https://git@github.com/Stremio/spatial-navigation.git#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
|
||||
"url": "0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.16.0",
|
||||
|
|
|
|||
|
|
@ -3,30 +3,32 @@
|
|||
require('spatial-navigation-polyfill');
|
||||
const React = require('react');
|
||||
const { Router } = require('stremio-router');
|
||||
const { Core, Shell, Chromecast, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
|
||||
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
|
||||
const { NotFound } = require('stremio/routes');
|
||||
const { ToastProvider, sanitizeLocationPath, CONSTANTS } = require('stremio/common');
|
||||
const CoreEventsToaster = require('./CoreEventsToaster');
|
||||
const { ToastProvider, CONSTANTS } = require('stremio/common');
|
||||
const ServicesToaster = require('./ServicesToaster');
|
||||
const DeepLinkHandler = require('./DeepLinkHandler');
|
||||
const ErrorDialog = require('./ErrorDialog');
|
||||
const routerViewsConfig = require('./routerViewsConfig');
|
||||
const styles = require('./styles');
|
||||
|
||||
window.core_imports = {
|
||||
app_version: process.env.VERSION,
|
||||
shell_version: null,
|
||||
sanitize_location_path: sanitizeLocationPath
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
const onPathNotMatch = React.useCallback(() => {
|
||||
return NotFound;
|
||||
}, []);
|
||||
const services = React.useMemo(() => ({
|
||||
core: new Core(),
|
||||
shell: new Shell(),
|
||||
chromecast: new Chromecast(),
|
||||
keyboardShortcuts: new KeyboardShortcuts()
|
||||
}), []);
|
||||
const services = React.useMemo(() => {
|
||||
const core = new Core({
|
||||
appVersion: process.env.VERSION,
|
||||
shellVersion: null
|
||||
});
|
||||
return {
|
||||
core,
|
||||
shell: new Shell(),
|
||||
chromecast: new Chromecast(),
|
||||
keyboardShortcuts: new KeyboardShortcuts(),
|
||||
dragAndDrop: new DragAndDrop({ core })
|
||||
};
|
||||
}, []);
|
||||
const [initialized, setInitialized] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
let prevPath = window.location.hash.slice(1);
|
||||
|
|
@ -74,12 +76,14 @@ const App = () => {
|
|||
services.shell.start();
|
||||
services.chromecast.start();
|
||||
services.keyboardShortcuts.start();
|
||||
services.dragAndDrop.start();
|
||||
window.services = services;
|
||||
return () => {
|
||||
services.core.stop();
|
||||
services.shell.stop();
|
||||
services.chromecast.stop();
|
||||
services.keyboardShortcuts.stop();
|
||||
services.dragAndDrop.stop();
|
||||
services.core.off('stateChanged', onCoreStateChanged);
|
||||
services.shell.off('stateChanged', onShellStateChanged);
|
||||
services.chromecast.off('stateChanged', onChromecastStateChange);
|
||||
|
|
@ -93,6 +97,18 @@ const App = () => {
|
|||
action: 'PullAddonsFromAPI'
|
||||
}
|
||||
});
|
||||
services.core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'PullUserFromAPI'
|
||||
}
|
||||
});
|
||||
services.core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'SyncLibraryWithAPI'
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [initialized]);
|
||||
return (
|
||||
|
|
@ -104,7 +120,8 @@ const App = () => {
|
|||
<ErrorDialog className={styles['error-container']} />
|
||||
:
|
||||
<ToastProvider className={styles['toasts-container']}>
|
||||
<CoreEventsToaster />
|
||||
<ServicesToaster />
|
||||
<DeepLinkHandler />
|
||||
<Router
|
||||
className={styles['router']}
|
||||
viewsConfig={routerViewsConfig}
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { useToast } = require('stremio/common');
|
||||
|
||||
const CoreEventsToaster = () => {
|
||||
const { core } = useServices();
|
||||
const toast = useToast();
|
||||
React.useEffect(() => {
|
||||
const onCoreEvent = ({ event, args }) => {
|
||||
if (event === 'Error') {
|
||||
toast.show({
|
||||
type: 'error',
|
||||
title: args.source.event,
|
||||
message: args.error.message,
|
||||
timeout: 4000
|
||||
});
|
||||
}
|
||||
};
|
||||
core.transport.on('CoreEvent', onCoreEvent);
|
||||
return () => {
|
||||
core.transport.off('CoreEvent', onCoreEvent);
|
||||
};
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
||||
module.exports = CoreEventsToaster;
|
||||
22
src/App/DeepLinkHandler.js
Normal file
22
src/App/DeepLinkHandler.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const { withCoreSuspender, useStreamingServer } = require('stremio/common');
|
||||
|
||||
const DeepLinkHandler = () => {
|
||||
const streamingServer = useStreamingServer();
|
||||
React.useEffect(() => {
|
||||
if (streamingServer.torrent !== null) {
|
||||
const [, { type, content }] = streamingServer.torrent;
|
||||
if (type === 'Ready') {
|
||||
const [, deepLinks] = content;
|
||||
if (typeof deepLinks.metaDetailsVideos === 'string') {
|
||||
window.location = deepLinks.metaDetailsVideos;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [streamingServer.torrent]);
|
||||
return null;
|
||||
};
|
||||
|
||||
module.exports = withCoreSuspender(DeepLinkHandler);
|
||||
69
src/App/ServicesToaster.js
Normal file
69
src/App/ServicesToaster.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { useToast } = require('stremio/common');
|
||||
|
||||
const ServicesToaster = () => {
|
||||
const { core, dragAndDrop } = useServices();
|
||||
const toast = useToast();
|
||||
React.useEffect(() => {
|
||||
const onCoreEvent = ({ event, args }) => {
|
||||
switch (event) {
|
||||
case 'Error': {
|
||||
if (args.source.event === 'UserPulledFromAPI' && args.source.args.uid === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (args.source.event === 'LibrarySyncWithAPIPlanned' && args.source.args.uid === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
toast.show({
|
||||
type: 'error',
|
||||
title: args.source.event,
|
||||
message: args.error.message,
|
||||
timeout: 4000,
|
||||
dataset: {
|
||||
type: 'CoreEvent'
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'TorrentParsed': {
|
||||
toast.show({
|
||||
type: 'success',
|
||||
title: 'Torrent file parsed',
|
||||
timeout: 4000
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'MagnetParsed': {
|
||||
toast.show({
|
||||
type: 'success',
|
||||
title: 'Magnet link parsed',
|
||||
timeout: 4000
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
const onDragAndDropError = (error) => {
|
||||
toast.show({
|
||||
type: 'error',
|
||||
title: error.message,
|
||||
message: error.file?.name,
|
||||
timeout: 4000
|
||||
});
|
||||
};
|
||||
core.transport.on('CoreEvent', onCoreEvent);
|
||||
dragAndDrop.on('error', onDragAndDropError);
|
||||
return () => {
|
||||
core.transport.off('CoreEvent', onCoreEvent);
|
||||
dragAndDrop.off('error', onDragAndDropError);
|
||||
};
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
||||
module.exports = ServicesToaster;
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
|
||||
:global {
|
||||
@import (once, less) '~stremio/common/animations.less';
|
||||
@import (once, less) '~stremio-router/styles.css';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const ModalDialog = require('stremio/common/ModalDialog');
|
||||
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
|
||||
const { useServices } = require('stremio/services');
|
||||
const AddonDetailsWithRemoteAndLocalAddon = withRemoteAndLocalAddon(require('./AddonDetails'));
|
||||
const useAddonDetails = require('./useAddonDetails');
|
||||
|
|
@ -144,4 +145,18 @@ AddonDetailsModal.propTypes = {
|
|||
onCloseRequest: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports = AddonDetailsModal;
|
||||
const AddonDetailsModalFallback = ({ onCloseRequest }) => (
|
||||
<ModalDialog
|
||||
className={styles['addon-details-modal-container']}
|
||||
title={'Stremio addon'}
|
||||
onCloseRequest={onCloseRequest}
|
||||
>
|
||||
<div className={styles['addon-details-message-container']}>
|
||||
Loading addon manifest
|
||||
</div>
|
||||
</ModalDialog>
|
||||
);
|
||||
|
||||
AddonDetailsModalFallback.propTypes = AddonDetailsModal.propTypes;
|
||||
|
||||
module.exports = withCoreSuspender(AddonDetailsModal, AddonDetailsModalFallback);
|
||||
|
|
|
|||
|
|
@ -3,12 +3,6 @@
|
|||
const React = require('react');
|
||||
const useModelState = require('stremio/common/useModelState');
|
||||
|
||||
const init = () => ({
|
||||
selected: null,
|
||||
localAddon: null,
|
||||
remoteAddon: null
|
||||
});
|
||||
|
||||
const useAddonDetails = (transportUrl) => {
|
||||
const action = React.useMemo(() => {
|
||||
if (typeof transportUrl === 'string') {
|
||||
|
|
@ -27,7 +21,7 @@ const useAddonDetails = (transportUrl) => {
|
|||
};
|
||||
}
|
||||
}, [transportUrl]);
|
||||
return useModelState({ model: 'addon_details', action, init });
|
||||
return useModelState({ model: 'addon_details', action });
|
||||
};
|
||||
|
||||
module.exports = useAddonDetails;
|
||||
|
|
|
|||
78
src/common/CoreSuspender.js
Normal file
78
src/common/CoreSuspender.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const { useServices } = require('stremio/services');
|
||||
|
||||
const CoreSuspenderContext = React.createContext(null);
|
||||
|
||||
CoreSuspenderContext.displayName = 'CoreSuspenderContext';
|
||||
|
||||
function wrapPromise(promise) {
|
||||
let status = 'pending';
|
||||
let result;
|
||||
const suspender = promise.then(
|
||||
(resp) => {
|
||||
status = 'success';
|
||||
result = resp;
|
||||
},
|
||||
(error) => {
|
||||
status = 'error';
|
||||
result = error;
|
||||
}
|
||||
);
|
||||
return {
|
||||
read() {
|
||||
if (status === 'pending') {
|
||||
throw suspender;
|
||||
} else if (status === 'error') {
|
||||
throw result;
|
||||
} else if (status === 'success') {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const useCoreSuspender = () => {
|
||||
return React.useContext(CoreSuspenderContext);
|
||||
};
|
||||
|
||||
const withCoreSuspender = (Component, Fallback = () => { }) => {
|
||||
return function withCoreSuspender(props) {
|
||||
const { core } = useServices();
|
||||
const parentSuspender = useCoreSuspender();
|
||||
const [render, setRender] = React.useState(parentSuspender === null);
|
||||
const statesRef = React.useRef({});
|
||||
const streamsRef = React.useRef({});
|
||||
const getState = React.useCallback((model) => {
|
||||
if (!statesRef.current[model]) {
|
||||
statesRef.current[model] = wrapPromise(core.transport.getState(model));
|
||||
}
|
||||
|
||||
return statesRef.current[model].read();
|
||||
}, []);
|
||||
const decodeStream = React.useCallback((stream) => {
|
||||
if (!streamsRef.current[stream]) {
|
||||
streamsRef.current[stream] = wrapPromise(core.transport.decodeStream(stream));
|
||||
}
|
||||
|
||||
return streamsRef.current[stream].read();
|
||||
}, []);
|
||||
const suspender = React.useMemo(() => ({ getState, decodeStream }), []);
|
||||
React.useLayoutEffect(() => {
|
||||
if (!render) {
|
||||
setRender(true);
|
||||
}
|
||||
}, []);
|
||||
return render ?
|
||||
<React.Suspense fallback={<Fallback {...props} />}>
|
||||
<CoreSuspenderContext.Provider value={suspender}>
|
||||
<Component {...props} />
|
||||
</CoreSuspenderContext.Provider>
|
||||
</React.Suspense>
|
||||
:
|
||||
null;
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = { withCoreSuspender, useCoreSuspender };
|
||||
23
src/common/DelayedRenderer/DelayedRenderer.js
Normal file
23
src/common/DelayedRenderer/DelayedRenderer.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
const DelayedRenderer = ({ children, delay }) => {
|
||||
const [render, setRender] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setRender(true);
|
||||
}, delay);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, []);
|
||||
return render ? children : null;
|
||||
};
|
||||
|
||||
DelayedRenderer.propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
module.exports = DelayedRenderer;
|
||||
5
src/common/DelayedRenderer/index.js
Normal file
5
src/common/DelayedRenderer/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const DelayedRenderer = require('./DelayedRenderer');
|
||||
|
||||
module.exports = DelayedRenderer;
|
||||
|
|
@ -3,22 +3,14 @@
|
|||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const Icon = require('@stremio/stremio-icons/dom');
|
||||
const { useRouteFocused } = require('stremio-router');
|
||||
const { useServices } = require('stremio/services');
|
||||
const Button = require('stremio/common/Button');
|
||||
const Popup = require('stremio/common/Popup');
|
||||
const useBinaryState = require('stremio/common/useBinaryState');
|
||||
const useFullscreen = require('stremio/common/useFullscreen');
|
||||
const useProfile = require('stremio/common/useProfile');
|
||||
const styles = require('./styles');
|
||||
const NavMenuContent = require('./NavMenuContent');
|
||||
|
||||
const NavMenu = (props) => {
|
||||
const { core } = useServices();
|
||||
const routeFocused = useRouteFocused();
|
||||
const profile = useProfile();
|
||||
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
|
||||
const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen();
|
||||
const popupLabelOnClick = React.useCallback((event) => {
|
||||
if (!event.nativeEvent.togglePopupPrevented) {
|
||||
toggleMenu();
|
||||
|
|
@ -27,14 +19,6 @@ const NavMenu = (props) => {
|
|||
const popupMenuOnClick = React.useCallback((event) => {
|
||||
event.nativeEvent.togglePopupPrevented = true;
|
||||
}, []);
|
||||
const logoutButtonOnClick = React.useCallback(() => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'Logout'
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
const renderLabel = React.useMemo(() => ({ ref, className, children }) => (
|
||||
props.renderLabel({
|
||||
ref,
|
||||
|
|
@ -44,65 +28,8 @@ const NavMenu = (props) => {
|
|||
})
|
||||
), [menuOpen, popupLabelOnClick, props.renderLabel]);
|
||||
const renderMenu = React.useCallback(() => (
|
||||
<div className={styles['nav-menu-container']} onClick={popupMenuOnClick}>
|
||||
<div className={styles['user-info-container']}>
|
||||
<div
|
||||
className={styles['avatar-container']}
|
||||
style={{
|
||||
backgroundImage: profile.auth === null ?
|
||||
`url('${require('/images/anonymous.png')}')`
|
||||
:
|
||||
`url('${profile.auth.user.avatar}'), url('${require('/images/default_avatar.png')}')`
|
||||
}}
|
||||
/>
|
||||
<div className={styles['email-container']}>
|
||||
<div className={styles['email-label']}>{profile.auth === null ? 'Anonymous user' : profile.auth.user.email}</div>
|
||||
</div>
|
||||
<Button className={styles['logout-button-container']} title={profile.auth === null ? 'Log in / Sign up' : 'Log out'} href={'#/intro'} onClick={logoutButtonOnClick}>
|
||||
<div className={styles['logout-label']}>{profile.auth === null ? 'Log in / Sign up' : 'Log out'}</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles['nav-menu-section']}>
|
||||
<Button className={styles['nav-menu-option-container']} title={fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'} onClick={fullscreen ? exitFullscreen : requestFullscreen}>
|
||||
<Icon className={styles['icon']} icon={fullscreen ? 'ic_exit_fullscreen' : 'ic_fullscreen'} />
|
||||
<div className={styles['nav-menu-option-label']}>{fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'}</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles['nav-menu-section']}>
|
||||
<Button className={styles['nav-menu-option-container']} title={'Settings'} href={'#/settings'}>
|
||||
<Icon className={styles['icon']} icon={'ic_settings'} />
|
||||
<div className={styles['nav-menu-option-label']}>Settings</div>
|
||||
</Button>
|
||||
<Button className={styles['nav-menu-option-container']} title={'Addons'} href={'#/addons'}>
|
||||
<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}>
|
||||
<Icon className={styles['icon']} icon={'ic_magnet'} />
|
||||
<div className={styles['nav-menu-option-label']}>Play Magnet Link</div>
|
||||
</Button>
|
||||
<Button className={styles['nav-menu-option-container']} title={'Help & Feedback'} href={'https://stremio.zendesk.com/'} target={'_blank'}>
|
||||
<Icon className={styles['icon']} icon={'ic_help'} />
|
||||
<div className={styles['nav-menu-option-label']}>Help & Feedback</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles['nav-menu-section']}>
|
||||
<Button className={styles['nav-menu-option-container']} title={'Terms of Service'} href={'https://www.stremio.com/tos'} target={'_blank'}>
|
||||
<div className={styles['nav-menu-option-label']}>Terms of Service</div>
|
||||
</Button>
|
||||
<Button className={styles['nav-menu-option-container']} title={'Privacy Policy'} href={'https://www.stremio.com/privacy'} target={'_blank'}>
|
||||
<div className={styles['nav-menu-option-label']}>Privacy Policy</div>
|
||||
</Button>
|
||||
<Button className={styles['nav-menu-option-container']} title={'About Stremio'} href={'https://www.stremio.com/'} target={'_blank'}>
|
||||
<div className={styles['nav-menu-option-label']}>About Stremio</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
), [profile, fullscreen]);
|
||||
<NavMenuContent onClick={popupMenuOnClick} />
|
||||
), []);
|
||||
React.useEffect(() => {
|
||||
if (!routeFocused) {
|
||||
closeMenu();
|
||||
|
|
@ -111,6 +38,7 @@ const NavMenu = (props) => {
|
|||
return (
|
||||
<Popup
|
||||
open={menuOpen}
|
||||
direction={'bottom-left'}
|
||||
onCloseRequest={closeMenu}
|
||||
renderLabel={renderLabel}
|
||||
renderMenu={renderMenu}
|
||||
|
|
|
|||
102
src/common/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js
Normal file
102
src/common/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// 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 { 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({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'Logout'
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
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']}>
|
||||
<div
|
||||
className={styles['avatar-container']}
|
||||
style={{
|
||||
backgroundImage: profile.auth === null ?
|
||||
`url('${require('/images/anonymous.png')}')`
|
||||
:
|
||||
`url('${profile.auth.user.avatar}'), url('${require('/images/default_avatar.png')}')`
|
||||
}}
|
||||
/>
|
||||
<div className={styles['email-container']}>
|
||||
<div className={styles['email-label']}>{profile.auth === null ? 'Anonymous user' : profile.auth.user.email}</div>
|
||||
</div>
|
||||
<Button className={styles['logout-button-container']} title={profile.auth === null ? 'Log in / Sign up' : 'Log out'} href={'#/intro'} onClick={logoutButtonOnClick}>
|
||||
<div className={styles['logout-label']}>{profile.auth === null ? 'Log in / Sign up' : 'Log out'}</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles['nav-menu-section']}>
|
||||
<Button className={styles['nav-menu-option-container']} title={fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'} onClick={fullscreen ? exitFullscreen : requestFullscreen}>
|
||||
<Icon className={styles['icon']} icon={fullscreen ? 'ic_exit_fullscreen' : 'ic_fullscreen'} />
|
||||
<div className={styles['nav-menu-option-label']}>{fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'}</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles['nav-menu-section']}>
|
||||
<Button className={styles['nav-menu-option-container']} title={'Settings'} href={'#/settings'}>
|
||||
<Icon className={styles['icon']} icon={'ic_settings'} />
|
||||
<div className={styles['nav-menu-option-label']}>Settings</div>
|
||||
</Button>
|
||||
<Button className={styles['nav-menu-option-container']} title={'Addons'} href={'#/addons'}>
|
||||
<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={'Play Magnet Link'} onClick={onPlayMagnetLinkClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_magnet'} />
|
||||
<div className={styles['nav-menu-option-label']}>Play Magnet Link</div>
|
||||
</Button>
|
||||
<Button className={styles['nav-menu-option-container']} title={'Help & Feedback'} href={'https://stremio.zendesk.com/'} target={'_blank'}>
|
||||
<Icon className={styles['icon']} icon={'ic_help'} />
|
||||
<div className={styles['nav-menu-option-label']}>Help & Feedback</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles['nav-menu-section']}>
|
||||
<Button className={styles['nav-menu-option-container']} title={'Terms of Service'} href={'https://www.stremio.com/tos'} target={'_blank'}>
|
||||
<div className={styles['nav-menu-option-label']}>Terms of Service</div>
|
||||
</Button>
|
||||
<Button className={styles['nav-menu-option-container']} title={'Privacy Policy'} href={'https://www.stremio.com/privacy'} target={'_blank'}>
|
||||
<div className={styles['nav-menu-option-label']}>Privacy Policy</div>
|
||||
</Button>
|
||||
<Button className={styles['nav-menu-option-container']} title={'About Stremio'} href={'https://www.stremio.com/'} target={'_blank'}>
|
||||
<div className={styles['nav-menu-option-label']}>About Stremio</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NavMenuContent.propTypes = {
|
||||
onClick: PropTypes.func
|
||||
};
|
||||
|
||||
const NavMenuContentFallback = () => (
|
||||
<div className={styles['nav-menu-container']} />
|
||||
);
|
||||
|
||||
module.exports = withCoreSuspender(NavMenuContent, NavMenuContentFallback);
|
||||
|
|
@ -7,16 +7,25 @@ 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 routeFocused = useRouteFocused();
|
||||
const { createTorrentFromMagnet } = useTorrent();
|
||||
const searchInputRef = React.useRef(null);
|
||||
const searchBarOnClick = React.useCallback(() => {
|
||||
if (!active) {
|
||||
window.location = '#/search';
|
||||
}
|
||||
}, [active]);
|
||||
const queryInputOnChange = React.useCallback(() => {
|
||||
try {
|
||||
createTorrentFromMagnet(searchInputRef.current.value);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch { }
|
||||
}, []);
|
||||
const queryInputOnSubmit = React.useCallback(() => {
|
||||
if (searchInputRef.current !== null) {
|
||||
const queryParams = new URLSearchParams([['search', searchInputRef.current.value]]);
|
||||
|
|
@ -40,6 +49,7 @@ const SearchBar = ({ className, query, active }) => {
|
|||
placeholder={'Search or paste link'}
|
||||
defaultValue={query}
|
||||
tabIndex={-1}
|
||||
onChange={queryInputOnChange}
|
||||
onSubmit={queryInputOnSubmit}
|
||||
/>
|
||||
:
|
||||
|
|
@ -60,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);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const PropTypes = require('prop-types');
|
|||
const classnames = require('classnames');
|
||||
const Button = require('stremio/common/Button');
|
||||
const useProfile = require('stremio/common/useProfile');
|
||||
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
|
||||
const styles = require('./styles');
|
||||
|
||||
const StreamingServerWarning = ({ className }) => {
|
||||
|
|
@ -39,6 +40,12 @@ const StreamingServerWarning = ({ className }) => {
|
|||
}
|
||||
});
|
||||
}, [profile.settings]);
|
||||
|
||||
if (!isNaN(profile.settings.streamingServerWarningDismissed.getTime()) &&
|
||||
profile.settings.streamingServerWarningDismissed.getTime() > Date.now()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classnames(className, styles['warning-container'])}>
|
||||
<div className={styles['warning-statement']}>Streaming server is not available.</div>
|
||||
|
|
@ -56,4 +63,4 @@ StreamingServerWarning.propTypes = {
|
|||
className: PropTypes.string
|
||||
};
|
||||
|
||||
module.exports = StreamingServerWarning;
|
||||
module.exports = withCoreSuspender(StreamingServerWarning);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
22
src/common/animations.less
Normal file
22
src/common/animations.less
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
:global(.animation-fade-in) {
|
||||
:local {
|
||||
animation-name: fade-in;
|
||||
}
|
||||
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-duration: 100ms;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0.6;
|
||||
transform: translateY(0.2vh);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ const AddonDetailsModal = require('./AddonDetailsModal');
|
|||
const Button = require('./Button');
|
||||
const Checkbox = require('./Checkbox');
|
||||
const ColorInput = require('./ColorInput');
|
||||
const DelayedRenderer = require('./DelayedRenderer');
|
||||
const Image = require('./Image');
|
||||
const LibItem = require('./LibItem');
|
||||
const MainNavBars = require('./MainNavBars');
|
||||
|
|
@ -24,27 +25,27 @@ const TextInput = require('./TextInput');
|
|||
const { ToastProvider, useToast } = require('./Toast');
|
||||
const comparatorWithPriorities = require('./comparatorWithPriorities');
|
||||
const CONSTANTS = require('./CONSTANTS');
|
||||
const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender');
|
||||
const getVisibleChildrenRange = require('./getVisibleChildrenRange');
|
||||
const languageNames = require('./languageNames');
|
||||
const routesRegexp = require('./routesRegexp');
|
||||
const sanitizeLocationPath = require('./sanitizeLocationPath');
|
||||
const useAnimationFrame = require('./useAnimationFrame');
|
||||
const useBinaryState = require('./useBinaryState');
|
||||
const useDeepEqualEffect = require('./useDeepEqualEffect');
|
||||
const useDeepEqualMemo = require('./useDeepEqualMemo');
|
||||
const useDeepEqualState = require('./useDeepEqualState');
|
||||
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,
|
||||
Button,
|
||||
Checkbox,
|
||||
ColorInput,
|
||||
DelayedRenderer,
|
||||
Image,
|
||||
LibItem,
|
||||
MainNavBars,
|
||||
|
|
@ -67,19 +68,19 @@ module.exports = {
|
|||
useToast,
|
||||
comparatorWithPriorities,
|
||||
CONSTANTS,
|
||||
withCoreSuspender,
|
||||
useCoreSuspender,
|
||||
getVisibleChildrenRange,
|
||||
languageNames,
|
||||
routesRegexp,
|
||||
sanitizeLocationPath,
|
||||
useAnimationFrame,
|
||||
useBinaryState,
|
||||
useDeepEqualEffect,
|
||||
useDeepEqualMemo,
|
||||
useDeepEqualState,
|
||||
useFullscreen,
|
||||
useLiveRef,
|
||||
useModelState,
|
||||
useOnScrollToBottom,
|
||||
useProfile,
|
||||
useStreamingServer
|
||||
useStreamingServer,
|
||||
useTorrent
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const routesRegexp = {
|
|||
urlParamsNames: []
|
||||
},
|
||||
metadetails: {
|
||||
regexp: /^\/metadetails\/([^/]*)\/([^/]*)(?:\/([^/]*))?$/,
|
||||
regexp: /^\/(?:metadetails|detail)\/([^/]*)\/([^/]*)(?:\/([^/]*))?$/,
|
||||
urlParamsNames: ['type', 'id', 'videoId']
|
||||
},
|
||||
addons: {
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const UrlUtils = require('url');
|
||||
const routesRegexp = require('stremio/common/routesRegexp');
|
||||
|
||||
const sanitizeLocationPath = (path) => {
|
||||
const { href, pathname, search } = UrlUtils.parse(path);
|
||||
if (typeof pathname === 'string') {
|
||||
const matches = pathname.match(routesRegexp.player.regexp);
|
||||
if (matches) {
|
||||
if (typeof matches[2] === 'string') {
|
||||
return `/player/***/***/${matches[3]}/${matches[4]}/${matches[5]}/${matches[6]}${typeof search === 'string' ? search : ''}`;
|
||||
} else {
|
||||
return `/player/***${typeof search === 'string' ? search : ''}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return href;
|
||||
};
|
||||
|
||||
module.exports = sanitizeLocationPath;
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const useDeepEqualMemo = require('stremio/common/useDeepEqualMemo');
|
||||
|
||||
const useDeepEqualEffect = (cb, deps) => {
|
||||
React.useEffect(cb, [useDeepEqualMemo(() => ({}), deps)]);
|
||||
};
|
||||
|
||||
module.exports = useDeepEqualEffect;
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const isEqual = require('lodash.isequal');
|
||||
|
||||
const useDeepEqualState = (initialState) => {
|
||||
return React.useReducer(
|
||||
(prevState, nextState) => {
|
||||
return isEqual(prevState, nextState) ?
|
||||
prevState
|
||||
:
|
||||
nextState;
|
||||
},
|
||||
undefined,
|
||||
() => {
|
||||
return typeof initialState === 'function' ?
|
||||
initialState()
|
||||
:
|
||||
initialState;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = useDeepEqualState;
|
||||
|
|
@ -3,16 +3,19 @@
|
|||
const React = require('react');
|
||||
const throttle = require('lodash.throttle');
|
||||
const isEqual = require('lodash.isequal');
|
||||
const intersection = require('lodash.intersection');
|
||||
const { useCoreSuspender } = require('stremio/common/CoreSuspender');
|
||||
const { useRouteFocused } = require('stremio-router');
|
||||
const { useServices } = require('stremio/services');
|
||||
|
||||
const useModelState = ({ init, action, ...args }) => {
|
||||
const useModelState = ({ action, ...args }) => {
|
||||
const { core } = useServices();
|
||||
const routeFocused = useRouteFocused();
|
||||
const mountedRef = React.useRef(false);
|
||||
const [model, timeout, map] = React.useMemo(() => {
|
||||
return [args.model, args.timeout, args.map];
|
||||
const [model, timeout, map, deps] = React.useMemo(() => {
|
||||
return [args.model, args.timeout, args.map, args.deps];
|
||||
}, []);
|
||||
const { getState } = useCoreSuspender();
|
||||
const [state, setState] = React.useReducer(
|
||||
(prevState, nextState) => {
|
||||
return Object.keys(prevState).reduce((result, key) => {
|
||||
|
|
@ -22,35 +25,41 @@ const useModelState = ({ init, action, ...args }) => {
|
|||
},
|
||||
undefined,
|
||||
() => {
|
||||
return typeof init === 'function' ?
|
||||
init()
|
||||
:
|
||||
init;
|
||||
if (typeof map === 'function') {
|
||||
return map(getState(model));
|
||||
} else {
|
||||
return getState(model);
|
||||
}
|
||||
}
|
||||
);
|
||||
React.useLayoutEffect(() => {
|
||||
React.useInsertionEffect(() => {
|
||||
if (action) {
|
||||
core.transport.dispatch(action, model);
|
||||
}
|
||||
}, [action]);
|
||||
React.useLayoutEffect(() => {
|
||||
React.useInsertionEffect(() => {
|
||||
return () => {
|
||||
core.transport.dispatch({ action: 'Unload' }, model);
|
||||
};
|
||||
}, []);
|
||||
React.useLayoutEffect(() => {
|
||||
const onNewStateThrottled = throttle(() => {
|
||||
const state = core.transport.getState(model);
|
||||
React.useInsertionEffect(() => {
|
||||
const onNewState = async (models) => {
|
||||
if (models.indexOf(model) === -1 && (!Array.isArray(deps) || intersection(deps, models).length === 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = await core.transport.getState(model);
|
||||
if (typeof map === 'function') {
|
||||
setState(map(state));
|
||||
} else {
|
||||
setState(state);
|
||||
}
|
||||
}, timeout);
|
||||
};
|
||||
const onNewStateThrottled = throttle(onNewState, timeout);
|
||||
if (routeFocused) {
|
||||
core.transport.on('NewState', onNewStateThrottled);
|
||||
if (mountedRef.current) {
|
||||
onNewStateThrottled.call();
|
||||
onNewState([model]);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
|
|
@ -58,7 +67,7 @@ const useModelState = ({ init, action, ...args }) => {
|
|||
core.transport.off('NewState', onNewStateThrottled);
|
||||
};
|
||||
}, [routeFocused]);
|
||||
React.useLayoutEffect(() => {
|
||||
React.useInsertionEffect(() => {
|
||||
mountedRef.current = true;
|
||||
}, []);
|
||||
return state;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const { useServices } = require('stremio/services');
|
||||
const useModelState = require('stremio/common/useModelState');
|
||||
|
||||
const map = (ctx) => ({
|
||||
|
|
@ -18,12 +16,7 @@ const map = (ctx) => ({
|
|||
});
|
||||
|
||||
const useProfile = () => {
|
||||
const { core } = useServices();
|
||||
const init = React.useCallback(() => {
|
||||
const ctx = core.transport.getState('ctx');
|
||||
return map(ctx);
|
||||
}, []);
|
||||
return useModelState({ model: 'ctx', init, map });
|
||||
return useModelState({ model: 'ctx', map });
|
||||
};
|
||||
|
||||
module.exports = useProfile;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,9 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const { useServices } = require('stremio/services');
|
||||
const useModelState = require('stremio/common/useModelState');
|
||||
|
||||
const useStreamingServer = () => {
|
||||
const { core } = useServices();
|
||||
const init = React.useCallback(() => {
|
||||
return core.transport.getState('streaming_server');
|
||||
}, []);
|
||||
return useModelState({ model: 'streaming_server', init });
|
||||
return useModelState({ model: 'streaming_server' });
|
||||
};
|
||||
|
||||
module.exports = useStreamingServer;
|
||||
|
|
|
|||
50
src/common/useTorrent.js
Normal file
50
src/common/useTorrent.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const magnet = require('magnet-uri');
|
||||
const { useServices } = require('stremio/services');
|
||||
const useToast = require('stremio/common/Toast/useToast');
|
||||
const useStreamingServer = require('stremio/common/useStreamingServer');
|
||||
|
||||
const useTorrent = () => {
|
||||
const { core } = useServices();
|
||||
const streamingServer = useStreamingServer();
|
||||
const toast = useToast();
|
||||
const createTorrentTimeout = React.useRef(null);
|
||||
const createTorrentFromMagnet = React.useCallback((text) => {
|
||||
const parsed = magnet.decode(text);
|
||||
if (parsed && typeof parsed.infoHash === 'string') {
|
||||
core.transport.dispatch({
|
||||
action: 'StreamingServer',
|
||||
args: {
|
||||
action: 'CreateTorrent',
|
||||
args: text
|
||||
}
|
||||
});
|
||||
clearTimeout(createTorrentTimeout.current);
|
||||
createTorrentTimeout.current = setTimeout(() => {
|
||||
toast.show({
|
||||
type: 'error',
|
||||
title: 'It\'s taking a long time to get metadata from the torrent.',
|
||||
timeout: 10000
|
||||
});
|
||||
}, 10000);
|
||||
}
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
if (streamingServer.torrent !== null) {
|
||||
const [, { type }] = streamingServer.torrent;
|
||||
if (type === 'Ready') {
|
||||
clearTimeout(createTorrentTimeout.current);
|
||||
}
|
||||
}
|
||||
}, [streamingServer.torrent]);
|
||||
React.useEffect(() => {
|
||||
return () => clearTimeout(createTorrentTimeout.current);
|
||||
}, []);
|
||||
return {
|
||||
createTorrentFromMagnet
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = useTorrent;
|
||||
|
|
@ -4,6 +4,8 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">
|
||||
<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="manifest" href="<%= htmlWebpackPlugin.options.manifestPath %>" />
|
||||
<meta name="theme-color" content="#2a2843">
|
||||
|
|
|
|||
|
|
@ -12,10 +12,11 @@ if (browser?.platform?.type === 'desktop') {
|
|||
}
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
const ReactDOM = require('react-dom/client');
|
||||
const App = require('./App');
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('app'));
|
||||
const root = ReactDOM.createRoot(document.getElementById('app'));
|
||||
root.render(<App />);
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
const React = require('react');
|
||||
|
||||
const RouteFocusedContext = React.createContext(false);
|
||||
const RouteFocusedContext = React.createContext(true);
|
||||
|
||||
RouteFocusedContext.displayName = 'RouteFocusedContext';
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const Icon = require('@stremio/stremio-icons/dom');
|
||||
const { AddonDetailsModal, Button, Image, Multiselect, MainNavBars, TextInput, SearchBar, SharePrompt, ModalDialog, useBinaryState } = require('stremio/common');
|
||||
const { AddonDetailsModal, Button, Image, Multiselect, MainNavBars, TextInput, SearchBar, SharePrompt, ModalDialog, useBinaryState, withCoreSuspender } = require('stremio/common');
|
||||
const Addon = require('./Addon');
|
||||
const useInstalledAddons = require('./useInstalledAddons');
|
||||
const useRemoteAddons = require('./useRemoteAddons');
|
||||
|
|
@ -118,7 +119,7 @@ const Addons = ({ urlParams, queryParams }) => {
|
|||
.map((addon, index) => (
|
||||
<Addon
|
||||
key={index}
|
||||
className={styles['addon']}
|
||||
className={classnames(styles['addon'], 'animation-fade-in')}
|
||||
id={addon.manifest.id}
|
||||
name={addon.manifest.name}
|
||||
version={addon.manifest.version}
|
||||
|
|
@ -152,7 +153,7 @@ const Addons = ({ urlParams, queryParams }) => {
|
|||
.map((addon, index) => (
|
||||
<Addon
|
||||
key={index}
|
||||
className={styles['addon']}
|
||||
className={classnames(styles['addon'], 'animation-fade-in')}
|
||||
id={addon.manifest.id}
|
||||
name={addon.manifest.name}
|
||||
version={addon.manifest.version}
|
||||
|
|
@ -261,4 +262,8 @@ Addons.propTypes = {
|
|||
queryParams: PropTypes.instanceOf(URLSearchParams)
|
||||
};
|
||||
|
||||
module.exports = Addons;
|
||||
const AddonsFallback = () => (
|
||||
<MainNavBars className={styles['addons-container']} route={'addons'} />
|
||||
);
|
||||
|
||||
module.exports = withCoreSuspender(Addons, AddonsFallback);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { useModelState } = require('stremio/common');
|
||||
|
||||
const useInstalledAddons = (urlParams) => {
|
||||
const { core } = useServices();
|
||||
const init = React.useMemo(() => {
|
||||
return core.transport.getState('installed_addons');
|
||||
}, []);
|
||||
const action = React.useMemo(() => {
|
||||
if (typeof urlParams.transportUrl !== 'string' && typeof urlParams.catalogId !== 'string') {
|
||||
return {
|
||||
|
|
@ -28,7 +23,7 @@ const useInstalledAddons = (urlParams) => {
|
|||
};
|
||||
}
|
||||
}, [urlParams]);
|
||||
return useModelState({ model: 'installed_addons', action, init });
|
||||
return useModelState({ model: 'installed_addons', action });
|
||||
};
|
||||
|
||||
module.exports = useInstalledAddons;
|
||||
|
|
|
|||
|
|
@ -3,15 +3,6 @@
|
|||
const React = require('react');
|
||||
const { useModelState } = require('stremio/common');
|
||||
|
||||
const init = () => ({
|
||||
selected: null,
|
||||
selectable: {
|
||||
catalogs: [],
|
||||
types: []
|
||||
},
|
||||
catalog: null,
|
||||
});
|
||||
|
||||
const useRemoteAddons = (urlParams) => {
|
||||
const action = React.useMemo(() => {
|
||||
if (typeof urlParams.type === 'string' && typeof urlParams.transportUrl === 'string' && typeof urlParams.catalogId === 'string') {
|
||||
|
|
@ -38,7 +29,7 @@ const useRemoteAddons = (urlParams) => {
|
|||
};
|
||||
}
|
||||
}, [urlParams]);
|
||||
return useModelState({ model: 'remote_addons', action, init });
|
||||
return useModelState({ model: 'remote_addons', action, deps: ['ctx'] });
|
||||
};
|
||||
|
||||
module.exports = useRemoteAddons;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
const React = require('react');
|
||||
const classnames = require('classnames');
|
||||
const debounce = require('lodash.debounce');
|
||||
const { MainNavBars, MetaRow, LibItem, MetaItem, StreamingServerWarning, useProfile, useStreamingServer, getVisibleChildrenRange } = require('stremio/common');
|
||||
const { MainNavBars, MetaRow, LibItem, MetaItem, StreamingServerWarning, useStreamingServer, withCoreSuspender, getVisibleChildrenRange } = require('stremio/common');
|
||||
const useBoard = require('./useBoard');
|
||||
const useContinueWatchingPreview = require('./useContinueWatchingPreview');
|
||||
const styles = require('./styles');
|
||||
|
|
@ -11,7 +11,6 @@ const styles = require('./styles');
|
|||
const THRESHOLD = 5;
|
||||
|
||||
const Board = () => {
|
||||
const profile = useProfile();
|
||||
const streamingServer = useStreamingServer();
|
||||
const continueWatchingPreview = useContinueWatchingPreview();
|
||||
const [board, loadBoardRows] = useBoard();
|
||||
|
|
@ -42,7 +41,7 @@ const Board = () => {
|
|||
{
|
||||
continueWatchingPreview.libraryItems.length > 0 ?
|
||||
<MetaRow
|
||||
className={classnames(styles['board-row'], styles['continue-watching-row'])}
|
||||
className={classnames(styles['board-row'], styles['continue-watching-row'], 'animation-fade-in')}
|
||||
title={'Continue Watching'}
|
||||
items={continueWatchingPreview.libraryItems}
|
||||
itemComponent={LibItem}
|
||||
|
|
@ -57,7 +56,7 @@ const Board = () => {
|
|||
return (
|
||||
<MetaRow
|
||||
key={index}
|
||||
className={classnames(styles['board-row'], styles[`board-row-${catalog.content.content[0].posterShape}`])}
|
||||
className={classnames(styles['board-row'], styles[`board-row-${catalog.content.content[0].posterShape}`], 'animation-fade-in')}
|
||||
title={catalog.title}
|
||||
items={catalog.content.content}
|
||||
itemComponent={MetaItem}
|
||||
|
|
@ -69,7 +68,7 @@ const Board = () => {
|
|||
return (
|
||||
<MetaRow
|
||||
key={index}
|
||||
className={styles['board-row']}
|
||||
className={classnames(styles['board-row'], 'animation-fade-in')}
|
||||
title={catalog.title}
|
||||
message={catalog.content.content}
|
||||
deepLinks={catalog.deepLinks}
|
||||
|
|
@ -80,7 +79,7 @@ const Board = () => {
|
|||
return (
|
||||
<MetaRow.Placeholder
|
||||
key={index}
|
||||
className={classnames(styles['board-row'], styles['board-row-poster'])}
|
||||
className={classnames(styles['board-row'], styles['board-row-poster'], 'animation-fade-in')}
|
||||
title={catalog.title}
|
||||
deepLinks={catalog.deepLinks}
|
||||
/>
|
||||
|
|
@ -91,8 +90,7 @@ const Board = () => {
|
|||
</div>
|
||||
</MainNavBars>
|
||||
{
|
||||
streamingServer.settings !== null && streamingServer.settings.type === 'Err' &&
|
||||
(isNaN(profile.settings.streamingServerWarningDismissed.getTime()) || profile.settings.streamingServerWarningDismissed.getTime() < Date.now()) ?
|
||||
streamingServer.settings !== null && streamingServer.settings.type === 'Err' ?
|
||||
<StreamingServerWarning className={styles['board-warning-container']} />
|
||||
:
|
||||
null
|
||||
|
|
@ -101,4 +99,10 @@ const Board = () => {
|
|||
);
|
||||
};
|
||||
|
||||
module.exports = Board;
|
||||
const BoardFallback = () => (
|
||||
<div className={styles['board-container']}>
|
||||
<MainNavBars className={styles['board-content-container']} route={'board'} />
|
||||
</div>
|
||||
);
|
||||
|
||||
module.exports = withCoreSuspender(Board, BoardFallback);
|
||||
|
|
|
|||
|
|
@ -4,11 +4,6 @@ const React = require('react');
|
|||
const { useServices } = require('stremio/services');
|
||||
const { useModelState } = require('stremio/common');
|
||||
|
||||
const init = () => ({
|
||||
selected: null,
|
||||
catalogs: []
|
||||
});
|
||||
|
||||
const useBoard = () => {
|
||||
const { core } = useServices();
|
||||
const action = React.useMemo(() => ({
|
||||
|
|
@ -27,7 +22,7 @@ const useBoard = () => {
|
|||
}
|
||||
}, 'board');
|
||||
}, []);
|
||||
const board = useModelState({ model: 'board', timeout: 1500, action, init });
|
||||
const board = useModelState({ model: 'board', timeout: 1500, action });
|
||||
return [board, loadRange];
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,9 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { useModelState } = require('stremio/common');
|
||||
|
||||
const useContinueWatchingPreview = () => {
|
||||
const { core } = useServices();
|
||||
const init = React.useMemo(() => {
|
||||
return core.transport.getState('continue_watching_preview');
|
||||
}, []);
|
||||
return useModelState({ model: 'continue_watching_preview', init });
|
||||
return useModelState({ model: 'continue_watching_preview' });
|
||||
};
|
||||
|
||||
module.exports = useContinueWatchingPreview;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
|
|||
const classnames = require('classnames');
|
||||
const Icon = require('@stremio/stremio-icons/dom');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { AddonDetailsModal, Button, MainNavBars, MetaItem, Image, MetaPreview, Multiselect, ModalDialog, CONSTANTS, useBinaryState, useOnScrollToBottom } = require('stremio/common');
|
||||
const { AddonDetailsModal, DelayedRenderer, Button, MainNavBars, MetaItem, Image, MetaPreview, Multiselect, ModalDialog, CONSTANTS, useBinaryState, useOnScrollToBottom, withCoreSuspender } = require('stremio/common');
|
||||
const useDiscover = require('./useDiscover');
|
||||
const useSelectableInputs = require('./useSelectableInputs');
|
||||
const styles = require('./styles');
|
||||
|
|
@ -75,37 +75,26 @@ const Discover = ({ urlParams, queryParams }) => {
|
|||
closeAddonModal();
|
||||
setSelectedMetaItemIndex(0);
|
||||
}, [discover.selected]);
|
||||
const metaItemsContainerRef = React.useRef();
|
||||
React.useEffect(() => {
|
||||
if (discover.catalog?.content.type === 'Loading') {
|
||||
metaItemsContainerRef.current.scrollTop = 0;
|
||||
}
|
||||
}, [discover.catalog]);
|
||||
return (
|
||||
<MainNavBars className={styles['discover-container']} route={'discover'}>
|
||||
<div className={styles['discover-content']}>
|
||||
<div className={styles['catalog-container']}>
|
||||
{
|
||||
discover.defaultRequest ?
|
||||
<div className={styles['selectable-inputs-container']}>
|
||||
{selectInputs.map(({ title, options, selected, renderLabelText, onSelect }, index) => (
|
||||
<Multiselect
|
||||
key={index}
|
||||
className={styles['select-input']}
|
||||
title={title}
|
||||
options={options}
|
||||
selected={selected}
|
||||
renderLabelText={renderLabelText}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
<Button className={styles['filter-container']} title={'All filters'} onClick={openInputsModal}>
|
||||
<Icon className={styles['filter-icon']} icon={'ic_filter'} />
|
||||
</Button>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles['selectable-inputs-container']}>
|
||||
{selectInputs.map(({ title, options, selected, renderLabelText, onSelect }, index) => (
|
||||
<Multiselect
|
||||
key={index}
|
||||
className={styles['select-input']}
|
||||
title={title}
|
||||
options={options}
|
||||
selected={selected}
|
||||
renderLabelText={renderLabelText}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
<Button className={styles['filter-container']} title={'All filters'} onClick={openInputsModal}>
|
||||
<Icon className={styles['filter-icon']} icon={'ic_filter'} />
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
discover.catalog !== null && !discover.catalog.installed ?
|
||||
<div className={styles['missing-addon-warning-container']}>
|
||||
|
|
@ -119,10 +108,12 @@ const Discover = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
{
|
||||
discover.catalog === null ?
|
||||
<div className={styles['message-container']}>
|
||||
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
|
||||
<div className={styles['message-label']}>No catalog selected!</div>
|
||||
</div>
|
||||
<DelayedRenderer delay={500}>
|
||||
<div className={styles['message-container']}>
|
||||
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
|
||||
<div className={styles['message-label']}>No catalog selected!</div>
|
||||
</div>
|
||||
</DelayedRenderer>
|
||||
:
|
||||
discover.catalog.content.type === 'Err' ?
|
||||
<div className={styles['message-container']}>
|
||||
|
|
@ -131,7 +122,7 @@ const Discover = ({ urlParams, queryParams }) => {
|
|||
</div>
|
||||
:
|
||||
discover.catalog.content.type === 'Loading' ?
|
||||
<div ref={metaItemsContainerRef} className={styles['meta-items-container']}>
|
||||
<div className={classnames(styles['meta-items-container'], 'animation-fade-in')}>
|
||||
{Array(CONSTANTS.CATALOG_PAGE_SIZE).fill(null).map((_, index) => (
|
||||
<div key={index} className={styles['meta-item-placeholder']}>
|
||||
<div className={styles['poster-container']} />
|
||||
|
|
@ -142,7 +133,7 @@ const Discover = ({ urlParams, queryParams }) => {
|
|||
))}
|
||||
</div>
|
||||
:
|
||||
<div ref={metaItemsContainerRef} className={styles['meta-items-container']} onScroll={onScroll} onFocusCapture={metaItemsOnFocusCapture}>
|
||||
<div className={classnames(styles['meta-items-container'], 'animation-fade-in')} onScroll={onScroll} onFocusCapture={metaItemsOnFocusCapture}>
|
||||
{discover.catalog.content.content.map((metaItem, index) => (
|
||||
<MetaItem
|
||||
key={index}
|
||||
|
|
@ -185,7 +176,7 @@ const Discover = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
</div>
|
||||
{
|
||||
inputsModalOpen && discover.defaultRequest ?
|
||||
inputsModalOpen ?
|
||||
<ModalDialog title={'Catalog filters'} className={styles['selectable-inputs-modal']} onCloseRequest={closeInputsModal}>
|
||||
{selectInputs.map(({ title, options, selected, renderLabelText, onSelect }, index) => (
|
||||
<Multiselect
|
||||
|
|
@ -221,4 +212,8 @@ Discover.propTypes = {
|
|||
queryParams: PropTypes.instanceOf(URLSearchParams)
|
||||
};
|
||||
|
||||
module.exports = Discover;
|
||||
const DiscoverFallback = () => (
|
||||
<MainNavBars className={styles['discover-container']} route={'discover'} />
|
||||
);
|
||||
|
||||
module.exports = withCoreSuspender(Discover, DiscoverFallback);
|
||||
|
|
|
|||
|
|
@ -5,18 +5,6 @@ const UrlUtils = require('url');
|
|||
const { useServices } = require('stremio/services');
|
||||
const { useModelState } = require('stremio/common');
|
||||
|
||||
const init = () => ({
|
||||
selected: null,
|
||||
selectable: {
|
||||
types: [],
|
||||
catalogs: [],
|
||||
extra: [],
|
||||
nextPage: false
|
||||
},
|
||||
catalog: null,
|
||||
defaultRequest: null,
|
||||
});
|
||||
|
||||
const map = (discover) => ({
|
||||
...discover,
|
||||
catalog: discover.catalog !== null && discover.catalog.content.type === 'Ready' ?
|
||||
|
|
@ -67,25 +55,20 @@ const useDiscover = (urlParams, queryParams) => {
|
|||
};
|
||||
}
|
||||
} else {
|
||||
const discover = core.transport.getState('discover');
|
||||
if (discover.defaultRequest !== null) {
|
||||
return {
|
||||
action: 'Load',
|
||||
args: {
|
||||
model: 'CatalogWithFilters',
|
||||
args: {
|
||||
request: discover.defaultRequest
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
action: 'Load',
|
||||
args: {
|
||||
model: 'CatalogWithFilters',
|
||||
args: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
action: 'Unload'
|
||||
};
|
||||
}, [urlParams, queryParams]);
|
||||
const discover = useModelState({ model: 'discover', action, map, init });
|
||||
const discover = useModelState({ model: 'discover', action, map, deps: ['ctx'] });
|
||||
return [discover, loadNextPage];
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -186,7 +186,6 @@ const Intro = ({ queryParams }) => {
|
|||
tos: state.termsAccepted,
|
||||
privacy: state.privacyPolicyAccepted,
|
||||
marketing: state.marketingAccepted,
|
||||
time: new Date(),
|
||||
from: 'web'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
|
|||
const classnames = require('classnames');
|
||||
const Icon = require('@stremio/stremio-icons/dom');
|
||||
const NotFound = require('stremio/routes/NotFound');
|
||||
const { Button, Multiselect, MainNavBars, LibItem, Image, ModalDialog, PaginationInput, useProfile, routesRegexp, useBinaryState } = require('stremio/common');
|
||||
const { Button, DelayedRenderer, Multiselect, MainNavBars, LibItem, Image, ModalDialog, PaginationInput, useProfile, routesRegexp, useBinaryState, withCoreSuspender } = require('stremio/common');
|
||||
const useLibrary = require('./useLibrary');
|
||||
const useSelectableInputs = require('./useSelectableInputs');
|
||||
const styles = require('./styles');
|
||||
|
|
@ -85,14 +85,16 @@ const Library = ({ model, urlParams, queryParams }) => {
|
|||
</div>
|
||||
:
|
||||
library.selected === null ?
|
||||
<div className={styles['message-container']}>
|
||||
<Image
|
||||
className={styles['image']}
|
||||
src={require('/images/empty.png')}
|
||||
alt={' '}
|
||||
/>
|
||||
<div className={styles['message-label']}>{model === 'library' ? 'Library' : 'Continue Watching'} not loaded!</div>
|
||||
</div>
|
||||
<DelayedRenderer delay={500}>
|
||||
<div className={styles['message-container']}>
|
||||
<Image
|
||||
className={styles['image']}
|
||||
src={require('/images/empty.png')}
|
||||
alt={' '}
|
||||
/>
|
||||
<div className={styles['message-label']}>{model === 'library' ? 'Library' : 'Continue Watching'} not loaded!</div>
|
||||
</div>
|
||||
</DelayedRenderer>
|
||||
:
|
||||
library.catalog.length === 0 ?
|
||||
<div className={styles['message-container']}>
|
||||
|
|
@ -104,7 +106,7 @@ const Library = ({ model, urlParams, queryParams }) => {
|
|||
<div className={styles['message-label']}>Empty {model === 'library' ? 'Library' : 'Continue Watching'}</div>
|
||||
</div>
|
||||
:
|
||||
<div className={styles['meta-items-container']}>
|
||||
<div className={classnames(styles['meta-items-container'], 'animation-fade-in')}>
|
||||
{library.catalog.map((libItem, index) => (
|
||||
<LibItem {...libItem} removable={model === 'library'} key={index} />
|
||||
))}
|
||||
|
|
@ -132,4 +134,10 @@ Library.propTypes = {
|
|||
queryParams: PropTypes.instanceOf(URLSearchParams)
|
||||
};
|
||||
|
||||
module.exports = withModel(Library);
|
||||
const LibraryFallback = ({ model }) => (
|
||||
<MainNavBars className={styles['library-container']} route={model} />
|
||||
);
|
||||
|
||||
LibraryFallback.propTypes = Library.propTypes;
|
||||
|
||||
module.exports = withModel(withCoreSuspender(Library, LibraryFallback));
|
||||
|
|
|
|||
|
|
@ -3,17 +3,6 @@
|
|||
const React = require('react');
|
||||
const { useModelState } = require('stremio/common');
|
||||
|
||||
const init = () => ({
|
||||
selected: null,
|
||||
selectable: {
|
||||
types: [],
|
||||
sorts: [],
|
||||
prevPage: null,
|
||||
nextPage: null
|
||||
},
|
||||
catalog: []
|
||||
});
|
||||
|
||||
const useLibrary = (model, urlParams, queryParams) => {
|
||||
const action = React.useMemo(() => ({
|
||||
action: 'Load',
|
||||
|
|
@ -28,7 +17,7 @@ const useLibrary = (model, urlParams, queryParams) => {
|
|||
}
|
||||
}
|
||||
}), [urlParams, queryParams]);
|
||||
return useModelState({ model, action, init });
|
||||
return useModelState({ model, action });
|
||||
};
|
||||
|
||||
module.exports = useLibrary;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { VerticalNavBar, HorizontalNavBar, MetaPreview, ModalDialog, Image } = require('stremio/common');
|
||||
const { VerticalNavBar, HorizontalNavBar, MetaPreview, ModalDialog, Image, DelayedRenderer, withCoreSuspender } = require('stremio/common');
|
||||
const StreamsList = require('./StreamsList');
|
||||
const VideosList = require('./VideosList');
|
||||
const useMetaDetails = require('./useMetaDetails');
|
||||
|
|
@ -86,10 +87,12 @@ const MetaDetails = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
{
|
||||
metaPath === null ?
|
||||
<div className={styles['meta-message-container']}>
|
||||
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
|
||||
<div className={styles['message-label']}>No meta was selected!</div>
|
||||
</div>
|
||||
<DelayedRenderer delay={500}>
|
||||
<div className={styles['meta-message-container']}>
|
||||
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
|
||||
<div className={styles['message-label']}>No meta was selected!</div>
|
||||
</div>
|
||||
</DelayedRenderer>
|
||||
:
|
||||
metaDetails.metaItem === null ?
|
||||
<div className={styles['meta-message-container']}>
|
||||
|
|
@ -122,7 +125,7 @@ const MetaDetails = ({ urlParams, queryParams }) => {
|
|||
null
|
||||
}
|
||||
<MetaPreview
|
||||
className={styles['meta-preview']}
|
||||
className={classnames(styles['meta-preview'], 'animation-fade-in')}
|
||||
name={metaDetails.metaItem.content.content.name}
|
||||
logo={metaDetails.metaItem.content.content.logo}
|
||||
runtime={metaDetails.metaItem.content.content.runtime}
|
||||
|
|
@ -188,4 +191,16 @@ MetaDetails.propTypes = {
|
|||
queryParams: PropTypes.instanceOf(URLSearchParams)
|
||||
};
|
||||
|
||||
module.exports = MetaDetails;
|
||||
const MetaDetailsFallback = () => (
|
||||
<div className={styles['metadetails-container']}>
|
||||
<HorizontalNavBar
|
||||
className={styles['nav-bar']}
|
||||
backButton={true}
|
||||
addonsButton={true}
|
||||
fullscreenButton={true}
|
||||
navMenu={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
module.exports = withCoreSuspender(MetaDetails, MetaDetailsFallback);
|
||||
|
|
|
|||
|
|
@ -4,32 +4,70 @@ const React = require('react');
|
|||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const Icon = require('@stremio/stremio-icons/dom');
|
||||
const { Button, Image } = require('stremio/common');
|
||||
const { Button, Image, Multiselect } = require('stremio/common');
|
||||
const { useServices } = require('stremio/services');
|
||||
const Stream = require('./Stream');
|
||||
const styles = require('./styles');
|
||||
|
||||
const ALL_ADDONS_KEY = 'ALL';
|
||||
|
||||
const StreamsList = ({ className, ...props }) => {
|
||||
const { core } = useServices();
|
||||
const streams = React.useMemo(() => {
|
||||
const [selectedAddon, setSelectedAddon] = React.useState(ALL_ADDONS_KEY);
|
||||
const onAddonSelected = React.useCallback((event) => {
|
||||
setSelectedAddon(event.value);
|
||||
}, []);
|
||||
const streamsByAddon = React.useMemo(() => {
|
||||
return props.streams
|
||||
.filter((streams) => streams.content.type === 'Ready')
|
||||
.map((streams) => {
|
||||
return streams.content.content.map((stream) => ({
|
||||
...stream,
|
||||
onClick: () => {
|
||||
core.transport.analytics({
|
||||
event: 'StreamClicked',
|
||||
args: {
|
||||
stream
|
||||
}
|
||||
});
|
||||
},
|
||||
addonName: streams.addon.manifest.name
|
||||
}));
|
||||
})
|
||||
.flat(1);
|
||||
.reduce((streamsByAddon, streams) => {
|
||||
streamsByAddon[streams.addon.transportUrl] = {
|
||||
addon: streams.addon,
|
||||
streams: streams.content.content.map((stream) => ({
|
||||
...stream,
|
||||
onClick: () => {
|
||||
core.transport.analytics({
|
||||
event: 'StreamClicked',
|
||||
args: {
|
||||
stream
|
||||
}
|
||||
});
|
||||
},
|
||||
addonName: streams.addon.manifest.name
|
||||
}))
|
||||
};
|
||||
|
||||
return streamsByAddon;
|
||||
}, {});
|
||||
}, [props.streams]);
|
||||
const filteredStreams = React.useMemo(() => {
|
||||
return selectedAddon === ALL_ADDONS_KEY ?
|
||||
Object.values(streamsByAddon).map(({ streams }) => streams).flat(1)
|
||||
:
|
||||
streamsByAddon[selectedAddon] ?
|
||||
streamsByAddon[selectedAddon].streams
|
||||
:
|
||||
[];
|
||||
}, [streamsByAddon, selectedAddon]);
|
||||
const selectableOptions = React.useMemo(() => {
|
||||
return {
|
||||
title: 'Select Addon',
|
||||
options: [
|
||||
{
|
||||
value: ALL_ADDONS_KEY,
|
||||
label: 'All',
|
||||
title: 'All'
|
||||
},
|
||||
...Object.keys(streamsByAddon).map((transportUrl) => ({
|
||||
value: transportUrl,
|
||||
label: streamsByAddon[transportUrl].addon.manifest.name,
|
||||
title: streamsByAddon[transportUrl].addon.manifest.name,
|
||||
}))
|
||||
],
|
||||
selected: [selectedAddon],
|
||||
onSelect: onAddonSelected
|
||||
};
|
||||
}, [streamsByAddon, selectedAddon]);
|
||||
return (
|
||||
<div className={classnames(className, styles['streams-list-container'])}>
|
||||
{
|
||||
|
|
@ -45,26 +83,37 @@ const StreamsList = ({ className, ...props }) => {
|
|||
<div className={styles['label']}>No streams were found!</div>
|
||||
</div>
|
||||
:
|
||||
streams.length === 0 ?
|
||||
filteredStreams.length === 0 ?
|
||||
<div className={styles['streams-container']}>
|
||||
<Stream.Placeholder />
|
||||
<Stream.Placeholder />
|
||||
</div>
|
||||
:
|
||||
<div className={styles['streams-container']}>
|
||||
{streams.map((stream, index) => (
|
||||
<Stream
|
||||
key={index}
|
||||
addonName={stream.addonName}
|
||||
name={stream.name}
|
||||
description={stream.description}
|
||||
thumbnail={stream.thumbnail}
|
||||
progress={stream.progress}
|
||||
deepLinks={stream.deepLinks}
|
||||
onClick={stream.onClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<React.Fragment>
|
||||
{
|
||||
Object.keys(streamsByAddon).length > 1 ?
|
||||
<Multiselect
|
||||
{...selectableOptions}
|
||||
className={styles['select-input-container']}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles['streams-container']}>
|
||||
{filteredStreams.map((stream, index) => (
|
||||
<Stream
|
||||
key={index}
|
||||
addonName={stream.addonName}
|
||||
name={stream.name}
|
||||
description={stream.description}
|
||||
thumbnail={stream.thumbnail}
|
||||
progress={stream.progress}
|
||||
deepLinks={stream.deepLinks}
|
||||
onClick={stream.onClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
<Button className={styles['install-button-container']} title={'Install Addons'} href={'#/addons'}>
|
||||
<Icon className={styles['icon']} icon={'ic_addons'} />
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@
|
|||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
@import (reference) '~stremio/common/screen-sizes.less';
|
||||
|
||||
:import('~stremio/common/Multiselect/styles.less') {
|
||||
multiselect-menu-container: menu-container;
|
||||
multiselect-label: label;
|
||||
multiselect-icon: icon;
|
||||
}
|
||||
|
||||
.streams-list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -35,6 +41,30 @@
|
|||
}
|
||||
}
|
||||
|
||||
.select-input-container {
|
||||
flex: 0 0 auto;
|
||||
height: 3.5rem;
|
||||
margin: 1em 1em 0 1em;
|
||||
background: none;
|
||||
|
||||
&:hover, &:focus, &:global(.active) {
|
||||
background-color: @color-background;
|
||||
}
|
||||
|
||||
& >.multiselect-label {
|
||||
color: @color-surface-light5-90;
|
||||
}
|
||||
|
||||
& >.multiselect-icon {
|
||||
fill: @color-surface-light5-90;
|
||||
}
|
||||
|
||||
.multiselect-menu-container {
|
||||
max-height: calc(3.2rem * 7);
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.streams-container {
|
||||
flex: 0 1 auto;
|
||||
align-self: stretch;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w
|
|||
core.transport.dispatch({
|
||||
action: 'MetaDetails',
|
||||
args: {
|
||||
action: 'MarkAsWatched',
|
||||
action: 'MarkVideoAsWatched',
|
||||
args: [id, !watched]
|
||||
}
|
||||
});
|
||||
|
|
@ -71,7 +71,7 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w
|
|||
renderFallback={() => (
|
||||
<Icon
|
||||
className={styles['placeholder-icon']}
|
||||
icon={'ic_broken_link'}
|
||||
icon={'ic_stremio_tray'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
@ -88,7 +88,7 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w
|
|||
{
|
||||
released instanceof Date && !isNaN(released.getTime()) ?
|
||||
<div className={styles['released-container']}>
|
||||
{released.toLocaleString(undefined, { year: '2-digit', month: 'short', day: 'numeric' })}
|
||||
{released.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })}
|
||||
</div>
|
||||
:
|
||||
scheduled ?
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@
|
|||
flex-wrap: wrap;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
&:hover, &:focus, &:global(.active) {
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:global(.active) {
|
||||
background-color: @color-background;
|
||||
}
|
||||
|
||||
|
|
@ -26,7 +28,7 @@
|
|||
|
||||
.thumbnail {
|
||||
display: block;
|
||||
width: 5rem;
|
||||
width: 7.5rem;
|
||||
height: 5rem;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
|
|
@ -35,10 +37,12 @@
|
|||
|
||||
.placeholder-icon {
|
||||
display: block;
|
||||
width: 5rem;
|
||||
width: 7.5rem;
|
||||
height: 5rem;
|
||||
padding: 0.5rem;
|
||||
fill: @color-secondaryvariant1-light3-90;
|
||||
padding: 1rem;
|
||||
fill: @color-surface-light5;
|
||||
background-color: @color-surface-light5-40;
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +94,8 @@
|
|||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.upcoming-container, .watched-container {
|
||||
.upcoming-container,
|
||||
.watched-container {
|
||||
flex: none;
|
||||
padding: 0.2rem 0.4rem;
|
||||
max-width: 10rem;
|
||||
|
|
@ -129,54 +134,65 @@
|
|||
background-color: @color-primaryvariant1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: @minimum) {
|
||||
.video-container {
|
||||
&:hover, &:focus, &:global(.active) {
|
||||
background-color: @color-surface-light5-20;
|
||||
}
|
||||
.context-menu-container {
|
||||
max-width: calc(90% - 1.5rem);
|
||||
|
||||
.context-menu-container {
|
||||
max-width: calc(90% - 1.5rem);
|
||||
.context-menu-content {
|
||||
--spatial-navigation-contain: contain;
|
||||
background-color: @color-background-dark1;
|
||||
|
||||
&.menu-direction-top-left, &.menu-direction-bottom-left {
|
||||
right: 1.5rem;
|
||||
}
|
||||
.context-menu-option-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
&.menu-direction-top-right, &.menu-direction-bottom-right {
|
||||
left: 1.5rem;
|
||||
}
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: @color-background;
|
||||
}
|
||||
|
||||
&.menu-direction-top-left, &.menu-direction-top-right {
|
||||
bottom: 90%;
|
||||
}
|
||||
|
||||
&.menu-direction-bottom-left, &.menu-direction-bottom-right {
|
||||
top: 90%;
|
||||
}
|
||||
|
||||
.context-menu-content {
|
||||
--spatial-navigation-contain: contain;
|
||||
background-color: @color-background-dark1;
|
||||
|
||||
.context-menu-option-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: @color-background;
|
||||
}
|
||||
|
||||
.context-menu-option-label {
|
||||
font-size: 1rem;
|
||||
max-height: 2.4em;
|
||||
color: @color-surface-light5-90;
|
||||
}
|
||||
.context-menu-option-label {
|
||||
font-size: 1rem;
|
||||
max-height: 2.4em;
|
||||
color: @color-surface-light5-90;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: @minimum) {
|
||||
.video-container {
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:global(.active) {
|
||||
background-color: @color-surface-light5-20;
|
||||
}
|
||||
|
||||
.context-menu-container {
|
||||
|
||||
&.menu-direction-top-left,
|
||||
&.menu-direction-bottom-left {
|
||||
right: 1.5rem;
|
||||
}
|
||||
|
||||
&.menu-direction-top-right,
|
||||
&.menu-direction-bottom-right {
|
||||
left: 1.5rem;
|
||||
}
|
||||
|
||||
&.menu-direction-top-left,
|
||||
&.menu-direction-top-right {
|
||||
bottom: 90%;
|
||||
}
|
||||
|
||||
&.menu-direction-bottom-left,
|
||||
&.menu-direction-bottom-right {
|
||||
top: 90%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,14 +3,6 @@
|
|||
const React = require('react');
|
||||
const { useModelState } = require('stremio/common');
|
||||
|
||||
const init = () => ({
|
||||
selected: null,
|
||||
metaItem: null,
|
||||
streams: [],
|
||||
metaExtensions: [],
|
||||
title: null
|
||||
});
|
||||
|
||||
const map = (metaDetails) => ({
|
||||
...metaDetails,
|
||||
metaItem: metaDetails.metaItem !== null && metaDetails.metaItem.content.type === 'Ready' ?
|
||||
|
|
@ -74,7 +66,7 @@ const useMetaDetails = (urlParams) => {
|
|||
};
|
||||
}
|
||||
}, [urlParams]);
|
||||
return useModelState({ model: 'meta_details', action, map, init });
|
||||
return useModelState({ model: 'meta_details', action, map });
|
||||
};
|
||||
|
||||
module.exports = useMetaDetails;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const ControlBar = ({
|
|||
subtitlesTracks,
|
||||
audioTracks,
|
||||
metaItem,
|
||||
nextVideo,
|
||||
onPlayRequested,
|
||||
onPauseRequested,
|
||||
onMuteRequested,
|
||||
|
|
@ -29,6 +30,7 @@ const ControlBar = ({
|
|||
onSeekRequested,
|
||||
onToggleSubtitlesMenu,
|
||||
onToggleInfoMenu,
|
||||
onToggleVideosMenu,
|
||||
...props
|
||||
}) => {
|
||||
const { chromecast } = useServices();
|
||||
|
|
@ -40,6 +42,9 @@ const ControlBar = ({
|
|||
const onInfoButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.infoMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onVideosButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.videosMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onPlayPauseButtonClick = React.useCallback(() => {
|
||||
if (paused) {
|
||||
if (typeof onPlayRequested === 'function') {
|
||||
|
|
@ -51,6 +56,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') {
|
||||
|
|
@ -72,6 +86,11 @@ const ControlBar = ({
|
|||
onToggleInfoMenu();
|
||||
}
|
||||
}, [onToggleInfoMenu]);
|
||||
const onVideosButtonClick = React.useCallback(() => {
|
||||
if (typeof onToggleVideosMenu === 'function') {
|
||||
onToggleVideosMenu();
|
||||
}
|
||||
}, [onToggleVideosMenu]);
|
||||
const onChromecastButtonClick = React.useCallback(() => {
|
||||
chromecast.transport.requestSession();
|
||||
}, []);
|
||||
|
|
@ -96,6 +115,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']}
|
||||
|
|
@ -130,9 +157,14 @@ const ControlBar = ({
|
|||
<Button className={classnames(styles['control-bar-button'], { 'disabled': (!Array.isArray(subtitlesTracks) || subtitlesTracks.length === 0) && (!Array.isArray(audioTracks) || audioTracks.length === 0) })} tabIndex={-1} onMouseDown={onSubtitlesButtonMouseDown} onClick={onSubtitlesButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_sub'} />
|
||||
</Button>
|
||||
<Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}>
|
||||
<Icon className={styles['icon']} icon={'ic_videos'} />
|
||||
</Button>
|
||||
{
|
||||
metaItem?.content?.videos?.length > 0 ?
|
||||
<Button className={styles['control-bar-button']} tabIndex={-1} onMouseDown={onVideosButtonMouseDown} onClick={onVideosButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_videos'} />
|
||||
</Button>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -149,6 +181,7 @@ ControlBar.propTypes = {
|
|||
subtitlesTracks: PropTypes.array,
|
||||
audioTracks: PropTypes.array,
|
||||
metaItem: PropTypes.object,
|
||||
nextVideo: PropTypes.object,
|
||||
onPlayRequested: PropTypes.func,
|
||||
onPauseRequested: PropTypes.func,
|
||||
onMuteRequested: PropTypes.func,
|
||||
|
|
@ -156,7 +189,8 @@ ControlBar.propTypes = {
|
|||
onVolumeChangeRequested: PropTypes.func,
|
||||
onSeekRequested: PropTypes.func,
|
||||
onToggleSubtitlesMenu: PropTypes.func,
|
||||
onToggleInfoMenu: PropTypes.func
|
||||
onToggleInfoMenu: PropTypes.func,
|
||||
onToggleVideosMenu: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports = ControlBar;
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ const classnames = require('classnames');
|
|||
const debounce = require('lodash.debounce');
|
||||
const { useRouteFocused } = require('stremio-router');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { HorizontalNavBar, Button, useFullscreen, useBinaryState, useToast, useStreamingServer } = require('stremio/common');
|
||||
const { HorizontalNavBar, Button, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common');
|
||||
const Icon = require('@stremio/stremio-icons/dom');
|
||||
const BufferingLoader = require('./BufferingLoader');
|
||||
const ControlBar = require('./ControlBar');
|
||||
const InfoMenu = require('./InfoMenu');
|
||||
const VideosMenu = require('./VideosMenu');
|
||||
const SubtitlesMenu = require('./SubtitlesMenu');
|
||||
const Video = require('./Video');
|
||||
const usePlayer = require('./usePlayer');
|
||||
|
|
@ -18,14 +19,14 @@ const useSettings = require('./useSettings');
|
|||
const styles = require('./styles');
|
||||
|
||||
const Player = ({ urlParams, queryParams }) => {
|
||||
const { core, chromecast } = useServices();
|
||||
const { chromecast, shell } = useServices();
|
||||
const [forceTranscoding, maxAudioChannels] = React.useMemo(() => {
|
||||
return [
|
||||
queryParams.has('forceTranscoding'),
|
||||
queryParams.has('maxAudioChannels') ? parseInt(queryParams.get('maxAudioChannels'), 10) : null
|
||||
];
|
||||
}, [queryParams]);
|
||||
const [player, updateLibraryItemState, pushToLibrary] = usePlayer(urlParams);
|
||||
const [player, timeChanged, pausedChanged, ended, pushToLibrary] = usePlayer(urlParams);
|
||||
const [settings, updateSettings] = useSettings();
|
||||
const streamingServer = useStreamingServer();
|
||||
const routeFocused = useRouteFocused();
|
||||
|
|
@ -38,10 +39,12 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
|
||||
const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
|
||||
const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
|
||||
const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [videoState, setVideoState] = React.useReducer(
|
||||
(videoState, nextVideoState) => ({ ...videoState, ...nextVideoState }),
|
||||
{
|
||||
manifest: null,
|
||||
stream: null,
|
||||
paused: null,
|
||||
time: null,
|
||||
|
|
@ -75,6 +78,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
}, []);
|
||||
const onImplementationChanged = React.useCallback((manifest) => {
|
||||
setVideoState({ manifest });
|
||||
manifest.props.forEach((propName) => {
|
||||
dispatch({ type: 'observeProp', propName });
|
||||
});
|
||||
|
|
@ -93,16 +97,8 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
setVideoState({ [propName]: propValue });
|
||||
}, []);
|
||||
const onEnded = React.useCallback(() => {
|
||||
ended();
|
||||
pushToLibrary();
|
||||
if (player.libraryItem !== null) {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'RewindLibraryItem',
|
||||
args: player.libraryItem._id
|
||||
}
|
||||
});
|
||||
}
|
||||
if (player.nextVideo !== null) {
|
||||
window.location.replace(
|
||||
typeof player.nextVideo.deepLinks.player === 'string' ?
|
||||
|
|
@ -204,6 +200,9 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
if (!event.nativeEvent.infoMenuClosePrevented) {
|
||||
closeInfoMenu();
|
||||
}
|
||||
if (!event.nativeEvent.videosMenuClosePrevented) {
|
||||
closeVideosMenu();
|
||||
}
|
||||
}, []);
|
||||
const onContainerMouseMove = React.useCallback((event) => {
|
||||
setImmersed(false);
|
||||
|
|
@ -224,7 +223,8 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
setError(null);
|
||||
if (player.selected === null) {
|
||||
dispatch({ type: 'command', commandName: 'unload' });
|
||||
} else if (streamingServer.baseUrl !== null && streamingServer.baseUrl.type !== 'Loading' && player.metaItem !== null && player.metaItem.type !== 'Loading') {
|
||||
} else if (streamingServer.baseUrl !== null && streamingServer.baseUrl.type !== 'Loading' &&
|
||||
(player.selected.metaRequest === null || (player.metaItem !== null && player.metaItem.type !== 'Loading'))) {
|
||||
dispatch({
|
||||
type: 'command',
|
||||
commandName: 'load',
|
||||
|
|
@ -263,6 +263,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
}, {
|
||||
chromecastTransport: chromecast.active ? chromecast.transport : null,
|
||||
shellTransport: shell.active ? shell.transport : null,
|
||||
});
|
||||
}
|
||||
}, [streamingServer.baseUrl, player.selected, player.metaItem, forceTranscoding, maxAudioChannels, casting]);
|
||||
|
|
@ -301,10 +302,17 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
dispatch({ type: 'setProp', propName: 'extraSubtitlesOutlineColor', propValue: settings.subtitlesOutlineColor });
|
||||
}, [settings.subtitlesOutlineColor]);
|
||||
React.useEffect(() => {
|
||||
if (videoState.time !== null && !isNaN(videoState.time) && videoState.duration !== null && !isNaN(videoState.duration)) {
|
||||
updateLibraryItemState(videoState.time, videoState.duration);
|
||||
if (videoState.time !== null && !isNaN(videoState.time) &&
|
||||
videoState.duration !== null && !isNaN(videoState.duration) &&
|
||||
videoState.manifest !== null && typeof videoState.manifest.name === 'string') {
|
||||
timeChanged(videoState.time, videoState.duration, videoState.manifest.name);
|
||||
}
|
||||
}, [videoState.time, videoState.duration]);
|
||||
}, [videoState.time, videoState.duration, videoState.manifest]);
|
||||
React.useEffect(() => {
|
||||
if (videoState.paused !== null) {
|
||||
pausedChanged(videoState.paused);
|
||||
}
|
||||
}, [videoState.paused]);
|
||||
React.useEffect(() => {
|
||||
if ((!Array.isArray(videoState.subtitlesTracks) || videoState.subtitlesTracks.length === 0) &&
|
||||
(!Array.isArray(videoState.extraSubtitlesTracks) || videoState.extraSubtitlesTracks.length === 0) &&
|
||||
|
|
@ -315,6 +323,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
React.useEffect(() => {
|
||||
if (player.metaItem === null || player.metaItem.type !== 'Ready') {
|
||||
closeInfoMenu();
|
||||
closeVideosMenu();
|
||||
}
|
||||
}, [player.metaItem]);
|
||||
React.useEffect(() => {
|
||||
|
|
@ -326,6 +335,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);
|
||||
};
|
||||
|
|
@ -341,6 +352,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
chromecast.on('stateChanged', onChromecastServiceStateChange);
|
||||
onChromecastServiceStateChange();
|
||||
return () => {
|
||||
toast.removeFilter(toastFilter);
|
||||
chromecast.off('stateChanged', onChromecastServiceStateChange);
|
||||
if (chromecast.active) {
|
||||
chromecast.transport.off(
|
||||
|
|
@ -396,6 +408,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
case 'KeyS': {
|
||||
closeInfoMenu();
|
||||
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)) {
|
||||
|
|
@ -406,15 +419,26 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
case 'KeyI': {
|
||||
closeSubtitlesMenu();
|
||||
closeVideosMenu();
|
||||
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
|
||||
toggleInfoMenu();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyV': {
|
||||
closeInfoMenu();
|
||||
closeSubtitlesMenu();
|
||||
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
|
||||
toggleVideosMenu();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'Escape': {
|
||||
closeSubtitlesMenu();
|
||||
closeInfoMenu();
|
||||
closeVideosMenu();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -425,7 +449,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, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu]);
|
||||
React.useLayoutEffect(() => {
|
||||
return () => {
|
||||
setImmersedDebounced.cancel();
|
||||
|
|
@ -434,7 +458,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen })}
|
||||
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen })}
|
||||
onMouseDown={onContainerMouseDown}
|
||||
onMouseMove={onContainerMouseMove}
|
||||
onMouseOver={onContainerMouseMove}
|
||||
|
|
@ -502,6 +526,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
subtitlesTracks={videoState.subtitlesTracks.concat(videoState.extraSubtitlesTracks)}
|
||||
audioTracks={videoState.audioTracks}
|
||||
metaItem={player.metaItem}
|
||||
nextVideo={player.nextVideo}
|
||||
onPlayRequested={onPlayRequested}
|
||||
onPauseRequested={onPauseRequested}
|
||||
onMuteRequested={onMuteRequested}
|
||||
|
|
@ -510,6 +535,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
onSeekRequested={onSeekRequested}
|
||||
onToggleSubtitlesMenu={toggleSubtitlesMenu}
|
||||
onToggleInfoMenu={toggleInfoMenu}
|
||||
onToggleVideosMenu={toggleVideosMenu}
|
||||
onMouseMove={onBarMouseMove}
|
||||
onMouseOver={onBarMouseMove}
|
||||
/>
|
||||
|
|
@ -551,6 +577,16 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
videosMenuOpen ?
|
||||
<VideosMenu
|
||||
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||
metaItem={player.metaItem !== null && player.metaItem.type === 'Ready' ? player.metaItem.content : null}
|
||||
seriesInfo={player.seriesInfo}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -567,4 +603,8 @@ Player.propTypes = {
|
|||
queryParams: PropTypes.instanceOf(URLSearchParams)
|
||||
};
|
||||
|
||||
module.exports = Player;
|
||||
const PlayerFallback = () => (
|
||||
<div className={classnames(styles['player-container'])} />
|
||||
);
|
||||
|
||||
module.exports = withCoreSuspender(Player, PlayerFallback);
|
||||
|
|
|
|||
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;
|
||||
}
|
||||
|
|
@ -2,18 +2,7 @@
|
|||
|
||||
const React = require('react');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { useModelState } = require('stremio/common');
|
||||
|
||||
const init = () => ({
|
||||
selected: null,
|
||||
metaItem: null,
|
||||
subtitles: [],
|
||||
nextVideo: null,
|
||||
seriesInfo: null,
|
||||
libraryItem: null,
|
||||
title: null,
|
||||
addon: null,
|
||||
});
|
||||
const { useModelState, useCoreSuspender } = require('stremio/common');
|
||||
|
||||
const map = (player) => ({
|
||||
...player,
|
||||
|
|
@ -45,8 +34,9 @@ const map = (player) => ({
|
|||
|
||||
const usePlayer = (urlParams) => {
|
||||
const { core } = useServices();
|
||||
const { decodeStream } = useCoreSuspender();
|
||||
const stream = decodeStream(urlParams.stream);
|
||||
const action = React.useMemo(() => {
|
||||
const stream = core.transport.decodeStream(urlParams.stream);
|
||||
if (stream !== null) {
|
||||
return {
|
||||
action: 'Load',
|
||||
|
|
@ -96,12 +86,12 @@ const usePlayer = (urlParams) => {
|
|||
};
|
||||
}
|
||||
}, [urlParams]);
|
||||
const updateLibraryItemState = React.useCallback((time, duration) => {
|
||||
const timeChanged = React.useCallback((time, duration, device) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Player',
|
||||
args: {
|
||||
action: 'UpdateLibraryItemState',
|
||||
args: { time, duration }
|
||||
action: 'TimeChanged',
|
||||
args: { time, duration, device }
|
||||
}
|
||||
}, 'player');
|
||||
}, []);
|
||||
|
|
@ -113,8 +103,25 @@ const usePlayer = (urlParams) => {
|
|||
}
|
||||
}, 'player');
|
||||
}, []);
|
||||
const player = useModelState({ model: 'player', action, init, map });
|
||||
return [player, updateLibraryItemState, pushToLibrary];
|
||||
const ended = React.useCallback(() => {
|
||||
core.transport.dispatch({
|
||||
action: 'Player',
|
||||
args: {
|
||||
action: 'Ended'
|
||||
}
|
||||
}, 'player');
|
||||
}, []);
|
||||
const pausedChanged = React.useCallback((paused) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Player',
|
||||
args: {
|
||||
action: 'PausedChanged',
|
||||
args: { paused }
|
||||
}
|
||||
}, 'player');
|
||||
}, []);
|
||||
const player = useModelState({ model: 'player', action, map });
|
||||
return [player, timeChanged, pausedChanged, ended, pushToLibrary];
|
||||
};
|
||||
|
||||
module.exports = usePlayer;
|
||||
|
|
|
|||
|
|
@ -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, getVisibleChildrenRange } = require('stremio/common');
|
||||
const { Image, MainNavBars, MetaRow, MetaItem, useDeepEqualMemo, withCoreSuspender, getVisibleChildrenRange } = require('stremio/common');
|
||||
const useSearch = require('./useSearch');
|
||||
const styles = require('./styles');
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ const Search = ({ queryParams }) => {
|
|||
<div ref={scrollContainerRef} className={styles['search-content']} onScroll={onScroll}>
|
||||
{
|
||||
query === null ?
|
||||
<div className={styles['search-hints-container']}>
|
||||
<div className={classnames(styles['search-hints-container'], 'animation-fade-in')}>
|
||||
<div className={styles['search-hint-container']}>
|
||||
<Icon className={styles['icon']} icon={'ic_movies'} />
|
||||
<div className={styles['label']}>Search for movies, series, YouTube and TV channels</div>
|
||||
|
|
@ -74,7 +74,7 @@ const Search = ({ queryParams }) => {
|
|||
return (
|
||||
<MetaRow
|
||||
key={index}
|
||||
className={classnames(styles['search-row'], styles[`search-row-${catalog.content.content[0].posterShape}`])}
|
||||
className={classnames(styles['search-row'], styles[`search-row-${catalog.content.content[0].posterShape}`], 'animation-fade-in')}
|
||||
title={catalog.title}
|
||||
items={catalog.content.content}
|
||||
itemComponent={MetaItem}
|
||||
|
|
@ -86,7 +86,7 @@ const Search = ({ queryParams }) => {
|
|||
return (
|
||||
<MetaRow
|
||||
key={index}
|
||||
className={styles['search-row']}
|
||||
className={classnames(styles['search-row'], 'animation-fade-in')}
|
||||
title={catalog.title}
|
||||
message={catalog.content.content}
|
||||
deepLinks={catalog.deepLinks}
|
||||
|
|
@ -97,7 +97,7 @@ const Search = ({ queryParams }) => {
|
|||
return (
|
||||
<MetaRow.Placeholder
|
||||
key={index}
|
||||
className={classnames(styles['search-row'], styles['search-row-poster'])}
|
||||
className={classnames(styles['search-row'], styles['search-row-poster'], 'animation-fade-in')}
|
||||
title={catalog.title}
|
||||
deepLinks={catalog.deepLinks}
|
||||
/>
|
||||
|
|
@ -115,4 +115,10 @@ Search.propTypes = {
|
|||
queryParams: PropTypes.instanceOf(URLSearchParams)
|
||||
};
|
||||
|
||||
module.exports = Search;
|
||||
const SearchFallback = ({ queryParams }) => (
|
||||
<MainNavBars className={styles['search-container']} route={'search'} query={queryParams.get('search')} />
|
||||
);
|
||||
|
||||
SearchFallback.propTypes = Search.propTypes;
|
||||
|
||||
module.exports = withCoreSuspender(Search, SearchFallback);
|
||||
|
|
|
|||
|
|
@ -4,37 +4,33 @@ const React = require('react');
|
|||
const { useModelState } = require('stremio/common');
|
||||
const { useServices } = require('stremio/services');
|
||||
|
||||
const init = () => ({
|
||||
selected: null,
|
||||
catalogs: []
|
||||
});
|
||||
|
||||
const useSearch = (queryParams) => {
|
||||
const { core } = useServices();
|
||||
React.useEffect(() => {
|
||||
let timerId = setTimeout(emitSearchEvent, 500);
|
||||
function emitSearchEvent() {
|
||||
timerId = null;
|
||||
const state = core.transport.getState('search');
|
||||
if (state.selected !== null) {
|
||||
const [, query] = state.selected.extra.find(([name]) => name === 'search');
|
||||
const responses = state.catalogs.filter((catalog) => catalog.content?.type === 'Ready');
|
||||
core.transport.analytics({
|
||||
event: 'Search',
|
||||
args: {
|
||||
query,
|
||||
responsesCount: responses.length
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
if (timerId !== null) {
|
||||
clearTimeout(timerId);
|
||||
emitSearchEvent();
|
||||
}
|
||||
};
|
||||
}, [queryParams.get('search')]);
|
||||
// TODO: refactor this to be in stremio-core-web
|
||||
// React.useEffect(() => {
|
||||
// let timerId = setTimeout(emitSearchEvent, 500);
|
||||
// function emitSearchEvent() {
|
||||
// timerId = null;
|
||||
// const state = core.transport.getState('search');
|
||||
// if (state.selected !== null) {
|
||||
// const [, query] = state.selected.extra.find(([name]) => name === 'search');
|
||||
// const responses = state.catalogs.filter((catalog) => catalog.content?.type === 'Ready');
|
||||
// core.transport.analytics({
|
||||
// event: 'Search',
|
||||
// args: {
|
||||
// query,
|
||||
// responsesCount: responses.length
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// return () => {
|
||||
// if (timerId !== null) {
|
||||
// clearTimeout(timerId);
|
||||
// emitSearchEvent();
|
||||
// }
|
||||
// };
|
||||
// }, [queryParams.get('search')]);
|
||||
const action = React.useMemo(() => {
|
||||
if (queryParams.has('search') && queryParams.get('search').length > 0) {
|
||||
return {
|
||||
|
|
@ -63,7 +59,7 @@ const useSearch = (queryParams) => {
|
|||
}
|
||||
}, 'search');
|
||||
}, []);
|
||||
const search = useModelState({ model: 'search', action, init });
|
||||
const search = useModelState({ model: 'search', action });
|
||||
return [search, loadRange];
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const throttle = require('lodash.throttle');
|
|||
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 } = require('stremio/common');
|
||||
const { Button, Checkbox, MainNavBars, Multiselect, ColorInput, TextInput, ModalDialog, useProfile, useStreamingServer, useBinaryState, withCoreSuspender } = require('stremio/common');
|
||||
const useProfileSettingsInputs = require('./useProfileSettingsInputs');
|
||||
const useStreamingServerSettingsInputs = require('./useStreamingServerSettingsInputs');
|
||||
const styles = require('./styles');
|
||||
|
|
@ -14,6 +14,7 @@ const styles = require('./styles');
|
|||
const GENERAL_SECTION = 'general';
|
||||
const PLAYER_SECTION = 'player';
|
||||
const STREAMING_SECTION = 'streaming';
|
||||
const SHORTCUTS_SECTION = 'shortcuts';
|
||||
|
||||
const Settings = () => {
|
||||
const { core } = useServices();
|
||||
|
|
@ -93,10 +94,12 @@ const Settings = () => {
|
|||
const generalSectionRef = React.useRef(null);
|
||||
const playerSectionRef = React.useRef(null);
|
||||
const streamingServerSectionRef = React.useRef(null);
|
||||
const shortcutsSectionRef = React.useRef(null);
|
||||
const sections = React.useMemo(() => ([
|
||||
{ ref: generalSectionRef, id: GENERAL_SECTION },
|
||||
{ ref: playerSectionRef, id: PLAYER_SECTION },
|
||||
{ ref: streamingServerSectionRef, id: STREAMING_SECTION },
|
||||
{ ref: shortcutsSectionRef, id: SHORTCUTS_SECTION },
|
||||
]), []);
|
||||
const [selectedSectionId, setSelectedSectionId] = React.useState(GENERAL_SECTION);
|
||||
const updateSelectedSectionId = React.useCallback(() => {
|
||||
|
|
@ -131,7 +134,7 @@ const Settings = () => {
|
|||
}, [routeFocused]);
|
||||
return (
|
||||
<MainNavBars className={styles['settings-container']} route={'settings'}>
|
||||
<div className={styles['settings-content']}>
|
||||
<div className={classnames(styles['settings-content'], 'animation-fade-in')}>
|
||||
<div className={styles['side-menu-container']}>
|
||||
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === GENERAL_SECTION })} title={'General'} data-section={GENERAL_SECTION} onClick={sideMenuButtonOnClick}>
|
||||
General
|
||||
|
|
@ -142,6 +145,9 @@ const Settings = () => {
|
|||
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === STREAMING_SECTION })} title={'Streaming server'} data-section={STREAMING_SECTION} onClick={sideMenuButtonOnClick}>
|
||||
Streaming server
|
||||
</Button>
|
||||
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === SHORTCUTS_SECTION })} title={'Shortcuts'} data-section={SHORTCUTS_SECTION} onClick={sideMenuButtonOnClick}>
|
||||
Shortcuts
|
||||
</Button>
|
||||
<div className={styles['spacing']} />
|
||||
<div className={styles['version-info-label']} title={process.env.VERSION}>App Version: {process.env.VERSION}</div>
|
||||
{
|
||||
|
|
@ -424,6 +430,107 @@ const Settings = () => {
|
|||
null
|
||||
}
|
||||
</div>
|
||||
<div ref={shortcutsSectionRef} className={styles['section-container']}>
|
||||
<div className={styles['section-title']}>Shortcuts</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Play / Pause</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>Space</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Seek Forward</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>→</kbd>
|
||||
<div className={styles['label']}>or</div>
|
||||
<kbd>⇧ Shift</kbd>
|
||||
<div className={styles['label']}>+</div>
|
||||
<kbd>→</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Seek Backward</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>←</kbd>
|
||||
<div className={styles['label']}>or</div>
|
||||
<kbd>⇧ Shift</kbd>
|
||||
<div className={styles['label']}>+</div>
|
||||
<kbd>←</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Volume Up</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>↑</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Volume Down</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>↓</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Toggle Subtitles Menu</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>S</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Toggle Info Menu</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>I</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Toggle Fullscreen</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>F</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Navigate Between Menus</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>1</kbd>
|
||||
<div className={styles['label']}>to</div>
|
||||
<kbd>5</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Go to Search</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>0</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>Close Menu or Modal</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>Esc</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
|
|
@ -449,4 +556,8 @@ const Settings = () => {
|
|||
);
|
||||
};
|
||||
|
||||
module.exports = Settings;
|
||||
const SettingsFallback = () => (
|
||||
<MainNavBars className={styles['settings-container']} route={'settings'} />
|
||||
);
|
||||
|
||||
module.exports = withCoreSuspender(Settings, SettingsFallback);
|
||||
|
|
|
|||
|
|
@ -315,6 +315,30 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.shortcut-container {
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
|
||||
kbd {
|
||||
flex: 0 1 auto;
|
||||
height: 2.5rem;
|
||||
min-width: 2.5rem;
|
||||
line-height: 2.5rem;
|
||||
padding: 0 1rem;
|
||||
font-weight: 500;
|
||||
color: @color-secondaryvariant1-90;
|
||||
border-radius: 0.25em;
|
||||
box-shadow: 0 4px 0 1px @color-background-40;
|
||||
background-color: @color-background;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 0 1rem;
|
||||
color: @color-secondaryvariant1-90;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const { CONSTANTS, languageNames, useDeepEqualMemo } = require('stremio/common')
|
|||
|
||||
const useProfileSettingsInputs = (profile) => {
|
||||
const { core } = useServices();
|
||||
// TODO combine those useDeepEqualMemo in one
|
||||
const interfaceLanguageSelect = useDeepEqualMemo(() => ({
|
||||
options: Object.keys(languageNames).map((code) => ({
|
||||
value: code,
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ const cacheSizeToString = (size) => {
|
|||
|
||||
const TORRENT_PROFILES = {
|
||||
default: {
|
||||
btDownloadSpeedHardLimit: 2621440,
|
||||
btDownloadSpeedSoftLimit: 1677721.6,
|
||||
btDownloadSpeedHardLimit: 3670016,
|
||||
btDownloadSpeedSoftLimit: 2621440,
|
||||
btHandshakeTimeout: 20000,
|
||||
btMaxConnections: 35,
|
||||
btMaxConnections: 55,
|
||||
btMinPeersForStable: 5,
|
||||
btRequestTimeout: 4000
|
||||
},
|
||||
|
|
@ -40,11 +40,20 @@ const TORRENT_PROFILES = {
|
|||
btMaxConnections: 200,
|
||||
btMinPeersForStable: 10,
|
||||
btRequestTimeout: 4000
|
||||
},
|
||||
'ultra fast': {
|
||||
btDownloadSpeedHardLimit: 78643200,
|
||||
btDownloadSpeedSoftLimit: 8388608,
|
||||
btHandshakeTimeout: 25000,
|
||||
btMaxConnections: 400,
|
||||
btMinPeersForStable: 10,
|
||||
btRequestTimeout: 6000
|
||||
}
|
||||
};
|
||||
|
||||
const useStreamingServerSettingsInputs = (streamingServer) => {
|
||||
const { core } = useServices();
|
||||
// TODO combine those useDeepEqualMemo in one
|
||||
const cacheSizeSelect = useDeepEqualMemo(() => {
|
||||
if (streamingServer.settings === null || streamingServer.settings.type !== 'Ready') {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
const EventEmitter = require('eventemitter3');
|
||||
const CoreTransport = require('./CoreTransport');
|
||||
|
||||
function Core() {
|
||||
function Core(args) {
|
||||
let active = false;
|
||||
let error = null;
|
||||
let starting = false;
|
||||
|
|
@ -66,7 +66,7 @@ function Core() {
|
|||
}
|
||||
|
||||
starting = true;
|
||||
transport = new CoreTransport();
|
||||
transport = new CoreTransport(args);
|
||||
transport.on('init', onTransportInit);
|
||||
transport.on('error', onTransportError);
|
||||
onStateChanged();
|
||||
|
|
|
|||
|
|
@ -1,19 +1,22 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const EventEmitter = require('eventemitter3');
|
||||
const { default: initialize_api, initialize_runtime, get_state, get_debug_state, dispatch, analytics, decode_stream } = require('@stremio/stremio-core-web');
|
||||
const Bridge = require('@stremio/stremio-core-web/bridge');
|
||||
|
||||
function CoreTransport() {
|
||||
function CoreTransport(args) {
|
||||
const events = new EventEmitter();
|
||||
const worker = new Worker(`${process.env.COMMIT_HASH}/scripts/worker.js`);
|
||||
const bridge = new Bridge(window, worker);
|
||||
|
||||
initialize_api(require('@stremio/stremio-core-web/stremio_core_web_bg.wasm'))
|
||||
.then(() => initialize_runtime(({ name, args }) => {
|
||||
try {
|
||||
events.emit(name, args);
|
||||
} catch (error) {
|
||||
console.error('CoreTransport', error);
|
||||
}
|
||||
}))
|
||||
window.onCoreEvent = ({ name, args }) => {
|
||||
try {
|
||||
events.emit(name, args);
|
||||
} catch (error) {
|
||||
console.error('CoreTransport', error);
|
||||
}
|
||||
};
|
||||
|
||||
bridge.call(['init'], [args])
|
||||
.then(() => {
|
||||
try {
|
||||
events.emit('init');
|
||||
|
|
@ -34,28 +37,20 @@ function CoreTransport() {
|
|||
this.removeAllListeners = function() {
|
||||
events.removeAllListeners();
|
||||
};
|
||||
this.getState = function(field) {
|
||||
return get_state(field);
|
||||
this.getState = async function(field) {
|
||||
return bridge.call(['getState'], [field]);
|
||||
};
|
||||
this.getDebugState = function() {
|
||||
return get_debug_state();
|
||||
this.getDebugState = async function() {
|
||||
return bridge.call(['getDebugState'], []);
|
||||
};
|
||||
this.dispatch = function(action, field) {
|
||||
try {
|
||||
dispatch(action, field);
|
||||
} catch (error) {
|
||||
console.error('CoreTransport', error);
|
||||
}
|
||||
this.dispatch = async function(action, field) {
|
||||
return bridge.call(['dispatch'], [action, field, location.hash]);
|
||||
};
|
||||
this.analytics = function(event) {
|
||||
try {
|
||||
analytics(event);
|
||||
} catch (error) {
|
||||
console.error('CoreTransport', error);
|
||||
}
|
||||
this.analytics = async function(event) {
|
||||
return bridge.call(['analytics'], [event, location.hash]);
|
||||
};
|
||||
this.decodeStream = function(stream) {
|
||||
return decode_stream(stream);
|
||||
this.decodeStream = async function(stream) {
|
||||
return bridge.call(['decodeStream'], [stream]);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
89
src/services/DragAndDrop/DragAndDrop.js
Normal file
89
src/services/DragAndDrop/DragAndDrop.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const EventEmitter = require('eventemitter3');
|
||||
|
||||
function DragAndDrop({ core }) {
|
||||
let active = false;
|
||||
|
||||
const events = new EventEmitter();
|
||||
|
||||
function onDragOver(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
async function onDrop(event) {
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer.files instanceof FileList && event.dataTransfer.files.length > 0) {
|
||||
const file = event.dataTransfer.files[0];
|
||||
switch (file.type) {
|
||||
case 'application/x-bittorrent': {
|
||||
try {
|
||||
const torrent = await file.arrayBuffer();
|
||||
core.transport.dispatch({
|
||||
action: 'StreamingServer',
|
||||
args: {
|
||||
action: 'CreateTorrent',
|
||||
args: Array.from(new Uint8Array(torrent))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
events.emit('error', {
|
||||
message: 'Failed to process file',
|
||||
file: {
|
||||
name: file.name,
|
||||
type: file.type
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
events.emit('error', {
|
||||
message: 'Unsupported file',
|
||||
file: {
|
||||
name: file.name,
|
||||
type: file.type
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function onStateChanged() {
|
||||
events.emit('stateChanged');
|
||||
}
|
||||
|
||||
Object.defineProperties(this, {
|
||||
active: {
|
||||
configurable: false,
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return active;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.start = function() {
|
||||
if (active) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('dragover', onDragOver);
|
||||
window.addEventListener('drop', onDrop);
|
||||
active = true;
|
||||
onStateChanged();
|
||||
};
|
||||
this.stop = function() {
|
||||
window.removeEventListener('dragover', onDragOver);
|
||||
window.removeEventListener('drop', onDrop);
|
||||
active = false;
|
||||
onStateChanged();
|
||||
};
|
||||
this.on = function(name, listener) {
|
||||
events.on(name, listener);
|
||||
};
|
||||
this.off = function(name, listener) {
|
||||
events.off(name, listener);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = DragAndDrop;
|
||||
5
src/services/DragAndDrop/index.js
Normal file
5
src/services/DragAndDrop/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const DragAndDrop = require('./DragAndDrop');
|
||||
|
||||
module.exports = DragAndDrop;
|
||||
|
|
@ -1,14 +1,31 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const EventEmitter = require('eventemitter3');
|
||||
const ShellTransport = require('./ShellTransport');
|
||||
|
||||
function Shell() {
|
||||
let active = false;
|
||||
let error = null;
|
||||
let starting = false;
|
||||
let transport = null;
|
||||
|
||||
const events = new EventEmitter();
|
||||
|
||||
function onTransportInit() {
|
||||
active = true;
|
||||
error = null;
|
||||
starting = false;
|
||||
onStateChanged();
|
||||
}
|
||||
function onTransportInitError(err) {
|
||||
console.error(err);
|
||||
active = false;
|
||||
error = new Error(err);
|
||||
starting = false;
|
||||
onStateChanged();
|
||||
transport = null;
|
||||
}
|
||||
|
||||
function onStateChanged() {
|
||||
events.emit('stateChanged');
|
||||
}
|
||||
|
|
@ -34,6 +51,13 @@ function Shell() {
|
|||
get: function() {
|
||||
return starting;
|
||||
}
|
||||
},
|
||||
transport: {
|
||||
configurable: false,
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return transport;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -43,8 +67,10 @@ function Shell() {
|
|||
}
|
||||
|
||||
active = false;
|
||||
error = new Error('Stremio Shell API not available');
|
||||
starting = false;
|
||||
starting = true;
|
||||
transport = new ShellTransport();
|
||||
transport.on('init', onTransportInit);
|
||||
transport.on('init-error', onTransportInitError);
|
||||
onStateChanged();
|
||||
};
|
||||
this.stop = function() {
|
||||
|
|
|
|||
121
src/services/Shell/ShellTransport.js
Normal file
121
src/services/Shell/ShellTransport.js
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
// Copyright (C) 2017-2022 Smart code 203358507
|
||||
|
||||
const EventEmitter = require('eventemitter3');
|
||||
|
||||
let shellAvailable = false;
|
||||
const shellEvents = new EventEmitter();
|
||||
|
||||
const QtMsgTypes = {
|
||||
signal: 1,
|
||||
propertyUpdate: 2,
|
||||
init: 3,
|
||||
idle: 4,
|
||||
debug: 5,
|
||||
invokeMethod: 6,
|
||||
connectToSignal: 7,
|
||||
disconnectFromSignal: 8,
|
||||
setProperty: 9,
|
||||
response: 10,
|
||||
};
|
||||
const QtObjId = 'transport'; // the ID of our transport object
|
||||
|
||||
window.initShellComm = function () {
|
||||
delete window.initShellComm;
|
||||
shellEvents.emit('availabilityChanged');
|
||||
};
|
||||
|
||||
const initialize = () => {
|
||||
if(!window.qt) return Promise.reject('Qt API not found');
|
||||
return new Promise((resolve) => {
|
||||
function onShellAvailabilityChanged() {
|
||||
shellEvents.off('availabilityChanged', onShellAvailabilityChanged);
|
||||
shellAvailable = true;
|
||||
resolve();
|
||||
}
|
||||
if (shellAvailable) {
|
||||
onShellAvailabilityChanged();
|
||||
} else {
|
||||
shellEvents.on('availabilityChanged', onShellAvailabilityChanged);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function ShellTransport() {
|
||||
const events = new EventEmitter();
|
||||
|
||||
this.props = {};
|
||||
|
||||
const shell = this;
|
||||
initialize()
|
||||
.then(() => {
|
||||
const transport = window.qt && window.qt.webChannelTransport;
|
||||
if (!transport) throw 'no viable transport found (qt.webChannelTransport)';
|
||||
|
||||
let id = 0;
|
||||
function send(msg) {
|
||||
msg.id = id++;
|
||||
transport.send(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
transport.onmessage = function (message) {
|
||||
const msg = JSON.parse(message.data);
|
||||
if (msg.id === 0) {
|
||||
const obj = msg.data[QtObjId];
|
||||
|
||||
obj.properties.slice(1).forEach(function (prop) {
|
||||
shell.props[prop[1]] = prop[3];
|
||||
});
|
||||
if (typeof shell.props.shellVersion === 'string') {
|
||||
shell.shellVersionArr = (
|
||||
shell.props.shellVersion.match(/(\d+)\.(\d+)\.(\d+)/) || []
|
||||
)
|
||||
.slice(1, 4)
|
||||
.map(Number);
|
||||
}
|
||||
events.emit('received-props', shell.props);
|
||||
|
||||
obj.signals.forEach(function (sig) {
|
||||
send({
|
||||
type: QtMsgTypes.connectToSignal,
|
||||
object: QtObjId,
|
||||
signal: sig[1],
|
||||
});
|
||||
});
|
||||
|
||||
const onEvent = obj.methods.filter(function (x) {
|
||||
return x[0] === 'onEvent';
|
||||
})[0];
|
||||
|
||||
shell.send = function (ev, args) {
|
||||
send({
|
||||
type: QtMsgTypes.invokeMethod,
|
||||
object: QtObjId,
|
||||
method: onEvent[1],
|
||||
args: [ev, args || {}],
|
||||
});
|
||||
};
|
||||
|
||||
shell.send('app-ready', {}); // signal that we're ready to take events
|
||||
}
|
||||
|
||||
if (msg.object === QtObjId && msg.type === QtMsgTypes.signal)
|
||||
events.emit(msg.args[0], msg.args[1]);
|
||||
events.emit('init');
|
||||
};
|
||||
send({ type: QtMsgTypes.init });
|
||||
}) .catch((error) => {
|
||||
events.emit('init-error', error);
|
||||
});
|
||||
|
||||
this.on = function(name, listener) {
|
||||
events.on(name, listener);
|
||||
};
|
||||
this.off = function(name, listener) {
|
||||
events.off(name, listener);
|
||||
};
|
||||
this.removeAllListeners = function() {
|
||||
events.removeAllListeners();
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = ShellTransport;
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
const Chromecast = require('./Chromecast');
|
||||
const Core = require('./Core');
|
||||
const DragAndDrop = require('./DragAndDrop');
|
||||
const KeyboardShortcuts = require('./KeyboardShortcuts');
|
||||
const { ServicesProvider, useServices } = require('./ServicesContext');
|
||||
const Shell = require('./Shell');
|
||||
|
|
@ -9,6 +10,7 @@ const Shell = require('./Shell');
|
|||
module.exports = {
|
||||
Chromecast,
|
||||
Core,
|
||||
DragAndDrop,
|
||||
KeyboardShortcuts,
|
||||
ServicesProvider,
|
||||
useServices,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@ const COMMIT_HASH = execSync('git rev-parse HEAD').toString().trim();
|
|||
module.exports = (env, argv) => ({
|
||||
mode: argv.mode,
|
||||
devtool: argv.mode === 'production' ? 'source-map' : 'eval-source-map',
|
||||
entry: './src/index.js',
|
||||
entry: {
|
||||
main: './src/index.js',
|
||||
worker: './node_modules/@stremio/stremio-core-web/worker.js'
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'build'),
|
||||
filename: `${COMMIT_HASH}/scripts/[name].js`
|
||||
|
|
|
|||
Loading…
Reference in a new issue