mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 17:15:48 +00:00
Merge branch 'development' of github.com:Stremio/stremio-web into nav-bars
This commit is contained in:
commit
092f8dc321
54 changed files with 1016 additions and 223 deletions
|
|
@ -13,6 +13,7 @@
|
||||||
"FB": "readonly"
|
"FB": "readonly"
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
|
"node": true,
|
||||||
"commonjs": true,
|
"commonjs": true,
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"es6": true
|
"es6": true
|
||||||
|
|
|
||||||
23
.github/workflows/build.yml
vendored
Normal file
23
.github/workflows/build.yml
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '*'
|
||||||
|
tags-ignore:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
echo -e "[url \"https://github.com/\"]\n\tinsteadOf = ssh://git@github.com/" > ~/.gitconfig
|
||||||
|
yarn install && yarn build
|
||||||
|
- uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: stremio-web
|
||||||
|
path: build
|
||||||
34
.github/workflows/release.yml
vendored
Normal file
34
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- '*'
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
echo -e "[url \"https://github.com/\"]\n\tinsteadOf = ssh://git@github.com/" > ~/.gitconfig
|
||||||
|
yarn install && yarn build
|
||||||
|
- run: zip -r stremio-web.zip ./build
|
||||||
|
- name: Upload binaries to release
|
||||||
|
uses: svenstaro/upload-release-action@v1-release
|
||||||
|
with:
|
||||||
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
file: stremio-web.zip
|
||||||
|
asset_name: stremio-web.zip
|
||||||
|
tag: ${{ github.ref }}
|
||||||
|
overwrite: true
|
||||||
|
- run: |
|
||||||
|
curl -H "Content-Type: application/zip" \
|
||||||
|
-H "Authorization: Bearer ${{ secrets.netlify_access_token }}" \
|
||||||
|
--data-binary "@stremio-web.zip" \
|
||||||
|
https://api.netlify.com/api/v1/sites/stremio-staging.netlify.com/deploys
|
||||||
|
|
||||||
20
.github/workflows/test.yml
vendored
Normal file
20
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
name: Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '*'
|
||||||
|
tags-ignore:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
echo -e "[url \"https://github.com/\"]\n\tinsteadOf = ssh://git@github.com/" > ~/.gitconfig
|
||||||
|
yarn install && yarn test
|
||||||
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
/node_modules
|
/node_modules
|
||||||
/dist
|
/build
|
||||||
/package-lock.json
|
/package-lock.json
|
||||||
/npm-debug.log
|
/npm-debug.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"lint": "eslint src"
|
"lint": "eslint src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@sentry/browser": "5.11.1",
|
||||||
"a-color-picker": "1.2.1",
|
"a-color-picker": "1.2.1",
|
||||||
"classnames": "2.2.6",
|
"classnames": "2.2.6",
|
||||||
"events": "1.1.1",
|
"events": "1.1.1",
|
||||||
|
|
@ -50,6 +51,7 @@
|
||||||
"css-loader": "3.4.0",
|
"css-loader": "3.4.0",
|
||||||
"cssnano": "4.1.10",
|
"cssnano": "4.1.10",
|
||||||
"cssnano-preset-advanced": "4.0.7",
|
"cssnano-preset-advanced": "4.0.7",
|
||||||
|
"dotenv": "8.2.0",
|
||||||
"eslint": "6.7.2",
|
"eslint": "6.7.2",
|
||||||
"eslint-plugin-react": "7.17.0",
|
"eslint-plugin-react": "7.17.0",
|
||||||
"html-webpack-plugin": "3.2.0",
|
"html-webpack-plugin": "3.2.0",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ require('spatial-navigation-polyfill');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { Router } = require('stremio-router');
|
const { Router } = require('stremio-router');
|
||||||
const { Core, KeyboardNavigation, ServicesProvider, Shell } = require('stremio/services');
|
const { Core, KeyboardNavigation, ServicesProvider, Shell } = require('stremio/services');
|
||||||
|
const { ToastProvider } = require('stremio/common');
|
||||||
|
const CoreEventsToaster = require('./CoreEventsToaster');
|
||||||
const routerViewsConfig = require('./routerViewsConfig');
|
const routerViewsConfig = require('./routerViewsConfig');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
||||||
|
|
@ -21,11 +23,16 @@ const App = () => {
|
||||||
setShellInitialized(services.shell.active || services.shell.error instanceof Error);
|
setShellInitialized(services.shell.active || services.shell.error instanceof Error);
|
||||||
};
|
};
|
||||||
const onCoreStateChanged = () => {
|
const onCoreStateChanged = () => {
|
||||||
setCoreInitialized(services.core.active || services.core.error instanceof Error);
|
|
||||||
if (services.core.active) {
|
if (services.core.active) {
|
||||||
services.core.dispatch({ action: 'LoadCtx' });
|
|
||||||
window.core = services.core;
|
window.core = services.core;
|
||||||
|
services.core.dispatch({
|
||||||
|
action: 'Load',
|
||||||
|
args: {
|
||||||
|
model: 'Ctx'
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
setCoreInitialized(services.core.active || services.core.error instanceof Error);
|
||||||
};
|
};
|
||||||
services.shell.on('stateChanged', onShellStateChanged);
|
services.shell.on('stateChanged', onShellStateChanged);
|
||||||
services.core.on('stateChanged', onCoreStateChanged);
|
services.core.on('stateChanged', onCoreStateChanged);
|
||||||
|
|
@ -45,12 +52,15 @@ const App = () => {
|
||||||
<ServicesProvider services={services}>
|
<ServicesProvider services={services}>
|
||||||
{
|
{
|
||||||
shellInitialized && coreInitialized ?
|
shellInitialized && coreInitialized ?
|
||||||
<Router
|
<ToastProvider className={styles['toasts-container']}>
|
||||||
className={styles['router']}
|
<CoreEventsToaster />
|
||||||
homePath={'/'}
|
<Router
|
||||||
viewsConfig={routerViewsConfig}
|
className={styles['router']}
|
||||||
onPathNotMatch={onPathNotMatch}
|
homePath={'/'}
|
||||||
/>
|
viewsConfig={routerViewsConfig}
|
||||||
|
onPathNotMatch={onPathNotMatch}
|
||||||
|
/>
|
||||||
|
</ToastProvider>
|
||||||
:
|
:
|
||||||
<div className={styles['app-loader']} />
|
<div className={styles['app-loader']} />
|
||||||
}
|
}
|
||||||
|
|
|
||||||
29
src/App/CoreEventsToaster.js
Normal file
29
src/App/CoreEventsToaster.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
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 onEvent = ({ event, args }) => {
|
||||||
|
// UserAuthenticated are handled only in the /intro route
|
||||||
|
if (event === 'Error' && args.source.event !== 'UserAuthenticated') {
|
||||||
|
toast.show({
|
||||||
|
type: 'error',
|
||||||
|
title: args.source.event,
|
||||||
|
message: args.error.message,
|
||||||
|
icon: 'ic_warning',
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
core.on('Event', onEvent);
|
||||||
|
return () => {
|
||||||
|
core.off('Event', onEvent);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = CoreEventsToaster;
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
--landscape-shape-ratio: 0.5625;
|
--landscape-shape-ratio: 0.5625;
|
||||||
--poster-shape-ratio: 1.464;
|
--poster-shape-ratio: 1.464;
|
||||||
--scroll-bar-width: 6px;
|
--scroll-bar-width: 6px;
|
||||||
|
--nav-bar-size: 3.2rem;
|
||||||
--focus-outline-size: 2px;
|
--focus-outline-size: 2px;
|
||||||
--color-facebook: #4267b2;
|
--color-facebook: #4267b2;
|
||||||
--color-twitter: #1DA1F2;
|
--color-twitter: #1DA1F2;
|
||||||
|
|
@ -71,6 +72,23 @@ html {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
.toasts-container {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(1.2 * var(--nav-bar-size));
|
||||||
|
right: 0;
|
||||||
|
bottom: calc(1.2 * var(--nav-bar-size));
|
||||||
|
left: auto;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0 calc(1.2 * var(--nav-bar-size));
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: var(--scroll-bar-width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.router {
|
.router {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ const ColorInput = ({ className, value, dataset, onChange, ...props }) => {
|
||||||
ColorInput.propTypes = {
|
ColorInput.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
dataset: PropTypes.objectOf(PropTypes.string),
|
dataset: PropTypes.object,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
onClick: PropTypes.func
|
onClick: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ MetaItem.propTypes = {
|
||||||
playIcon: PropTypes.bool,
|
playIcon: PropTypes.bool,
|
||||||
progress: PropTypes.number,
|
progress: PropTypes.number,
|
||||||
options: PropTypes.array,
|
options: PropTypes.array,
|
||||||
dataset: PropTypes.objectOf(PropTypes.string),
|
dataset: PropTypes.object,
|
||||||
optionOnSelect: PropTypes.func,
|
optionOnSelect: PropTypes.func,
|
||||||
onClick: PropTypes.func
|
onClick: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ ModalDialog.propTypes = {
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
PropTypes.node
|
PropTypes.node
|
||||||
]),
|
]),
|
||||||
dataset: PropTypes.objectOf(PropTypes.string),
|
dataset: PropTypes.object,
|
||||||
onCloseRequest: PropTypes.func
|
onCloseRequest: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ Multiselect.propTypes = {
|
||||||
})),
|
})),
|
||||||
selected: PropTypes.arrayOf(PropTypes.string),
|
selected: PropTypes.arrayOf(PropTypes.string),
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
dataset: PropTypes.objectOf(PropTypes.string),
|
dataset: PropTypes.object,
|
||||||
renderLabelContent: PropTypes.func,
|
renderLabelContent: PropTypes.func,
|
||||||
renderLabelText: PropTypes.func,
|
renderLabelText: PropTypes.func,
|
||||||
onOpen: PropTypes.func,
|
onOpen: PropTypes.func,
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,19 @@ const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const classnames = require('classnames');
|
const classnames = require('classnames');
|
||||||
const Icon = require('stremio-icons/dom');
|
const Icon = require('stremio-icons/dom');
|
||||||
|
const { useServices } = require('stremio/services');
|
||||||
const Button = require('stremio/common/Button');
|
const Button = require('stremio/common/Button');
|
||||||
const Popup = require('stremio/common/Popup');
|
const Popup = require('stremio/common/Popup');
|
||||||
const useBinaryState = require('stremio/common/useBinaryState');
|
const useBinaryState = require('stremio/common/useBinaryState');
|
||||||
const useFullscreen = require('stremio/common/useFullscreen');
|
const useFullscreen = require('stremio/common/useFullscreen');
|
||||||
const useUser = require('stremio/common/useUser');
|
const useProfile = require('./useProfile');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
||||||
const NavMenu = ({ className }) => {
|
const NavMenu = ({ className }) => {
|
||||||
|
const { core } = useServices();
|
||||||
|
const profile = useProfile();
|
||||||
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
|
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
|
||||||
const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen();
|
const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen();
|
||||||
const [user, logout] = useUser();
|
|
||||||
const popupLabelOnClick = React.useCallback((event) => {
|
const popupLabelOnClick = React.useCallback((event) => {
|
||||||
if (!event.nativeEvent.togglePopupPrevented) {
|
if (!event.nativeEvent.togglePopupPrevented) {
|
||||||
toggleMenu();
|
toggleMenu();
|
||||||
|
|
@ -22,7 +24,12 @@ const NavMenu = ({ className }) => {
|
||||||
event.nativeEvent.togglePopupPrevented = true;
|
event.nativeEvent.togglePopupPrevented = true;
|
||||||
}, []);
|
}, []);
|
||||||
const logoutButtonOnClick = React.useCallback(() => {
|
const logoutButtonOnClick = React.useCallback(() => {
|
||||||
logout();
|
core.dispatch({
|
||||||
|
action: 'Ctx',
|
||||||
|
args: {
|
||||||
|
action: 'Logout'
|
||||||
|
}
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<Popup
|
<Popup
|
||||||
|
|
@ -40,17 +47,17 @@ const NavMenu = ({ className }) => {
|
||||||
<div
|
<div
|
||||||
className={styles['avatar-container']}
|
className={styles['avatar-container']}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: user === null ?
|
backgroundImage: profile.auth === null ?
|
||||||
'url(\'/images/anonymous.png\')'
|
'url(\'/images/anonymous.png\')'
|
||||||
:
|
:
|
||||||
`url('${user.avatar}'), url('/images/default_avatar.png')`
|
`url('${profile.auth.user.avatar}'), url('/images/default_avatar.png')`
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className={styles['email-container']}>
|
<div className={styles['email-container']}>
|
||||||
<div className={styles['email-label']}>{user === null ? 'Anonymous user' : user.email}</div>
|
<div className={styles['email-label']}>{profile.auth === null ? 'Anonymous user' : profile.auth.user.email}</div>
|
||||||
</div>
|
</div>
|
||||||
<Button className={styles['logout-button-container']} title={user === null ? 'Log in / Sign up' : 'Log out'} href={'#/intro'} onClick={logoutButtonOnClick}>
|
<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']}>{user === null ? 'Log in / Sign up' : 'Log out'}</div>
|
<div className={styles['logout-label']}>{profile.auth === null ? 'Log in / Sign up' : 'Log out'}</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['nav-menu-section']}>
|
<div className={styles['nav-menu-section']}>
|
||||||
|
|
|
||||||
23
src/common/NavBar/NavMenu/useProfile.js
Normal file
23
src/common/NavBar/NavMenu/useProfile.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
const React = require('react');
|
||||||
|
const { useServices } = require('stremio/services');
|
||||||
|
const useModelState = require('stremio/common/useModelState');
|
||||||
|
|
||||||
|
const mapProfileState = (ctx) => {
|
||||||
|
return ctx.profile;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useProfile = () => {
|
||||||
|
const { core } = useServices();
|
||||||
|
const initProfileState = React.useCallback(() => {
|
||||||
|
const ctx = core.getState('ctx');
|
||||||
|
return mapProfileState(ctx);
|
||||||
|
}, []);
|
||||||
|
const profile = useModelState({
|
||||||
|
model: 'ctx',
|
||||||
|
init: initProfileState,
|
||||||
|
map: mapProfileState
|
||||||
|
});
|
||||||
|
return profile;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = useProfile;
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
.nav-bar-container {
|
.nav-bar-container {
|
||||||
--nav-bar-size: 3.2rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ const PaginationInput = ({ className, label, dataset, onSelect, ...props }) => {
|
||||||
PaginationInput.propTypes = {
|
PaginationInput.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
dataset: PropTypes.objectOf(PropTypes.string),
|
dataset: PropTypes.object,
|
||||||
onSelect: PropTypes.func
|
onSelect: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ Popup.propTypes = {
|
||||||
direction: PropTypes.oneOf(['top-left', 'bottom-left', 'top-right', 'bottom-right']),
|
direction: PropTypes.oneOf(['top-left', 'bottom-left', 'top-right', 'bottom-right']),
|
||||||
renderLabel: PropTypes.func.isRequired,
|
renderLabel: PropTypes.func.isRequired,
|
||||||
renderMenu: PropTypes.func.isRequired,
|
renderMenu: PropTypes.func.isRequired,
|
||||||
dataset: PropTypes.objectOf(PropTypes.string),
|
dataset: PropTypes.object,
|
||||||
onCloseRequest: PropTypes.func
|
onCloseRequest: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const classnames = require('classnames');
|
const classnames = require('classnames');
|
||||||
const { useFocusedRoute } = require('stremio-router');
|
const { useRouteFocused } = require('stremio-router');
|
||||||
const useAnimationFrame = require('stremio/common/useAnimationFrame');
|
const useAnimationFrame = require('stremio/common/useAnimationFrame');
|
||||||
const useLiveRef = require('stremio/common/useLiveRef');
|
const useLiveRef = require('stremio/common/useLiveRef');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
@ -13,7 +13,7 @@ const Slider = ({ className, value, minimumValue, maximumValue, onSlide, onCompl
|
||||||
const onSlideRef = useLiveRef(onSlide, [onSlide]);
|
const onSlideRef = useLiveRef(onSlide, [onSlide]);
|
||||||
const onCompleteRef = useLiveRef(onComplete, [onComplete]);
|
const onCompleteRef = useLiveRef(onComplete, [onComplete]);
|
||||||
const sliderContainerRef = React.useRef(null);
|
const sliderContainerRef = React.useRef(null);
|
||||||
const routeFocused = useFocusedRoute();
|
const routeFocused = useRouteFocused();
|
||||||
const [requestThumbAnimation, cancelThumbAnimation] = useAnimationFrame();
|
const [requestThumbAnimation, cancelThumbAnimation] = useAnimationFrame();
|
||||||
const calculateValueForMouseX = React.useCallback((mouseX) => {
|
const calculateValueForMouseX = React.useCallback((mouseX) => {
|
||||||
if (sliderContainerRef.current === null) {
|
if (sliderContainerRef.current === null) {
|
||||||
|
|
|
||||||
10
src/common/Toast/ToastContext.js
Normal file
10
src/common/Toast/ToastContext.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
const React = require('react');
|
||||||
|
|
||||||
|
const ToastContext = React.createContext({
|
||||||
|
show: () => { },
|
||||||
|
clear: () => { }
|
||||||
|
});
|
||||||
|
|
||||||
|
ToastContext.displayName = 'ToastContext';
|
||||||
|
|
||||||
|
module.exports = ToastContext;
|
||||||
71
src/common/Toast/ToastItem/ToastItem.js
Normal file
71
src/common/Toast/ToastItem/ToastItem.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
const React = require('react');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const classnames = require('classnames');
|
||||||
|
const Icon = require('stremio-icons/dom');
|
||||||
|
const Button = require('stremio/common/Button');
|
||||||
|
const styles = require('./styles');
|
||||||
|
|
||||||
|
const ToastItem = ({ type, title, message, icon, dataset, onSelect, onClose }) => {
|
||||||
|
const toastOnClick = React.useCallback((event) => {
|
||||||
|
if (!event.nativeEvent.selectPrevented && typeof onSelect === 'function') {
|
||||||
|
onSelect({
|
||||||
|
type: 'select',
|
||||||
|
dataset: dataset,
|
||||||
|
reactEvent: event,
|
||||||
|
nativeEvent: event.nativeEvent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [dataset, onSelect]);
|
||||||
|
const closeButtonOnClick = React.useCallback((event) => {
|
||||||
|
event.nativeEvent.selectPrevented = true;
|
||||||
|
if (typeof onClose === 'function') {
|
||||||
|
onClose({
|
||||||
|
type: 'close',
|
||||||
|
dataset: dataset,
|
||||||
|
reactEvent: event,
|
||||||
|
nativeEvent: event.nativeEvent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [dataset, onClose]);
|
||||||
|
return (
|
||||||
|
<Button className={classnames(styles['toast-item-container'], styles['success'], styles[type])} tabIndex={-1} onClick={toastOnClick}>
|
||||||
|
{
|
||||||
|
typeof icon === 'string' && icon.length > 0 ?
|
||||||
|
<div className={styles['icon-container']}>
|
||||||
|
<Icon className={styles['icon']} icon={icon} />
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
<div className={styles['info-container']}>
|
||||||
|
{
|
||||||
|
typeof title === 'string' && title.length > 0 ?
|
||||||
|
<div className={styles['title-container']}>{title}</div>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
typeof message === 'string' && message.length > 0 ?
|
||||||
|
<div className={styles['message-container']}>{message}</div>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<Button className={styles['close-button-container']} title={'Close'} tabIndex={-1} onClick={closeButtonOnClick}>
|
||||||
|
<Icon className={styles['icon']} icon={'ic_x'} />
|
||||||
|
</Button>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ToastItem.propTypes = {
|
||||||
|
type: PropTypes.oneOf(['success', 'alert', 'error']),
|
||||||
|
title: PropTypes.string,
|
||||||
|
message: PropTypes.string,
|
||||||
|
icon: PropTypes.string,
|
||||||
|
dataset: PropTypes.object,
|
||||||
|
onSelect: PropTypes.func,
|
||||||
|
onClose: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = ToastItem;
|
||||||
3
src/common/Toast/ToastItem/index.js
Normal file
3
src/common/Toast/ToastItem/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
const ToastItem = require('./ToastItem');
|
||||||
|
|
||||||
|
module.exports = ToastItem;
|
||||||
73
src/common/Toast/ToastItem/styles.less
Normal file
73
src/common/Toast/ToastItem/styles.less
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
.toast-item-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 25rem;
|
||||||
|
min-height: 6rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: thin solid;
|
||||||
|
background-color: var(--color-surfacelighter);
|
||||||
|
overflow: visible;
|
||||||
|
box-shadow: 0 0.3rem 0.5rem var(--color-backgrounddarker40),
|
||||||
|
0 0.6rem 1rem var(--color-backgrounddarker20);
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
color: var(--color-signal5);
|
||||||
|
fill: var(--color-signal5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.alert {
|
||||||
|
color: var(--color-signal3);
|
||||||
|
fill: var(--color-signal3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
color: var(--color-signal2);
|
||||||
|
fill: var(--color-signal2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
flex: none;
|
||||||
|
align-self: stretch;
|
||||||
|
width: 4.5rem;
|
||||||
|
padding: 1.2rem 0 1.2rem 1.2rem;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-container {
|
||||||
|
flex: 1;
|
||||||
|
align-self: stretch;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.title-container {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-container {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button-container {
|
||||||
|
flex: none;
|
||||||
|
align-self: flex-start;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-surfacelight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/common/Toast/ToastProvider.js
Normal file
72
src/common/Toast/ToastProvider.js
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
const React = require('react');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const ToastItem = require('./ToastItem');
|
||||||
|
const ToastContext = require('./ToastContext');
|
||||||
|
|
||||||
|
const DEFAULT_TIMEOUT = 3000;
|
||||||
|
|
||||||
|
const ToastProvider = ({ className, children }) => {
|
||||||
|
const [container, setContainer] = React.useState(null);
|
||||||
|
const [items, dispatch] = React.useReducer(
|
||||||
|
(items, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'add':
|
||||||
|
return items.concat(action.item);
|
||||||
|
case 'remove':
|
||||||
|
return items.filter((item) => item.id !== action.id);
|
||||||
|
case 'clear':
|
||||||
|
return [];
|
||||||
|
default:
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const itemOnClose = React.useCallback((event) => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
clear: () => {
|
||||||
|
dispatch({ type: 'clear' });
|
||||||
|
}
|
||||||
|
}), []);
|
||||||
|
return (
|
||||||
|
<ToastContext.Provider value={toast}>
|
||||||
|
{container instanceof HTMLElement ? children : null}
|
||||||
|
<div ref={setContainer} className={className}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<ToastItem key={index} {...item} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ToastContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ToastProvider.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
children: PropTypes.node
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = ToastProvider;
|
||||||
7
src/common/Toast/index.js
Normal file
7
src/common/Toast/index.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
const ToastProvider = require('./ToastProvider');
|
||||||
|
const useToast = require('./useToast');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ToastProvider,
|
||||||
|
useToast
|
||||||
|
};
|
||||||
8
src/common/Toast/useToast.js
Normal file
8
src/common/Toast/useToast.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
const React = require('react');
|
||||||
|
const ToastContext = require('./ToastContext');
|
||||||
|
|
||||||
|
const useToast = () => {
|
||||||
|
return React.useContext(ToastContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = useToast;
|
||||||
|
|
@ -17,15 +17,17 @@ const Popup = require('./Popup');
|
||||||
const SharePrompt = require('./SharePrompt');
|
const SharePrompt = require('./SharePrompt');
|
||||||
const Slider = require('./Slider');
|
const Slider = require('./Slider');
|
||||||
const TextInput = require('./TextInput');
|
const TextInput = require('./TextInput');
|
||||||
|
const { ToastProvider, useToast } = require('./Toast');
|
||||||
const routesRegexp = require('./routesRegexp');
|
const routesRegexp = require('./routesRegexp');
|
||||||
const useAnimationFrame = require('./useAnimationFrame');
|
const useAnimationFrame = require('./useAnimationFrame');
|
||||||
const useBinaryState = require('./useBinaryState');
|
const useBinaryState = require('./useBinaryState');
|
||||||
|
const useCoreEvent = require('./useCoreEvent');
|
||||||
|
const useDeepEqualEffect = require('./useDeepEqualEffect');
|
||||||
const useDeepEqualState = require('./useDeepEqualState');
|
const useDeepEqualState = require('./useDeepEqualState');
|
||||||
const useFullscreen = require('./useFullscreen');
|
const useFullscreen = require('./useFullscreen');
|
||||||
const useInLibrary = require('./useInLibrary');
|
const useInLibrary = require('./useInLibrary');
|
||||||
const useLiveRef = require('./useLiveRef');
|
const useLiveRef = require('./useLiveRef');
|
||||||
const useModelState = require('./useModelState');
|
const useModelState = require('./useModelState');
|
||||||
const useUser = require('./useUser');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
AddonDetailsModal,
|
AddonDetailsModal,
|
||||||
|
|
@ -47,13 +49,16 @@ module.exports = {
|
||||||
SharePrompt,
|
SharePrompt,
|
||||||
Slider,
|
Slider,
|
||||||
TextInput,
|
TextInput,
|
||||||
|
ToastProvider,
|
||||||
|
useToast,
|
||||||
routesRegexp,
|
routesRegexp,
|
||||||
useAnimationFrame,
|
useAnimationFrame,
|
||||||
useBinaryState,
|
useBinaryState,
|
||||||
|
useCoreEvent,
|
||||||
|
useDeepEqualEffect,
|
||||||
useDeepEqualState,
|
useDeepEqualState,
|
||||||
useFullscreen,
|
useFullscreen,
|
||||||
useInLibrary,
|
useInLibrary,
|
||||||
useLiveRef,
|
useLiveRef,
|
||||||
useModelState,
|
useModelState
|
||||||
useUser
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ const routesRegexp = {
|
||||||
urlParamsNames: []
|
urlParamsNames: []
|
||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
regexp: /^\/player\/([^/]*)\/([^/]*)\/([^/]*)\/([^/]*)\/([^/]*)$/,
|
regexp: /^\/player\/([^/]*)(?:\/([^/]*)\/([^/]*)\/([^/]*)\/([^/]*))?$/,
|
||||||
urlParamsNames: ['transportUrl', 'type', 'id', 'videoId', 'stream']
|
urlParamsNames: ['stream', 'transportUrl', 'type', 'id', 'videoId']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
18
src/common/useCoreEvent.js
Normal file
18
src/common/useCoreEvent.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
const React = require('react');
|
||||||
|
const { useRouteFocused } = require('stremio-router');
|
||||||
|
const { useServices } = require('stremio/services');
|
||||||
|
|
||||||
|
const useCoreEvent = (onEvent) => {
|
||||||
|
const { core } = useServices();
|
||||||
|
const routeFocused = useRouteFocused();
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
if (routeFocused) {
|
||||||
|
core.on('Event', onEvent);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
core.off('Event', onEvent);
|
||||||
|
};
|
||||||
|
}, [routeFocused, onEvent]);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = useCoreEvent;
|
||||||
17
src/common/useDeepEqualEffect.js
Normal file
17
src/common/useDeepEqualEffect.js
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
const React = require('react');
|
||||||
|
const isEqual = require('lodash.isequal');
|
||||||
|
|
||||||
|
const useDeepEqualEffect = (cb, deps) => {
|
||||||
|
const mountedRef = React.useRef(false);
|
||||||
|
const depsRef = React.useRef(null);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!mountedRef.current || !isEqual(depsRef.current, deps)) {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
mountedRef.current = true;
|
||||||
|
depsRef.current = deps;
|
||||||
|
}, [deps]);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = useDeepEqualEffect;
|
||||||
|
|
@ -8,7 +8,7 @@ const UNLOAD_ACTION = {
|
||||||
action: 'Unload',
|
action: 'Unload',
|
||||||
};
|
};
|
||||||
|
|
||||||
const useModelState = ({ model, action, timeout, onNewState, map, mapWithCtx, init }) => {
|
const useModelState = ({ model, init, action, timeout, onNewState, map, mapWithCtx }) => {
|
||||||
const modelRef = React.useRef(model);
|
const modelRef = React.useRef(model);
|
||||||
const mountedRef = React.useRef(false);
|
const mountedRef = React.useRef(false);
|
||||||
const { core } = useServices();
|
const { core } = useServices();
|
||||||
|
|
@ -52,7 +52,7 @@ const useModelState = ({ model, action, timeout, onNewState, map, mapWithCtx, in
|
||||||
onNewStateThrottled.cancel();
|
onNewStateThrottled.cancel();
|
||||||
core.off('NewState', onNewStateThrottled);
|
core.off('NewState', onNewStateThrottled);
|
||||||
};
|
};
|
||||||
}, [routeFocused]);
|
}, [routeFocused, timeout, onNewState, map, mapWithCtx]);
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
mountedRef.current = true;
|
mountedRef.current = true;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
const React = require('react');
|
|
||||||
const { useServices } = require('stremio/services');
|
|
||||||
const useModelState = require('stremio/common/useModelState');
|
|
||||||
|
|
||||||
const mapUserState = (ctx) => {
|
|
||||||
return ctx.content.auth ? ctx.content.auth.user : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useUser = () => {
|
|
||||||
const { core } = useServices();
|
|
||||||
const logout = React.useCallback(() => {
|
|
||||||
core.dispatch({
|
|
||||||
action: 'UserOp',
|
|
||||||
args: {
|
|
||||||
userOp: 'Logout'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
const initUserState = React.useCallback(() => {
|
|
||||||
const ctx = core.getState('ctx');
|
|
||||||
return mapUserState(ctx);
|
|
||||||
}, []);
|
|
||||||
const user = useModelState({
|
|
||||||
model: 'ctx',
|
|
||||||
map: mapUserState,
|
|
||||||
init: initUserState
|
|
||||||
});
|
|
||||||
return [user, logout];
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = useUser;
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom');
|
const ReactDOM = require('react-dom');
|
||||||
|
const Sentry = require('@sentry/browser');
|
||||||
const App = require('./App');
|
const App = require('./App');
|
||||||
|
|
||||||
|
if (typeof process.env.SENTRY_DSN === 'string') {
|
||||||
|
Sentry.init({ dsn: process.env.SENTRY_DSN });
|
||||||
|
}
|
||||||
|
|
||||||
ReactDOM.render(<App />, document.getElementById('app'));
|
ReactDOM.render(<App />, document.getElementById('app'));
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ const classnames = require('classnames');
|
||||||
const FocusLock = require('react-focus-lock').default;
|
const FocusLock = require('react-focus-lock').default;
|
||||||
const { useModalsContainer } = require('../ModalsContainerContext');
|
const { useModalsContainer } = require('../ModalsContainerContext');
|
||||||
|
|
||||||
const Modal = ({ className, autoFocus, children, ...props }) => {
|
const Modal = ({ className, autoFocus, disabled, children, ...props }) => {
|
||||||
const modalsContainer = useModalsContainer();
|
const modalsContainer = useModalsContainer();
|
||||||
return ReactDOM.createPortal(
|
return ReactDOM.createPortal(
|
||||||
<FocusLock className={classnames(className, 'modal-container')} autoFocus={autoFocus} lockProps={props}>
|
<FocusLock className={classnames(className, 'modal-container')} autoFocus={autoFocus} disabled={disabled} lockProps={props}>
|
||||||
{children}
|
{children}
|
||||||
</FocusLock>,
|
</FocusLock>,
|
||||||
modalsContainer
|
modalsContainer
|
||||||
|
|
@ -18,6 +18,7 @@ const Modal = ({ className, autoFocus, children, ...props }) => {
|
||||||
Modal.propTypes = {
|
Modal.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
children: PropTypes.node
|
children: PropTypes.node
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ Addon.propTypes = {
|
||||||
installed: PropTypes.bool,
|
installed: PropTypes.bool,
|
||||||
onToggle: PropTypes.func,
|
onToggle: PropTypes.func,
|
||||||
onShare: PropTypes.func,
|
onShare: PropTypes.func,
|
||||||
dataset: PropTypes.objectOf(PropTypes.string)
|
dataset: PropTypes.object
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Addon;
|
module.exports = Addon;
|
||||||
|
|
|
||||||
|
|
@ -21,16 +21,16 @@ const mapAddonsStateWithCtx = (addons, ctx) => {
|
||||||
...addons.catalog_resource,
|
...addons.catalog_resource,
|
||||||
content: {
|
content: {
|
||||||
...addons.catalog_resource.content,
|
...addons.catalog_resource.content,
|
||||||
content: addons.catalog_resource.content.content.map((descriptor) => ({
|
content: addons.catalog_resource.content.content.map((addon) => ({
|
||||||
transportUrl: descriptor.transportUrl,
|
transportUrl: addon.transportUrl,
|
||||||
installed: ctx.content.addons.some((addon) => addon.transportUrl === descriptor.transportUrl),
|
installed: ctx.profile.addons.some(({ transportUrl }) => transportUrl === addon.transportUrl),
|
||||||
manifest: {
|
manifest: {
|
||||||
id: descriptor.manifest.id,
|
id: addon.manifest.id,
|
||||||
name: descriptor.manifest.name,
|
name: addon.manifest.name,
|
||||||
version: descriptor.manifest.version,
|
version: addon.manifest.version,
|
||||||
logo: descriptor.manifest.logo,
|
logo: addon.manifest.logo,
|
||||||
description: descriptor.manifest.description,
|
description: addon.manifest.description,
|
||||||
types: descriptor.manifest.types
|
types: addon.manifest.types
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
@ -45,8 +45,10 @@ const onNewAddonsState = (addons) => {
|
||||||
return {
|
return {
|
||||||
action: 'Load',
|
action: 'Load',
|
||||||
args: {
|
args: {
|
||||||
load: 'CatalogFiltered',
|
model: 'CatalogFiltered',
|
||||||
args: addons.selectable.catalogs[0].load_request
|
args: {
|
||||||
|
request: addons.selectable.catalogs[0].request
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -59,14 +61,16 @@ const useAddons = (urlParams) => {
|
||||||
return {
|
return {
|
||||||
action: 'Load',
|
action: 'Load',
|
||||||
args: {
|
args: {
|
||||||
load: 'CatalogFiltered',
|
model: 'CatalogFiltered',
|
||||||
args: {
|
args: {
|
||||||
base: urlParams.transportUrl,
|
request: {
|
||||||
path: {
|
base: urlParams.transportUrl,
|
||||||
resource: 'addon_catalog',
|
path: {
|
||||||
type_name: urlParams.type,
|
resource: 'addon_catalog',
|
||||||
id: urlParams.catalogId,
|
type_name: urlParams.type,
|
||||||
extra: []
|
id: urlParams.catalogId,
|
||||||
|
extra: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -77,8 +81,10 @@ const useAddons = (urlParams) => {
|
||||||
return {
|
return {
|
||||||
action: 'Load',
|
action: 'Load',
|
||||||
args: {
|
args: {
|
||||||
load: 'CatalogFiltered',
|
model: 'CatalogFiltered',
|
||||||
args: addons.selectable.catalogs[0].load_request
|
args: {
|
||||||
|
request: addons.selectable.catalogs[0].request
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
|
||||||
const navigateWithLoadRequest = (load_request) => {
|
const navigateWithRequest = (request) => {
|
||||||
const transportUrl = encodeURIComponent(load_request.base);
|
const transportUrl = encodeURIComponent(request.base);
|
||||||
const catalogId = encodeURIComponent(load_request.path.id);
|
const catalogId = encodeURIComponent(request.path.id);
|
||||||
const type = encodeURIComponent(load_request.path.type_name);
|
const type = encodeURIComponent(request.path.type_name);
|
||||||
window.location.replace(`#/addons/${transportUrl}/${catalogId}/${type}`);
|
window.location.replace(`#/addons/${transportUrl}/${catalogId}/${type}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -18,35 +18,35 @@ const mapSelectableInputs = (addons) => {
|
||||||
const catalogSelect = {
|
const catalogSelect = {
|
||||||
title: 'Select catalog',
|
title: 'Select catalog',
|
||||||
options: addons.selectable.catalogs
|
options: addons.selectable.catalogs
|
||||||
.map(({ name, load_request }) => ({
|
.map(({ name, request }) => ({
|
||||||
value: JSON.stringify(load_request),
|
value: JSON.stringify(request),
|
||||||
label: name
|
label: name
|
||||||
})),
|
})),
|
||||||
selected: addons.selectable.catalogs
|
selected: addons.selectable.catalogs
|
||||||
.filter(({ load_request: { path: { id } } }) => {
|
.filter(({ request: { path: { id } } }) => {
|
||||||
return addons.catalog_resource !== null &&
|
return addons.catalog_resource !== null &&
|
||||||
addons.catalog_resource.request.path.id === id;
|
addons.catalog_resource.request.path.id === id;
|
||||||
})
|
})
|
||||||
.map(({ load_request }) => JSON.stringify(load_request)),
|
.map(({ request }) => JSON.stringify(request)),
|
||||||
onSelect: (event) => {
|
onSelect: (event) => {
|
||||||
navigateWithLoadRequest(JSON.parse(event.value));
|
navigateWithRequest(JSON.parse(event.value));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const typeSelect = {
|
const typeSelect = {
|
||||||
title: 'Select type',
|
title: 'Select type',
|
||||||
options: addons.selectable.types
|
options: addons.selectable.types
|
||||||
.map(({ name, load_request }) => ({
|
.map(({ name, request }) => ({
|
||||||
value: JSON.stringify(load_request),
|
value: JSON.stringify(request),
|
||||||
label: name
|
label: name
|
||||||
})),
|
})),
|
||||||
selected: addons.selectable.types
|
selected: addons.selectable.types
|
||||||
.filter(({ load_request }) => {
|
.filter(({ request }) => {
|
||||||
return addons.catalog_resource !== null &&
|
return addons.catalog_resource !== null &&
|
||||||
equalWithouExtra(addons.catalog_resource.request, load_request);
|
equalWithouExtra(addons.catalog_resource.request, request);
|
||||||
})
|
})
|
||||||
.map(({ load_request }) => JSON.stringify(load_request)),
|
.map(({ request }) => JSON.stringify(request)),
|
||||||
onSelect: (event) => {
|
onSelect: (event) => {
|
||||||
navigateWithLoadRequest(JSON.parse(event.value));
|
navigateWithRequest(JSON.parse(event.value));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return [catalogSelect, typeSelect];
|
return [catalogSelect, typeSelect];
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const classnames = require('classnames');
|
const classnames = require('classnames');
|
||||||
const Icon = require('stremio-icons/dom');
|
const Icon = require('stremio-icons/dom');
|
||||||
const { Button, MainNavBar, MetaItem, MetaPreview, Multiselect, ModalDialog, PaginationInput, useBinaryState } = require('stremio/common');
|
const { AddonDetailsModal, Button, MainNavBar, MetaItem, MetaPreview, Multiselect, ModalDialog, PaginationInput, useBinaryState } = require('stremio/common');
|
||||||
|
const { useServices } = require('stremio/services');
|
||||||
const useDiscover = require('./useDiscover');
|
const useDiscover = require('./useDiscover');
|
||||||
const useSelectableInputs = require('./useSelectableInputs');
|
const useSelectableInputs = require('./useSelectableInputs');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
@ -19,9 +20,12 @@ const getMetaItemAtIndex = (catalog_resource, index) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Discover = ({ urlParams, queryParams }) => {
|
const Discover = ({ urlParams, queryParams }) => {
|
||||||
|
const { core } = useServices();
|
||||||
|
const state = core.getState();
|
||||||
const discover = useDiscover(urlParams, queryParams);
|
const discover = useDiscover(urlParams, queryParams);
|
||||||
const [selectInputs, paginationInput] = useSelectableInputs(discover);
|
const [selectInputs, paginationInput] = useSelectableInputs(discover);
|
||||||
const [inputsModalOpen, openInputsModal, closeInputsModal] = useBinaryState(false);
|
const [inputsModalOpen, openInputsModal, closeInputsModal] = useBinaryState(false);
|
||||||
|
const [addonModalOpen, openAddonModal, closeAddonModal] = useBinaryState(false);
|
||||||
const [selectedMetaItem, setSelectedMetaItem] = React.useState(() => {
|
const [selectedMetaItem, setSelectedMetaItem] = React.useState(() => {
|
||||||
return getMetaItemAtIndex(discover.catalog_resource, 0);
|
return getMetaItemAtIndex(discover.catalog_resource, 0);
|
||||||
});
|
});
|
||||||
|
|
@ -69,6 +73,17 @@ const Discover = ({ urlParams, queryParams }) => {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
{
|
||||||
|
discover.catalog_resource != null && !state.ctx.content.addons.some((addon) => addon.transportUrl === discover.catalog_resource.request.base) ?
|
||||||
|
<div className={styles['missing-addon-warning-container']}>
|
||||||
|
<div className={styles['warning-info']}>This addon is not installed. Install now?</div>
|
||||||
|
<Button className={styles['install-button']} title={'Install addon'} onClick={openAddonModal}>
|
||||||
|
<div className={styles['label']}>Install</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
<div className={styles['catalog-content-container']}>
|
<div className={styles['catalog-content-container']}>
|
||||||
{
|
{
|
||||||
discover.selectable.types.length === 0 && discover.catalog_resource === null ?
|
discover.selectable.types.length === 0 && discover.catalog_resource === null ?
|
||||||
|
|
@ -132,6 +147,15 @@ const Discover = ({ urlParams, queryParams }) => {
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
addonModalOpen ?
|
||||||
|
<AddonDetailsModal
|
||||||
|
transportUrl={discover.catalog_resource.request.base}
|
||||||
|
onCloseRequest={closeAddonModal}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,10 @@
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 28rem;
|
grid-template-columns: 1fr 28rem;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto auto 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"selectable-inputs-area meta-preview-area"
|
"selectable-inputs-area meta-preview-area"
|
||||||
|
"missing-addon-warning-container meta-preview-area"
|
||||||
"catalog-content-area meta-preview-area";
|
"catalog-content-area meta-preview-area";
|
||||||
|
|
||||||
.selectable-inputs-container {
|
.selectable-inputs-container {
|
||||||
|
|
@ -102,6 +103,38 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.missing-addon-warning-container {
|
||||||
|
grid-area: missing-addon-warning-container;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 1.5rem 1.5rem 1.5rem;
|
||||||
|
|
||||||
|
.warning-info {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--color-surfacelighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 10rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: var(--color-signal5);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: var(--color-surfacelighter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.catalog-content-container {
|
.catalog-content-container {
|
||||||
grid-area: catalog-content-area;
|
grid-area: catalog-content-area;
|
||||||
|
|
||||||
|
|
@ -203,9 +236,10 @@
|
||||||
.discover-container {
|
.discover-container {
|
||||||
.discover-content {
|
.discover-content {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto auto 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"selectable-inputs-area"
|
"selectable-inputs-area"
|
||||||
|
"missing-addon-warning-container"
|
||||||
"catalog-content-area";
|
"catalog-content-area";
|
||||||
|
|
||||||
.catalog-content-container {
|
.catalog-content-container {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ const PropTypes = require('prop-types');
|
||||||
const classnames = require('classnames');
|
const classnames = require('classnames');
|
||||||
const Icon = require('stremio-icons/dom');
|
const Icon = require('stremio-icons/dom');
|
||||||
const { useRouteFocused } = require('stremio-router');
|
const { useRouteFocused } = require('stremio-router');
|
||||||
const { Button } = require('stremio/common');
|
const { Button, useCoreEvent } = require('stremio/common');
|
||||||
const { useServices } = require('stremio/services');
|
const { useServices } = require('stremio/services');
|
||||||
const CredentialsTextInput = require('./CredentialsTextInput');
|
const CredentialsTextInput = require('./CredentialsTextInput');
|
||||||
const ConsentCheckbox = require('./ConsentCheckbox');
|
const ConsentCheckbox = require('./ConsentCheckbox');
|
||||||
|
|
@ -71,29 +71,21 @@ const Intro = ({ queryParams }) => {
|
||||||
error: ''
|
error: ''
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
React.useEffect(() => {
|
useCoreEvent(React.useCallback(({ event, args }) => {
|
||||||
const onEvent = ({ event, args }) => {
|
switch (event) {
|
||||||
switch (event) {
|
case 'UserAuthenticated': {
|
||||||
case 'CtxActionErr': {
|
window.location.replace('#/');
|
||||||
const [, error] = args;
|
break;
|
||||||
dispatch({ type: 'error', error: error.args.message });
|
}
|
||||||
break;
|
case 'Error': {
|
||||||
}
|
if (args.source.event === 'UserAuthenticated') {
|
||||||
case 'CtxChanged': {
|
// TODO use error.code to match translated message;
|
||||||
const state = core.getState();
|
dispatch({ type: 'error', error: args.error.message });
|
||||||
if (state.ctx.content.auth !== null) {
|
}
|
||||||
window.location.replace('#/');
|
break;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
if (routeFocused) {
|
|
||||||
core.on('Event', onEvent);
|
|
||||||
}
|
}
|
||||||
return () => {
|
}, []));
|
||||||
core.off('Event', onEvent);
|
|
||||||
};
|
|
||||||
}, [routeFocused]);
|
|
||||||
const loginWithFacebook = React.useCallback(() => {
|
const loginWithFacebook = React.useCallback(() => {
|
||||||
FB.login((response) => {
|
FB.login((response) => {
|
||||||
if (response.status === 'connected') {
|
if (response.status === 'connected') {
|
||||||
|
|
@ -131,10 +123,11 @@ const Intro = ({ queryParams }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
core.dispatch({
|
core.dispatch({
|
||||||
action: 'UserOp',
|
action: 'Ctx',
|
||||||
args: {
|
args: {
|
||||||
userOp: 'Login',
|
action: 'Authenticate',
|
||||||
args: {
|
args: {
|
||||||
|
type: 'Login',
|
||||||
email: state.email,
|
email: state.email,
|
||||||
password: state.password
|
password: state.password
|
||||||
}
|
}
|
||||||
|
|
@ -147,9 +140,9 @@ const Intro = ({ queryParams }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
core.dispatch({
|
core.dispatch({
|
||||||
action: 'UserOp',
|
action: 'Ctx',
|
||||||
args: {
|
args: {
|
||||||
userOp: 'Logout'
|
action: 'Logout'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
window.location.replace('#/');
|
window.location.replace('#/');
|
||||||
|
|
@ -176,10 +169,11 @@ const Intro = ({ queryParams }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
core.dispatch({
|
core.dispatch({
|
||||||
action: 'UserOp',
|
action: 'Ctx',
|
||||||
args: {
|
args: {
|
||||||
userOp: 'Register',
|
action: 'Authenticate',
|
||||||
args: {
|
args: {
|
||||||
|
type: 'Register',
|
||||||
email: state.email,
|
email: state.email,
|
||||||
password: state.password,
|
password: state.password,
|
||||||
gdpr_consent: {
|
gdpr_consent: {
|
||||||
|
|
@ -236,6 +230,11 @@ const Intro = ({ queryParams }) => {
|
||||||
const toggleMarketingAccepted = React.useCallback(() => {
|
const toggleMarketingAccepted = React.useCallback(() => {
|
||||||
dispatch({ type: 'toggle-checkbox', name: 'marketingAccepted' });
|
dispatch({ type: 'toggle-checkbox', name: 'marketingAccepted' });
|
||||||
}, []);
|
}, []);
|
||||||
|
const switchFormOnClick = React.useCallback(() => {
|
||||||
|
const nextQueryParams = new URLSearchParams(queryParams);
|
||||||
|
nextQueryParams.set('form', state.form === SIGNUP_FORM ? LOGIN_FORM : SIGNUP_FORM);
|
||||||
|
window.location.replace(`#/intro?${nextQueryParams}`);
|
||||||
|
}, [queryParams, state.form]);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if ([LOGIN_FORM, SIGNUP_FORM].includes(queryParams.get('form'))) {
|
if ([LOGIN_FORM, SIGNUP_FORM].includes(queryParams.get('form'))) {
|
||||||
dispatch({ type: 'set-form', form: queryParams.get('form') });
|
dispatch({ type: 'set-form', form: queryParams.get('form') });
|
||||||
|
|
@ -258,7 +257,6 @@ const Intro = ({ queryParams }) => {
|
||||||
<Icon className={styles['icon']} icon={'ic_facebook'} />
|
<Icon className={styles['icon']} icon={'ic_facebook'} />
|
||||||
<div className={styles['label']}>Continue with Facebook</div>
|
<div className={styles['label']}>Continue with Facebook</div>
|
||||||
</Button>
|
</Button>
|
||||||
<div className={styles['facebook-statement']}>We won't post anything on your behalf</div>
|
|
||||||
<CredentialsTextInput
|
<CredentialsTextInput
|
||||||
ref={emailRef}
|
ref={emailRef}
|
||||||
className={styles['credentials-text-input']}
|
className={styles['credentials-text-input']}
|
||||||
|
|
@ -337,7 +335,7 @@ const Intro = ({ queryParams }) => {
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
<Button className={classnames(styles['form-button'], styles['switch-form-button'])} href={state.form === SIGNUP_FORM ? '#/intro?form=login' : '#/intro?form=signup'}>
|
<Button className={classnames(styles['form-button'], styles['switch-form-button'])} onClick={switchFormOnClick}>
|
||||||
<div className={styles['label']}>{state.form === SIGNUP_FORM ? 'LOG IN' : 'SING UP WITH EMAIL'}</div>
|
<div className={styles['label']}>{state.form === SIGNUP_FORM ? 'LOG IN' : 'SING UP WITH EMAIL'}</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@
|
||||||
|
|
||||||
.facebook-button {
|
.facebook-button {
|
||||||
min-height: 4.5rem;
|
min-height: 4.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
background: var(--color-facebook);
|
background: var(--color-facebook);
|
||||||
|
|
||||||
&:hover, &:focus {
|
&:hover, &:focus {
|
||||||
|
|
@ -58,13 +59,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.facebook-statement {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--color-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.credentials-text-input {
|
.credentials-text-input {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const hat = require('hat');
|
const hat = require('hat');
|
||||||
const useVideoImplementation = require('./useVideoImplementation');
|
const selectVideoImplementation = require('./selectVideoImplementation');
|
||||||
|
|
||||||
const Video = React.forwardRef(({ className, ...props }, ref) => {
|
const Video = React.forwardRef(({ className, ...props }, ref) => {
|
||||||
const [onEnded, onError, onPropValue, onPropChanged, onImplementationChanged] = React.useMemo(() => [
|
const [onEnded, onError, onPropValue, onPropChanged, onImplementationChanged] = React.useMemo(() => [
|
||||||
|
|
@ -16,7 +16,7 @@ const Video = React.forwardRef(({ className, ...props }, ref) => {
|
||||||
const id = React.useMemo(() => `video-${hat()}`, []);
|
const id = React.useMemo(() => `video-${hat()}`, []);
|
||||||
const dispatch = React.useCallback((args) => {
|
const dispatch = React.useCallback((args) => {
|
||||||
if (args && args.commandName === 'load' && args.commandArgs) {
|
if (args && args.commandName === 'load' && args.commandArgs) {
|
||||||
const Video = useVideoImplementation(args.commandArgs.shell, args.commandArgs.stream);
|
const Video = selectVideoImplementation(args.commandArgs.shell, args.commandArgs.stream);
|
||||||
if (typeof Video !== 'function') {
|
if (typeof Video !== 'function') {
|
||||||
videoRef.current = null;
|
videoRef.current = null;
|
||||||
} else if (videoRef.current === null || videoRef.current.constructor !== Video) {
|
} else if (videoRef.current === null || videoRef.current.constructor !== Video) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const { HTMLVideo, YouTubeVideo, MPVVideo } = require('stremio-video');
|
const { HTMLVideo, YouTubeVideo, MPVVideo } = require('stremio-video');
|
||||||
|
|
||||||
const useVideoImplementation = (shell, stream) => {
|
const selectVideoImplementation = (shell, stream) => {
|
||||||
if (shell) {
|
if (shell) {
|
||||||
return MPVVideo;
|
return MPVVideo;
|
||||||
}
|
}
|
||||||
|
|
@ -16,4 +16,4 @@ const useVideoImplementation = (shell, stream) => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = useVideoImplementation;
|
module.exports = selectVideoImplementation;
|
||||||
|
|
@ -1,13 +1,51 @@
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { useModelState } = require('stremio/common');
|
const { useModelState } = require('stremio/common');
|
||||||
|
|
||||||
const initPlayer = () => ({
|
const initPlayerState = () => ({
|
||||||
selected: null,
|
selected: {
|
||||||
|
transport_url: null,
|
||||||
|
type_name: null,
|
||||||
|
id: null,
|
||||||
|
video_id: null,
|
||||||
|
stream: null,
|
||||||
|
},
|
||||||
meta_resource: null,
|
meta_resource: null,
|
||||||
subtitles_resources: [],
|
subtitles_resources: [],
|
||||||
next_video: null
|
next_video: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mapPlayerStateWithCtx = (player, ctx) => {
|
||||||
|
const selected = player.selected;
|
||||||
|
const meta_resource = player.meta_resource;
|
||||||
|
const subtitles_resources = player.subtitles_resources.map((subtitles_resource) => {
|
||||||
|
if (subtitles_resource.content.type === 'Ready') {
|
||||||
|
const origin = ctx.content.addons.reduce((origin, addon) => {
|
||||||
|
if (addon.transportUrl === subtitles_resource.request.base) {
|
||||||
|
return typeof addon.manifest.name === 'string' && addon.manifest.name.length > 0 ?
|
||||||
|
addon.manifest.name
|
||||||
|
:
|
||||||
|
addon.manifest.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return origin;
|
||||||
|
}, subtitles_resource.request.base);
|
||||||
|
subtitles_resource.content.content = subtitles_resource.content.content.map((subtitles) => ({
|
||||||
|
...subtitles,
|
||||||
|
origin
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return subtitles_resource;
|
||||||
|
}, []);
|
||||||
|
const next_video = player.next_video;
|
||||||
|
return {
|
||||||
|
selected,
|
||||||
|
meta_resource,
|
||||||
|
subtitles_resources,
|
||||||
|
next_video
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const usePlayer = (urlParams) => {
|
const usePlayer = (urlParams) => {
|
||||||
const loadPlayerAction = React.useMemo(() => {
|
const loadPlayerAction = React.useMemo(() => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -34,7 +72,8 @@ const usePlayer = (urlParams) => {
|
||||||
return useModelState({
|
return useModelState({
|
||||||
model: 'player',
|
model: 'player',
|
||||||
action: loadPlayerAction,
|
action: loadPlayerAction,
|
||||||
init: initPlayer
|
init: initPlayerState,
|
||||||
|
mapWithCtx: mapPlayerStateWithCtx
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
25
src/routes/Player/useSubtitlesSettings.js
Normal file
25
src/routes/Player/useSubtitlesSettings.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
const React = require('react');
|
||||||
|
const { useModelState } = require('stremio/common');
|
||||||
|
const { useServices } = require('stremio/services');
|
||||||
|
|
||||||
|
const mapSubtitlesSettings = (ctx) => ({
|
||||||
|
size: ctx.content.settings.subtitles_size,
|
||||||
|
text_color: ctx.content.settings.subtitles_text_color,
|
||||||
|
background_color: ctx.content.settings.subtitles_background_color,
|
||||||
|
outline_color: ctx.content.settings.subtitles_outline_color,
|
||||||
|
});
|
||||||
|
|
||||||
|
const useSubtitlesSettings = () => {
|
||||||
|
const { core } = useServices();
|
||||||
|
const initSubtitlesSettings = React.useCallback(() => {
|
||||||
|
const ctx = core.getState('ctx');
|
||||||
|
return mapSubtitlesSettings(ctx);
|
||||||
|
}, []);
|
||||||
|
return useModelState({
|
||||||
|
model: 'ctx',
|
||||||
|
map: mapSubtitlesSettings,
|
||||||
|
init: initSubtitlesSettings
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = useSubtitlesSettings;
|
||||||
|
|
@ -2,20 +2,23 @@ const React = require('react');
|
||||||
const classnames = require('classnames');
|
const classnames = require('classnames');
|
||||||
const Route = require('stremio-router/Route');
|
const Route = require('stremio-router/Route');
|
||||||
const { RouteFocusedProvider } = require('stremio-router/RouteFocusedContext');
|
const { RouteFocusedProvider } = require('stremio-router/RouteFocusedContext');
|
||||||
|
const { ToastsContainerProvider } = require('stremio/common/Toasts/ToastsContainerContext');
|
||||||
const appStyles = require('stremio/App/styles');
|
const appStyles = require('stremio/App/styles');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
||||||
const RouterDecorator = ({ children }) => (
|
const RouterDecorator = ({ children }) => (
|
||||||
<div id={'app'}>
|
<div id={'app'}>
|
||||||
<div className={classnames('routes-container', appStyles['router'])}>
|
<ToastsContainerProvider>
|
||||||
<RouteFocusedProvider value={true}>
|
<div className={classnames('routes-container', appStyles['router'])}>
|
||||||
<Route>
|
<RouteFocusedProvider value={true}>
|
||||||
<div className={styles['route-content-container']}>
|
<Route>
|
||||||
{children}
|
<div className={styles['route-content-container']}>
|
||||||
</div>
|
{children}
|
||||||
</Route>
|
</div>
|
||||||
</RouteFocusedProvider>
|
</Route>
|
||||||
</div>
|
</RouteFocusedProvider>
|
||||||
|
</div>
|
||||||
|
</ToastsContainerProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
42
storybook/stories/Toast/SimpleToast/SimpleToast.js
Normal file
42
storybook/stories/Toast/SimpleToast/SimpleToast.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
const React = require('react');
|
||||||
|
const { storiesOf } = require('@storybook/react');
|
||||||
|
const { Toasts } = require('stremio/common');
|
||||||
|
const styles = require('./styles');
|
||||||
|
|
||||||
|
storiesOf('Toast', module).add('SimpleToast', () => {
|
||||||
|
const toastRef = React.useRef(null);
|
||||||
|
|
||||||
|
const showToast = React.useCallback((message) => {
|
||||||
|
toastRef.current.show({
|
||||||
|
title: 'Something to take your attention',
|
||||||
|
timeout: 0,
|
||||||
|
type: 'info',
|
||||||
|
icon: 'ic_sub',
|
||||||
|
closeButton: true,
|
||||||
|
...message
|
||||||
|
});
|
||||||
|
}, [toastRef.current]);
|
||||||
|
|
||||||
|
const clickSuccess = () => showToast({
|
||||||
|
title: 'You clicked it',
|
||||||
|
text: 'Congratulations! Click event handled successfully.',
|
||||||
|
type: 'success',
|
||||||
|
icon: 'ic_check',
|
||||||
|
timeout: 2e3
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles['root-container']}>
|
||||||
|
<button onClick={() => showToast({ text: 'Longer message that contains a lot of words but it does not state anything. The idea is to test the handling of long messages.' })}>Long message</button>
|
||||||
|
<button onClick={() => showToast({ text: 'This will close after 3 seconds', timeout: 3e3 })}>Timeout 3s</button>
|
||||||
|
<button onClick={() => showToast({ text: 'Click me and see what happens later', title: 'Click here!', onClick: clickSuccess })}>Clickable</button>
|
||||||
|
<button onClick={() => showToast({ text: 'Type success', type: 'success', icon: 'ic_check' })}>Success</button>
|
||||||
|
<button onClick={() => showToast({ text: 'Type alert', type: 'alert', icon: 'ic_warning' })}>Alert</button>
|
||||||
|
<button onClick={() => showToast({ text: 'Type error', type: 'error', icon: 'ic_x' })}>Error</button>
|
||||||
|
<button onClick={() => showToast({ text: 'No title', type: 'info', title: null, icon: null })}>No title</button>
|
||||||
|
<button onClick={() => toastRef.current.show({})}>Empty</button>
|
||||||
|
<button onClick={() => toastRef.current.hideAll()}>Close all</button>
|
||||||
|
<Toasts ref={toastRef} className={styles['toasts-container']} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
1
storybook/stories/Toast/SimpleToast/index.js
Normal file
1
storybook/stories/Toast/SimpleToast/index.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
require('./SimpleToast');
|
||||||
16
storybook/stories/Toast/SimpleToast/styles.less
Normal file
16
storybook/stories/Toast/SimpleToast/styles.less
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
.root-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--color-surfacelighter);
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: var(--color-surfacelighter);
|
||||||
|
background-color: var(--color-primarydark);
|
||||||
|
padding: .5rem;
|
||||||
|
margin: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toasts-container {
|
||||||
|
width: 30rem;
|
||||||
|
}
|
||||||
1
storybook/stories/Toast/index.js
Normal file
1
storybook/stories/Toast/index.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
require('./SimpleToast');
|
||||||
|
|
@ -12,3 +12,4 @@ require('./Popup');
|
||||||
require('./SeasonsBar');
|
require('./SeasonsBar');
|
||||||
require('./SharePrompt');
|
require('./SharePrompt');
|
||||||
require('./Stream');
|
require('./Stream');
|
||||||
|
require('./Toast');
|
||||||
|
|
|
||||||
|
|
@ -353,84 +353,159 @@ describe('routesRegexp', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('player route regexp', () => {
|
describe('player route regexp', () => {
|
||||||
it('match /player////', async () => {
|
it('match /player/////', async () => {
|
||||||
expect(Array.from('/player////'.match(routesRegexp.player.regexp)))
|
expect(Array.from('/player/////'.match(routesRegexp.player.regexp)))
|
||||||
.toEqual(['/player////', '', '', '', '']);
|
.toEqual(['/player/////', '', '', '', '', '']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('match /player/1///', async () => {
|
it('match /player/1////', async () => {
|
||||||
expect(Array.from('/player/1///'.match(routesRegexp.player.regexp)))
|
expect(Array.from('/player/1////'.match(routesRegexp.player.regexp)))
|
||||||
.toEqual(['/player/1///', '1', '', '', '']);
|
.toEqual(['/player/1////', '1', '', '', '', '']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('match /player//2//', async () => {
|
it('match /player//2///', async () => {
|
||||||
expect(Array.from('/player//2//'.match(routesRegexp.player.regexp)))
|
expect(Array.from('/player//2///'.match(routesRegexp.player.regexp)))
|
||||||
.toEqual(['/player//2//', '', '2', '', '']);
|
.toEqual(['/player//2///', '', '2', '', '', '']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('match /player///3/', async () => {
|
it('match /player///3//', async () => {
|
||||||
expect(Array.from('/player///3/'.match(routesRegexp.player.regexp)))
|
expect(Array.from('/player///3//'.match(routesRegexp.player.regexp)))
|
||||||
.toEqual(['/player///3/', '', '', '3', '']);
|
.toEqual(['/player///3//', '', '', '3', '', '']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('match /player////4', async () => {
|
it('match /player////4/', async () => {
|
||||||
expect(Array.from('/player////4'.match(routesRegexp.player.regexp)))
|
expect(Array.from('/player////4/'.match(routesRegexp.player.regexp)))
|
||||||
.toEqual(['/player////4', '', '', '', '4']);
|
.toEqual(['/player////4/', '', '', '', '4', '']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('match /player/1/2//', async () => {
|
it('match /player/////5', async () => {
|
||||||
expect(Array.from('/player/1/2//'.match(routesRegexp.player.regexp)))
|
expect(Array.from('/player/////5'.match(routesRegexp.player.regexp)))
|
||||||
.toEqual(['/player/1/2//', '1', '2', '', '']);
|
.toEqual(['/player/////5', '', '', '', '', '5']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('match /player/1//3/', async () => {
|
it('match /player/1/2///', async () => {
|
||||||
expect(Array.from('/player/1//3/'.match(routesRegexp.player.regexp)))
|
expect(Array.from('/player/1/2///'.match(routesRegexp.player.regexp)))
|
||||||
.toEqual(['/player/1//3/', '1', '', '3', '']);
|
.toEqual(['/player/1/2///', '1', '2', '', '', '']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('match /player/1///4', async () => {
|
it('match /player/1//3//', async () => {
|
||||||
expect(Array.from('/player/1///4'.match(routesRegexp.player.regexp)))
|
expect(Array.from('/player/1//3//'.match(routesRegexp.player.regexp)))
|
||||||
.toEqual(['/player/1///4', '1', '', '', '4']);
|
.toEqual(['/player/1//3//', '1', '', '3', '', '']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('match /player//2/3/', async () => {
|
it('match /player/1///4/', async () => {
|
||||||
expect(Array.from('/player//2/3/'.match(routesRegexp.player.regexp)))
|
expect(Array.from('/player/1///4/'.match(routesRegexp.player.regexp)))
|
||||||
.toEqual(['/player//2/3/', '', '2', '3', '']);
|
.toEqual(['/player/1///4/', '1', '', '', '4', '']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('match /player//2//4', async () => {
|
it('match /player/1////5', async () => {
|
||||||
expect(Array.from('/player//2//4'.match(routesRegexp.player.regexp)))
|
expect(Array.from('/player/1////5'.match(routesRegexp.player.regexp)))
|
||||||
.toEqual(['/player//2//4', '', '2', '', '4']);
|
.toEqual(['/player/1////5', '1', '', '', '', '5']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('match /player///3/4', async () => {
|
it('match /player//2/3//', async () => {
|
||||||
expect(Array.from('/player///3/4'.match(routesRegexp.player.regexp)))
|
expect(Array.from('/player//2/3//'.match(routesRegexp.player.regexp)))
|
||||||
.toEqual(['/player///3/4', '', '', '3', '4']);
|
.toEqual(['/player//2/3//', '', '2', '3', '', '']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('match /player/1/2/3/', async () => {
|
it('match /player//2//4/', async () => {
|
||||||
expect(Array.from('/player/1/2/3/'.match(routesRegexp.player.regexp)))
|
expect(Array.from('/player//2//4/'.match(routesRegexp.player.regexp)))
|
||||||
.toEqual(['/player/1/2/3/', '1', '2', '3', '']);
|
.toEqual(['/player//2//4/', '', '2', '', '4', '']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('match /player/1/2//4', async () => {
|
it('match /player//2///5', async () => {
|
||||||
expect(Array.from('/player/1/2//4'.match(routesRegexp.player.regexp)))
|
expect(Array.from('/player//2///5'.match(routesRegexp.player.regexp)))
|
||||||
.toEqual(['/player/1/2//4', '1', '2', '', '4']);
|
.toEqual(['/player//2///5', '', '2', '', '', '5']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('match /player/1//3/4', async () => {
|
it('match /player///3/4/', async () => {
|
||||||
expect(Array.from('/player/1//3/4'.match(routesRegexp.player.regexp)))
|
expect(Array.from('/player///3/4/'.match(routesRegexp.player.regexp)))
|
||||||
.toEqual(['/player/1//3/4', '1', '', '3', '4']);
|
.toEqual(['/player///3/4/', '', '', '3', '4', '']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('match /player//2/3/4', async () => {
|
it('match /player///3//5', async () => {
|
||||||
expect(Array.from('/player//2/3/4'.match(routesRegexp.player.regexp)))
|
expect(Array.from('/player///3//5'.match(routesRegexp.player.regexp)))
|
||||||
.toEqual(['/player//2/3/4', '', '2', '3', '4']);
|
.toEqual(['/player///3//5', '', '', '3', '', '5']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('match /player/1/2/3/4', async () => {
|
it('match /player////4/5', async () => {
|
||||||
expect(Array.from('/player/1/2/3/4'.match(routesRegexp.player.regexp)))
|
expect(Array.from('/player////4/5'.match(routesRegexp.player.regexp)))
|
||||||
.toEqual(['/player/1/2/3/4', '1', '2', '3', '4']);
|
.toEqual(['/player////4/5', '', '', '', '4', '5']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('match /player/1/2/3//', async () => {
|
||||||
|
expect(Array.from('/player/1/2/3//'.match(routesRegexp.player.regexp)))
|
||||||
|
.toEqual(['/player/1/2/3//', '1', '2', '3', '', '']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('match /player/1/2//4/', async () => {
|
||||||
|
expect(Array.from('/player/1/2//4/'.match(routesRegexp.player.regexp)))
|
||||||
|
.toEqual(['/player/1/2//4/', '1', '2', '', '4', '']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('match /player/1/2///5', async () => {
|
||||||
|
expect(Array.from('/player/1/2///5'.match(routesRegexp.player.regexp)))
|
||||||
|
.toEqual(['/player/1/2///5', '1', '2', '', '', '5']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('match /player/1//3/4/', async () => {
|
||||||
|
expect(Array.from('/player/1//3/4/'.match(routesRegexp.player.regexp)))
|
||||||
|
.toEqual(['/player/1//3/4/', '1', '', '3', '4', '']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('match /player/1//3//5', async () => {
|
||||||
|
expect(Array.from('/player/1//3//5'.match(routesRegexp.player.regexp)))
|
||||||
|
.toEqual(['/player/1//3//5', '1', '', '3', '', '5']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('match /player/1///4/5', async () => {
|
||||||
|
expect(Array.from('/player/1///4/5'.match(routesRegexp.player.regexp)))
|
||||||
|
.toEqual(['/player/1///4/5', '1', '', '', '4', '5']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('match /player//2/3/4/', async () => {
|
||||||
|
expect(Array.from('/player//2/3/4/'.match(routesRegexp.player.regexp)))
|
||||||
|
.toEqual(['/player//2/3/4/', '', '2', '3', '4', '']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('match /player//2/3//5', async () => {
|
||||||
|
expect(Array.from('/player//2/3//5'.match(routesRegexp.player.regexp)))
|
||||||
|
.toEqual(['/player//2/3//5', '', '2', '3', '', '5']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('match /player///3/4/5', async () => {
|
||||||
|
expect(Array.from('/player///3/4/5'.match(routesRegexp.player.regexp)))
|
||||||
|
.toEqual(['/player///3/4/5', '', '', '3', '4', '5']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('match /player/1/2/3/4/', async () => {
|
||||||
|
expect(Array.from('/player/1/2/3/4/'.match(routesRegexp.player.regexp)))
|
||||||
|
.toEqual(['/player/1/2/3/4/', '1', '2', '3', '4', '']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('match /player/1/2/3//5', async () => {
|
||||||
|
expect(Array.from('/player/1/2/3//5'.match(routesRegexp.player.regexp)))
|
||||||
|
.toEqual(['/player/1/2/3//5', '1', '2', '3', '', '5']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('match /player/1/2//4/5', async () => {
|
||||||
|
expect(Array.from('/player/1/2//4/5'.match(routesRegexp.player.regexp)))
|
||||||
|
.toEqual(['/player/1/2//4/5', '1', '2', '', '4', '5']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('match /player/1//3/4/5', async () => {
|
||||||
|
expect(Array.from('/player/1//3/4/5'.match(routesRegexp.player.regexp)))
|
||||||
|
.toEqual(['/player/1//3/4/5', '1', '', '3', '4', '5']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('match /player//2/3/4/5', async () => {
|
||||||
|
expect(Array.from('/player//2/3/4/5'.match(routesRegexp.player.regexp)))
|
||||||
|
.toEqual(['/player//2/3/4/5', '', '2', '3', '4', '5']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('match /player/1/2/3/4/5', async () => {
|
||||||
|
expect(Array.from('/player/1/2/3/4/5'.match(routesRegexp.player.regexp)))
|
||||||
|
.toEqual(['/player/1/2/3/4/5', '1', '2', '3', '4', '5']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('not match /player', async () => {
|
it('not match /player', async () => {
|
||||||
|
|
@ -439,8 +514,8 @@ describe('routesRegexp', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('not match /player/', async () => {
|
it('not match /player/', async () => {
|
||||||
expect('/player/'.match(routesRegexp.player.regexp))
|
expect(Array.from('/player/'.match(routesRegexp.player.regexp)))
|
||||||
.toBe(null);
|
.toEqual(['/player/', '', undefined, undefined, undefined, undefined]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('not match /player//', async () => {
|
it('not match /player//', async () => {
|
||||||
|
|
@ -453,14 +528,14 @@ describe('routesRegexp', () => {
|
||||||
.toBe(null);
|
.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('not match /player/////', async () => {
|
it('not match /player//////', async () => {
|
||||||
expect('/player/////'.match(routesRegexp.player.regexp))
|
expect('/player//////'.match(routesRegexp.player.regexp))
|
||||||
.toBe(null);
|
.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('not match /player/1', async () => {
|
it('not match /player/1', async () => {
|
||||||
expect('/player/1'.match(routesRegexp.player.regexp))
|
expect(Array.from('/player/1'.match(routesRegexp.player.regexp)))
|
||||||
.toBe(null);
|
.toEqual(['/player/1', '1', undefined, undefined, undefined, undefined]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('not match /player/1/', async () => {
|
it('not match /player/1/', async () => {
|
||||||
|
|
@ -473,18 +548,38 @@ describe('routesRegexp', () => {
|
||||||
.toBe(null);
|
.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('not match /player/1///', async () => {
|
||||||
|
expect('/player/1///'.match(routesRegexp.player.regexp))
|
||||||
|
.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('not match /player//2', async () => {
|
||||||
|
expect('/player//2'.match(routesRegexp.player.regexp))
|
||||||
|
.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
it('not match /player//2/', async () => {
|
it('not match /player//2/', async () => {
|
||||||
expect('/player//2/'.match(routesRegexp.player.regexp))
|
expect('/player//2/'.match(routesRegexp.player.regexp))
|
||||||
.toBe(null);
|
.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('not match /player//2//', async () => {
|
||||||
|
expect('/player//2//'.match(routesRegexp.player.regexp))
|
||||||
|
.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
it('not match /player///3', async () => {
|
it('not match /player///3', async () => {
|
||||||
expect('/player///3'.match(routesRegexp.player.regexp))
|
expect('/player///3'.match(routesRegexp.player.regexp))
|
||||||
.toBe(null);
|
.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('not match /player////4/', async () => {
|
it('not match /player///3/', async () => {
|
||||||
expect('/player////4/'.match(routesRegexp.player.regexp))
|
expect('/player///3/'.match(routesRegexp.player.regexp))
|
||||||
|
.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('not match /player////4', async () => {
|
||||||
|
expect('/player////4'.match(routesRegexp.player.regexp))
|
||||||
.toBe(null);
|
.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -498,18 +593,48 @@ describe('routesRegexp', () => {
|
||||||
.toBe(null);
|
.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('not match /player/1/2//', async () => {
|
||||||
|
expect('/player/1/2//'.match(routesRegexp.player.regexp))
|
||||||
|
.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
it('not match /player/1//3', async () => {
|
it('not match /player/1//3', async () => {
|
||||||
expect('/player/1//3'.match(routesRegexp.player.regexp))
|
expect('/player/1//3'.match(routesRegexp.player.regexp))
|
||||||
.toBe(null);
|
.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('not match /player/1//3/', async () => {
|
||||||
|
expect('/player/1//3/'.match(routesRegexp.player.regexp))
|
||||||
|
.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('not match /player/1///4', async () => {
|
||||||
|
expect('/player/1///4'.match(routesRegexp.player.regexp))
|
||||||
|
.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
it('not match /player/1/2/3', async () => {
|
it('not match /player/1/2/3', async () => {
|
||||||
expect('/player/1/2/3'.match(routesRegexp.player.regexp))
|
expect('/player/1/2/3'.match(routesRegexp.player.regexp))
|
||||||
.toBe(null);
|
.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('not match /player/1/2/3/4/', async () => {
|
it('not match /player/1/2/3/', async () => {
|
||||||
expect('/player/1/2/3/4/'.match(routesRegexp.player.regexp))
|
expect('/player/1/2/3/'.match(routesRegexp.player.regexp))
|
||||||
|
.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('not match /player/1/2//4', async () => {
|
||||||
|
expect('/player/1/2//4'.match(routesRegexp.player.regexp))
|
||||||
|
.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('not match /player/1/2/3/4', async () => {
|
||||||
|
expect('/player/1/2/3/4'.match(routesRegexp.player.regexp))
|
||||||
|
.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('not match /player/1/2/3/4/5/', async () => {
|
||||||
|
expect('/player/1/2/3/4/5/'.match(routesRegexp.player.regexp))
|
||||||
.toBe(null);
|
.toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,11 @@ const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = (env, argv) => ({
|
||||||
entry: './src/index.js',
|
entry: './src/index.js',
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, 'build')
|
||||||
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
|
|
@ -125,6 +128,10 @@ module.exports = {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
new webpack.EnvironmentPlugin({
|
||||||
|
DEBUG: argv.mode !== 'production',
|
||||||
|
...env
|
||||||
|
}),
|
||||||
new webpack.ProgressPlugin(),
|
new webpack.ProgressPlugin(),
|
||||||
new CopyWebpackPlugin([
|
new CopyWebpackPlugin([
|
||||||
{ from: 'node_modules/stremio-core-web/static', to: '' },
|
{ from: 'node_modules/stremio-core-web/static', to: '' },
|
||||||
|
|
@ -142,4 +149,4 @@ module.exports = {
|
||||||
cleanAfterEveryBuildPatterns: ['./main.js', './main.css']
|
cleanAfterEveryBuildPatterns: ['./main.js', './main.css']
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
};
|
});
|
||||||
|
|
|
||||||
62
yarn.lock
62
yarn.lock
|
|
@ -1240,6 +1240,58 @@
|
||||||
react-lifecycles-compat "^3.0.4"
|
react-lifecycles-compat "^3.0.4"
|
||||||
warning "^3.0.0"
|
warning "^3.0.0"
|
||||||
|
|
||||||
|
"@sentry/browser@5.11.1":
|
||||||
|
version "5.11.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.11.1.tgz#337ffcb52711b23064c847a07629e966f54a5ebb"
|
||||||
|
integrity sha512-oqOX/otmuP92DEGRyZeBuQokXdeT9HQRxH73oqIURXXNLMP3PWJALSb4HtT4AftEt/2ROGobZLuA4TaID6My/Q==
|
||||||
|
dependencies:
|
||||||
|
"@sentry/core" "5.11.1"
|
||||||
|
"@sentry/types" "5.11.0"
|
||||||
|
"@sentry/utils" "5.11.1"
|
||||||
|
tslib "^1.9.3"
|
||||||
|
|
||||||
|
"@sentry/core@5.11.1":
|
||||||
|
version "5.11.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.11.1.tgz#9e2da485e196ae32971545c1c49ee6fe719930e2"
|
||||||
|
integrity sha512-BpvPosVNT20Xso4gAV54Lu3KqDmD20vO63HYwbNdST5LUi8oYV4JhvOkoBraPEM2cbBwQvwVcFdeEYKk4tin9A==
|
||||||
|
dependencies:
|
||||||
|
"@sentry/hub" "5.11.1"
|
||||||
|
"@sentry/minimal" "5.11.1"
|
||||||
|
"@sentry/types" "5.11.0"
|
||||||
|
"@sentry/utils" "5.11.1"
|
||||||
|
tslib "^1.9.3"
|
||||||
|
|
||||||
|
"@sentry/hub@5.11.1":
|
||||||
|
version "5.11.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.11.1.tgz#ddcb865563fae53852d405885c46b4c6de68a91b"
|
||||||
|
integrity sha512-ucKprYCbGGLLjVz4hWUqHN9KH0WKUkGf5ZYfD8LUhksuobRkYVyig0ZGbshECZxW5jcDTzip4Q9Qimq/PkkXBg==
|
||||||
|
dependencies:
|
||||||
|
"@sentry/types" "5.11.0"
|
||||||
|
"@sentry/utils" "5.11.1"
|
||||||
|
tslib "^1.9.3"
|
||||||
|
|
||||||
|
"@sentry/minimal@5.11.1":
|
||||||
|
version "5.11.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.11.1.tgz#0e705d01a567282d8fbbda2aed848b4974cc3cec"
|
||||||
|
integrity sha512-HK8zs7Pgdq7DsbZQTThrhQPrJsVWzz7MaluAbQA0rTIAJ3TvHKQpsVRu17xDpjZXypqWcKCRsthDrC4LxDM1Bg==
|
||||||
|
dependencies:
|
||||||
|
"@sentry/hub" "5.11.1"
|
||||||
|
"@sentry/types" "5.11.0"
|
||||||
|
tslib "^1.9.3"
|
||||||
|
|
||||||
|
"@sentry/types@5.11.0":
|
||||||
|
version "5.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.11.0.tgz#40f0f3174362928e033ddd9725d55e7c5cb7c5b6"
|
||||||
|
integrity sha512-1Uhycpmeo1ZK2GLvrtwZhTwIodJHcyIS6bn+t4IMkN9MFoo6ktbAfhvexBDW/IDtdLlCGJbfm8nIZerxy0QUpg==
|
||||||
|
|
||||||
|
"@sentry/utils@5.11.1":
|
||||||
|
version "5.11.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.11.1.tgz#aa19fcc234cf632257b2281261651d2fac967607"
|
||||||
|
integrity sha512-O0Zl4R2JJh8cTkQ8ZL2cDqGCmQdpA5VeXpuBbEl1v78LQPkBDISi35wH4mKmLwMsLBtTVpx2UeUHBj0KO5aLlA==
|
||||||
|
dependencies:
|
||||||
|
"@sentry/types" "5.11.0"
|
||||||
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sheerun/mutationobserver-shim@^0.3.2":
|
"@sheerun/mutationobserver-shim@^0.3.2":
|
||||||
version "0.3.2"
|
version "0.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b"
|
resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b"
|
||||||
|
|
@ -4420,16 +4472,16 @@ dotenv-webpack@^1.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
dotenv-defaults "^1.0.2"
|
dotenv-defaults "^1.0.2"
|
||||||
|
|
||||||
|
dotenv@8.2.0, dotenv@^8.0.0:
|
||||||
|
version "8.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
|
||||||
|
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
|
||||||
|
|
||||||
dotenv@^6.2.0:
|
dotenv@^6.2.0:
|
||||||
version "6.2.0"
|
version "6.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064"
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064"
|
||||||
integrity sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==
|
integrity sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==
|
||||||
|
|
||||||
dotenv@^8.0.0:
|
|
||||||
version "8.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
|
|
||||||
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
|
|
||||||
|
|
||||||
duplexer@^0.1.1:
|
duplexer@^0.1.1:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
|
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue