mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-01-11 22:40:31 +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"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"commonjs": true,
|
||||
"browser": 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
|
||||
/dist
|
||||
/build
|
||||
/package-lock.json
|
||||
/npm-debug.log
|
||||
.DS_Store
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"lint": "eslint src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/browser": "5.11.1",
|
||||
"a-color-picker": "1.2.1",
|
||||
"classnames": "2.2.6",
|
||||
"events": "1.1.1",
|
||||
|
|
@ -50,6 +51,7 @@
|
|||
"css-loader": "3.4.0",
|
||||
"cssnano": "4.1.10",
|
||||
"cssnano-preset-advanced": "4.0.7",
|
||||
"dotenv": "8.2.0",
|
||||
"eslint": "6.7.2",
|
||||
"eslint-plugin-react": "7.17.0",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ require('spatial-navigation-polyfill');
|
|||
const React = require('react');
|
||||
const { Router } = require('stremio-router');
|
||||
const { Core, KeyboardNavigation, ServicesProvider, Shell } = require('stremio/services');
|
||||
const { ToastProvider } = require('stremio/common');
|
||||
const CoreEventsToaster = require('./CoreEventsToaster');
|
||||
const routerViewsConfig = require('./routerViewsConfig');
|
||||
const styles = require('./styles');
|
||||
|
||||
|
|
@ -21,11 +23,16 @@ const App = () => {
|
|||
setShellInitialized(services.shell.active || services.shell.error instanceof Error);
|
||||
};
|
||||
const onCoreStateChanged = () => {
|
||||
setCoreInitialized(services.core.active || services.core.error instanceof Error);
|
||||
if (services.core.active) {
|
||||
services.core.dispatch({ action: 'LoadCtx' });
|
||||
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.core.on('stateChanged', onCoreStateChanged);
|
||||
|
|
@ -45,12 +52,15 @@ const App = () => {
|
|||
<ServicesProvider services={services}>
|
||||
{
|
||||
shellInitialized && coreInitialized ?
|
||||
<Router
|
||||
className={styles['router']}
|
||||
homePath={'/'}
|
||||
viewsConfig={routerViewsConfig}
|
||||
onPathNotMatch={onPathNotMatch}
|
||||
/>
|
||||
<ToastProvider className={styles['toasts-container']}>
|
||||
<CoreEventsToaster />
|
||||
<Router
|
||||
className={styles['router']}
|
||||
homePath={'/'}
|
||||
viewsConfig={routerViewsConfig}
|
||||
onPathNotMatch={onPathNotMatch}
|
||||
/>
|
||||
</ToastProvider>
|
||||
:
|
||||
<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;
|
||||
--poster-shape-ratio: 1.464;
|
||||
--scroll-bar-width: 6px;
|
||||
--nav-bar-size: 3.2rem;
|
||||
--focus-outline-size: 2px;
|
||||
--color-facebook: #4267b2;
|
||||
--color-twitter: #1DA1F2;
|
||||
|
|
@ -71,6 +72,23 @@ html {
|
|||
width: 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 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ const ColorInput = ({ className, value, dataset, onChange, ...props }) => {
|
|||
ColorInput.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
dataset: PropTypes.objectOf(PropTypes.string),
|
||||
dataset: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
onClick: PropTypes.func
|
||||
};
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ MetaItem.propTypes = {
|
|||
playIcon: PropTypes.bool,
|
||||
progress: PropTypes.number,
|
||||
options: PropTypes.array,
|
||||
dataset: PropTypes.objectOf(PropTypes.string),
|
||||
dataset: PropTypes.object,
|
||||
optionOnSelect: PropTypes.func,
|
||||
onClick: PropTypes.func
|
||||
};
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ ModalDialog.propTypes = {
|
|||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node
|
||||
]),
|
||||
dataset: PropTypes.objectOf(PropTypes.string),
|
||||
dataset: PropTypes.object,
|
||||
onCloseRequest: PropTypes.func
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ Multiselect.propTypes = {
|
|||
})),
|
||||
selected: PropTypes.arrayOf(PropTypes.string),
|
||||
disabled: PropTypes.bool,
|
||||
dataset: PropTypes.objectOf(PropTypes.string),
|
||||
dataset: PropTypes.object,
|
||||
renderLabelContent: PropTypes.func,
|
||||
renderLabelText: PropTypes.func,
|
||||
onOpen: PropTypes.func,
|
||||
|
|
|
|||
|
|
@ -2,17 +2,19 @@ const React = require('react');
|
|||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const Icon = require('stremio-icons/dom');
|
||||
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 useUser = require('stremio/common/useUser');
|
||||
const useProfile = require('./useProfile');
|
||||
const styles = require('./styles');
|
||||
|
||||
const NavMenu = ({ className }) => {
|
||||
const { core } = useServices();
|
||||
const profile = useProfile();
|
||||
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
|
||||
const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen();
|
||||
const [user, logout] = useUser();
|
||||
const popupLabelOnClick = React.useCallback((event) => {
|
||||
if (!event.nativeEvent.togglePopupPrevented) {
|
||||
toggleMenu();
|
||||
|
|
@ -22,7 +24,12 @@ const NavMenu = ({ className }) => {
|
|||
event.nativeEvent.togglePopupPrevented = true;
|
||||
}, []);
|
||||
const logoutButtonOnClick = React.useCallback(() => {
|
||||
logout();
|
||||
core.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'Logout'
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<Popup
|
||||
|
|
@ -40,17 +47,17 @@ const NavMenu = ({ className }) => {
|
|||
<div
|
||||
className={styles['avatar-container']}
|
||||
style={{
|
||||
backgroundImage: user === null ?
|
||||
backgroundImage: profile.auth === null ?
|
||||
'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-label']}>{user === null ? 'Anonymous user' : user.email}</div>
|
||||
<div className={styles['email-label']}>{profile.auth === null ? 'Anonymous user' : profile.auth.user.email}</div>
|
||||
</div>
|
||||
<Button className={styles['logout-button-container']} title={user === 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>
|
||||
<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']}>
|
||||
|
|
|
|||
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-size: 3.2rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const PaginationInput = ({ className, label, dataset, onSelect, ...props }) => {
|
|||
PaginationInput.propTypes = {
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
dataset: PropTypes.objectOf(PropTypes.string),
|
||||
dataset: PropTypes.object,
|
||||
onSelect: PropTypes.func
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ Popup.propTypes = {
|
|||
direction: PropTypes.oneOf(['top-left', 'bottom-left', 'top-right', 'bottom-right']),
|
||||
renderLabel: PropTypes.func.isRequired,
|
||||
renderMenu: PropTypes.func.isRequired,
|
||||
dataset: PropTypes.objectOf(PropTypes.string),
|
||||
dataset: PropTypes.object,
|
||||
onCloseRequest: PropTypes.func
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const { useFocusedRoute } = require('stremio-router');
|
||||
const { useRouteFocused } = require('stremio-router');
|
||||
const useAnimationFrame = require('stremio/common/useAnimationFrame');
|
||||
const useLiveRef = require('stremio/common/useLiveRef');
|
||||
const styles = require('./styles');
|
||||
|
|
@ -13,7 +13,7 @@ const Slider = ({ className, value, minimumValue, maximumValue, onSlide, onCompl
|
|||
const onSlideRef = useLiveRef(onSlide, [onSlide]);
|
||||
const onCompleteRef = useLiveRef(onComplete, [onComplete]);
|
||||
const sliderContainerRef = React.useRef(null);
|
||||
const routeFocused = useFocusedRoute();
|
||||
const routeFocused = useRouteFocused();
|
||||
const [requestThumbAnimation, cancelThumbAnimation] = useAnimationFrame();
|
||||
const calculateValueForMouseX = React.useCallback((mouseX) => {
|
||||
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 Slider = require('./Slider');
|
||||
const TextInput = require('./TextInput');
|
||||
const { ToastProvider, useToast } = require('./Toast');
|
||||
const routesRegexp = require('./routesRegexp');
|
||||
const useAnimationFrame = require('./useAnimationFrame');
|
||||
const useBinaryState = require('./useBinaryState');
|
||||
const useCoreEvent = require('./useCoreEvent');
|
||||
const useDeepEqualEffect = require('./useDeepEqualEffect');
|
||||
const useDeepEqualState = require('./useDeepEqualState');
|
||||
const useFullscreen = require('./useFullscreen');
|
||||
const useInLibrary = require('./useInLibrary');
|
||||
const useLiveRef = require('./useLiveRef');
|
||||
const useModelState = require('./useModelState');
|
||||
const useUser = require('./useUser');
|
||||
|
||||
module.exports = {
|
||||
AddonDetailsModal,
|
||||
|
|
@ -47,13 +49,16 @@ module.exports = {
|
|||
SharePrompt,
|
||||
Slider,
|
||||
TextInput,
|
||||
ToastProvider,
|
||||
useToast,
|
||||
routesRegexp,
|
||||
useAnimationFrame,
|
||||
useBinaryState,
|
||||
useCoreEvent,
|
||||
useDeepEqualEffect,
|
||||
useDeepEqualState,
|
||||
useFullscreen,
|
||||
useInLibrary,
|
||||
useLiveRef,
|
||||
useModelState,
|
||||
useUser
|
||||
useModelState
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ const routesRegexp = {
|
|||
urlParamsNames: []
|
||||
},
|
||||
player: {
|
||||
regexp: /^\/player\/([^/]*)\/([^/]*)\/([^/]*)\/([^/]*)\/([^/]*)$/,
|
||||
urlParamsNames: ['transportUrl', 'type', 'id', 'videoId', 'stream']
|
||||
regexp: /^\/player\/([^/]*)(?:\/([^/]*)\/([^/]*)\/([^/]*)\/([^/]*))?$/,
|
||||
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',
|
||||
};
|
||||
|
||||
const useModelState = ({ model, action, timeout, onNewState, map, mapWithCtx, init }) => {
|
||||
const useModelState = ({ model, init, action, timeout, onNewState, map, mapWithCtx }) => {
|
||||
const modelRef = React.useRef(model);
|
||||
const mountedRef = React.useRef(false);
|
||||
const { core } = useServices();
|
||||
|
|
@ -52,7 +52,7 @@ const useModelState = ({ model, action, timeout, onNewState, map, mapWithCtx, in
|
|||
onNewStateThrottled.cancel();
|
||||
core.off('NewState', onNewStateThrottled);
|
||||
};
|
||||
}, [routeFocused]);
|
||||
}, [routeFocused, timeout, onNewState, map, mapWithCtx]);
|
||||
React.useLayoutEffect(() => {
|
||||
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 ReactDOM = require('react-dom');
|
||||
const Sentry = require('@sentry/browser');
|
||||
const App = require('./App');
|
||||
|
||||
if (typeof process.env.SENTRY_DSN === 'string') {
|
||||
Sentry.init({ dsn: process.env.SENTRY_DSN });
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('app'));
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ const classnames = require('classnames');
|
|||
const FocusLock = require('react-focus-lock').default;
|
||||
const { useModalsContainer } = require('../ModalsContainerContext');
|
||||
|
||||
const Modal = ({ className, autoFocus, children, ...props }) => {
|
||||
const Modal = ({ className, autoFocus, disabled, children, ...props }) => {
|
||||
const modalsContainer = useModalsContainer();
|
||||
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}
|
||||
</FocusLock>,
|
||||
modalsContainer
|
||||
|
|
@ -18,6 +18,7 @@ const Modal = ({ className, autoFocus, children, ...props }) => {
|
|||
Modal.propTypes = {
|
||||
className: PropTypes.string,
|
||||
autoFocus: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ Addon.propTypes = {
|
|||
installed: PropTypes.bool,
|
||||
onToggle: PropTypes.func,
|
||||
onShare: PropTypes.func,
|
||||
dataset: PropTypes.objectOf(PropTypes.string)
|
||||
dataset: PropTypes.object
|
||||
};
|
||||
|
||||
module.exports = Addon;
|
||||
|
|
|
|||
|
|
@ -21,16 +21,16 @@ const mapAddonsStateWithCtx = (addons, ctx) => {
|
|||
...addons.catalog_resource,
|
||||
content: {
|
||||
...addons.catalog_resource.content,
|
||||
content: addons.catalog_resource.content.content.map((descriptor) => ({
|
||||
transportUrl: descriptor.transportUrl,
|
||||
installed: ctx.content.addons.some((addon) => addon.transportUrl === descriptor.transportUrl),
|
||||
content: addons.catalog_resource.content.content.map((addon) => ({
|
||||
transportUrl: addon.transportUrl,
|
||||
installed: ctx.profile.addons.some(({ transportUrl }) => transportUrl === addon.transportUrl),
|
||||
manifest: {
|
||||
id: descriptor.manifest.id,
|
||||
name: descriptor.manifest.name,
|
||||
version: descriptor.manifest.version,
|
||||
logo: descriptor.manifest.logo,
|
||||
description: descriptor.manifest.description,
|
||||
types: descriptor.manifest.types
|
||||
id: addon.manifest.id,
|
||||
name: addon.manifest.name,
|
||||
version: addon.manifest.version,
|
||||
logo: addon.manifest.logo,
|
||||
description: addon.manifest.description,
|
||||
types: addon.manifest.types
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
|
@ -45,8 +45,10 @@ const onNewAddonsState = (addons) => {
|
|||
return {
|
||||
action: 'Load',
|
||||
args: {
|
||||
load: 'CatalogFiltered',
|
||||
args: addons.selectable.catalogs[0].load_request
|
||||
model: 'CatalogFiltered',
|
||||
args: {
|
||||
request: addons.selectable.catalogs[0].request
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -59,14 +61,16 @@ const useAddons = (urlParams) => {
|
|||
return {
|
||||
action: 'Load',
|
||||
args: {
|
||||
load: 'CatalogFiltered',
|
||||
model: 'CatalogFiltered',
|
||||
args: {
|
||||
base: urlParams.transportUrl,
|
||||
path: {
|
||||
resource: 'addon_catalog',
|
||||
type_name: urlParams.type,
|
||||
id: urlParams.catalogId,
|
||||
extra: []
|
||||
request: {
|
||||
base: urlParams.transportUrl,
|
||||
path: {
|
||||
resource: 'addon_catalog',
|
||||
type_name: urlParams.type,
|
||||
id: urlParams.catalogId,
|
||||
extra: []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -77,8 +81,10 @@ const useAddons = (urlParams) => {
|
|||
return {
|
||||
action: 'Load',
|
||||
args: {
|
||||
load: 'CatalogFiltered',
|
||||
args: addons.selectable.catalogs[0].load_request
|
||||
model: 'CatalogFiltered',
|
||||
args: {
|
||||
request: addons.selectable.catalogs[0].request
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
const React = require('react');
|
||||
|
||||
const navigateWithLoadRequest = (load_request) => {
|
||||
const transportUrl = encodeURIComponent(load_request.base);
|
||||
const catalogId = encodeURIComponent(load_request.path.id);
|
||||
const type = encodeURIComponent(load_request.path.type_name);
|
||||
const navigateWithRequest = (request) => {
|
||||
const transportUrl = encodeURIComponent(request.base);
|
||||
const catalogId = encodeURIComponent(request.path.id);
|
||||
const type = encodeURIComponent(request.path.type_name);
|
||||
window.location.replace(`#/addons/${transportUrl}/${catalogId}/${type}`);
|
||||
};
|
||||
|
||||
|
|
@ -18,35 +18,35 @@ const mapSelectableInputs = (addons) => {
|
|||
const catalogSelect = {
|
||||
title: 'Select catalog',
|
||||
options: addons.selectable.catalogs
|
||||
.map(({ name, load_request }) => ({
|
||||
value: JSON.stringify(load_request),
|
||||
.map(({ name, request }) => ({
|
||||
value: JSON.stringify(request),
|
||||
label: name
|
||||
})),
|
||||
selected: addons.selectable.catalogs
|
||||
.filter(({ load_request: { path: { id } } }) => {
|
||||
.filter(({ request: { path: { id } } }) => {
|
||||
return addons.catalog_resource !== null &&
|
||||
addons.catalog_resource.request.path.id === id;
|
||||
})
|
||||
.map(({ load_request }) => JSON.stringify(load_request)),
|
||||
.map(({ request }) => JSON.stringify(request)),
|
||||
onSelect: (event) => {
|
||||
navigateWithLoadRequest(JSON.parse(event.value));
|
||||
navigateWithRequest(JSON.parse(event.value));
|
||||
}
|
||||
};
|
||||
const typeSelect = {
|
||||
title: 'Select type',
|
||||
options: addons.selectable.types
|
||||
.map(({ name, load_request }) => ({
|
||||
value: JSON.stringify(load_request),
|
||||
.map(({ name, request }) => ({
|
||||
value: JSON.stringify(request),
|
||||
label: name
|
||||
})),
|
||||
selected: addons.selectable.types
|
||||
.filter(({ load_request }) => {
|
||||
.filter(({ request }) => {
|
||||
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) => {
|
||||
navigateWithLoadRequest(JSON.parse(event.value));
|
||||
navigateWithRequest(JSON.parse(event.value));
|
||||
}
|
||||
};
|
||||
return [catalogSelect, typeSelect];
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ const React = require('react');
|
|||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
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 useSelectableInputs = require('./useSelectableInputs');
|
||||
const styles = require('./styles');
|
||||
|
|
@ -19,9 +20,12 @@ const getMetaItemAtIndex = (catalog_resource, index) => {
|
|||
};
|
||||
|
||||
const Discover = ({ urlParams, queryParams }) => {
|
||||
const { core } = useServices();
|
||||
const state = core.getState();
|
||||
const discover = useDiscover(urlParams, queryParams);
|
||||
const [selectInputs, paginationInput] = useSelectableInputs(discover);
|
||||
const [inputsModalOpen, openInputsModal, closeInputsModal] = useBinaryState(false);
|
||||
const [addonModalOpen, openAddonModal, closeAddonModal] = useBinaryState(false);
|
||||
const [selectedMetaItem, setSelectedMetaItem] = React.useState(() => {
|
||||
return getMetaItemAtIndex(discover.catalog_resource, 0);
|
||||
});
|
||||
|
|
@ -69,6 +73,17 @@ const Discover = ({ urlParams, queryParams }) => {
|
|||
null
|
||||
}
|
||||
</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']}>
|
||||
{
|
||||
discover.selectable.types.length === 0 && discover.catalog_resource === null ?
|
||||
|
|
@ -132,6 +147,15 @@ const Discover = ({ urlParams, queryParams }) => {
|
|||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
addonModalOpen ?
|
||||
<AddonDetailsModal
|
||||
transportUrl={discover.catalog_resource.request.base}
|
||||
onCloseRequest={closeAddonModal}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,9 +27,10 @@
|
|||
align-self: stretch;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 28rem;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
grid-template-areas:
|
||||
"selectable-inputs-area meta-preview-area"
|
||||
"missing-addon-warning-container meta-preview-area"
|
||||
"catalog-content-area meta-preview-area";
|
||||
|
||||
.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 {
|
||||
grid-area: catalog-content-area;
|
||||
|
||||
|
|
@ -203,9 +236,10 @@
|
|||
.discover-container {
|
||||
.discover-content {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
grid-template-areas:
|
||||
"selectable-inputs-area"
|
||||
"missing-addon-warning-container"
|
||||
"catalog-content-area";
|
||||
|
||||
.catalog-content-container {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const PropTypes = require('prop-types');
|
|||
const classnames = require('classnames');
|
||||
const Icon = require('stremio-icons/dom');
|
||||
const { useRouteFocused } = require('stremio-router');
|
||||
const { Button } = require('stremio/common');
|
||||
const { Button, useCoreEvent } = require('stremio/common');
|
||||
const { useServices } = require('stremio/services');
|
||||
const CredentialsTextInput = require('./CredentialsTextInput');
|
||||
const ConsentCheckbox = require('./ConsentCheckbox');
|
||||
|
|
@ -71,29 +71,21 @@ const Intro = ({ queryParams }) => {
|
|||
error: ''
|
||||
}
|
||||
);
|
||||
React.useEffect(() => {
|
||||
const onEvent = ({ event, args }) => {
|
||||
switch (event) {
|
||||
case 'CtxActionErr': {
|
||||
const [, error] = args;
|
||||
dispatch({ type: 'error', error: error.args.message });
|
||||
break;
|
||||
}
|
||||
case 'CtxChanged': {
|
||||
const state = core.getState();
|
||||
if (state.ctx.content.auth !== null) {
|
||||
window.location.replace('#/');
|
||||
}
|
||||
}
|
||||
useCoreEvent(React.useCallback(({ event, args }) => {
|
||||
switch (event) {
|
||||
case 'UserAuthenticated': {
|
||||
window.location.replace('#/');
|
||||
break;
|
||||
}
|
||||
case 'Error': {
|
||||
if (args.source.event === 'UserAuthenticated') {
|
||||
// TODO use error.code to match translated message;
|
||||
dispatch({ type: 'error', error: args.error.message });
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
if (routeFocused) {
|
||||
core.on('Event', onEvent);
|
||||
}
|
||||
return () => {
|
||||
core.off('Event', onEvent);
|
||||
};
|
||||
}, [routeFocused]);
|
||||
}, []));
|
||||
const loginWithFacebook = React.useCallback(() => {
|
||||
FB.login((response) => {
|
||||
if (response.status === 'connected') {
|
||||
|
|
@ -131,10 +123,11 @@ const Intro = ({ queryParams }) => {
|
|||
return;
|
||||
}
|
||||
core.dispatch({
|
||||
action: 'UserOp',
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
userOp: 'Login',
|
||||
action: 'Authenticate',
|
||||
args: {
|
||||
type: 'Login',
|
||||
email: state.email,
|
||||
password: state.password
|
||||
}
|
||||
|
|
@ -147,9 +140,9 @@ const Intro = ({ queryParams }) => {
|
|||
return;
|
||||
}
|
||||
core.dispatch({
|
||||
action: 'UserOp',
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
userOp: 'Logout'
|
||||
action: 'Logout'
|
||||
}
|
||||
});
|
||||
window.location.replace('#/');
|
||||
|
|
@ -176,10 +169,11 @@ const Intro = ({ queryParams }) => {
|
|||
return;
|
||||
}
|
||||
core.dispatch({
|
||||
action: 'UserOp',
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
userOp: 'Register',
|
||||
action: 'Authenticate',
|
||||
args: {
|
||||
type: 'Register',
|
||||
email: state.email,
|
||||
password: state.password,
|
||||
gdpr_consent: {
|
||||
|
|
@ -236,6 +230,11 @@ const Intro = ({ queryParams }) => {
|
|||
const toggleMarketingAccepted = React.useCallback(() => {
|
||||
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(() => {
|
||||
if ([LOGIN_FORM, SIGNUP_FORM].includes(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'} />
|
||||
<div className={styles['label']}>Continue with Facebook</div>
|
||||
</Button>
|
||||
<div className={styles['facebook-statement']}>We won't post anything on your behalf</div>
|
||||
<CredentialsTextInput
|
||||
ref={emailRef}
|
||||
className={styles['credentials-text-input']}
|
||||
|
|
@ -337,7 +335,7 @@ const Intro = ({ queryParams }) => {
|
|||
:
|
||||
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>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
|
||||
.facebook-button {
|
||||
min-height: 4.5rem;
|
||||
margin-bottom: 2rem;
|
||||
background: var(--color-facebook);
|
||||
|
||||
&: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 {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const hat = require('hat');
|
||||
const useVideoImplementation = require('./useVideoImplementation');
|
||||
const selectVideoImplementation = require('./selectVideoImplementation');
|
||||
|
||||
const Video = React.forwardRef(({ className, ...props }, ref) => {
|
||||
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 dispatch = React.useCallback((args) => {
|
||||
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') {
|
||||
videoRef.current = null;
|
||||
} else if (videoRef.current === null || videoRef.current.constructor !== Video) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const { HTMLVideo, YouTubeVideo, MPVVideo } = require('stremio-video');
|
||||
|
||||
const useVideoImplementation = (shell, stream) => {
|
||||
const selectVideoImplementation = (shell, stream) => {
|
||||
if (shell) {
|
||||
return MPVVideo;
|
||||
}
|
||||
|
|
@ -16,4 +16,4 @@ const useVideoImplementation = (shell, stream) => {
|
|||
return null;
|
||||
};
|
||||
|
||||
module.exports = useVideoImplementation;
|
||||
module.exports = selectVideoImplementation;
|
||||
|
|
@ -1,13 +1,51 @@
|
|||
const React = require('react');
|
||||
const { useModelState } = require('stremio/common');
|
||||
|
||||
const initPlayer = () => ({
|
||||
selected: null,
|
||||
const initPlayerState = () => ({
|
||||
selected: {
|
||||
transport_url: null,
|
||||
type_name: null,
|
||||
id: null,
|
||||
video_id: null,
|
||||
stream: null,
|
||||
},
|
||||
meta_resource: null,
|
||||
subtitles_resources: [],
|
||||
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 loadPlayerAction = React.useMemo(() => {
|
||||
try {
|
||||
|
|
@ -34,7 +72,8 @@ const usePlayer = (urlParams) => {
|
|||
return useModelState({
|
||||
model: 'player',
|
||||
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 Route = require('stremio-router/Route');
|
||||
const { RouteFocusedProvider } = require('stremio-router/RouteFocusedContext');
|
||||
const { ToastsContainerProvider } = require('stremio/common/Toasts/ToastsContainerContext');
|
||||
const appStyles = require('stremio/App/styles');
|
||||
const styles = require('./styles');
|
||||
|
||||
const RouterDecorator = ({ children }) => (
|
||||
<div id={'app'}>
|
||||
<div className={classnames('routes-container', appStyles['router'])}>
|
||||
<RouteFocusedProvider value={true}>
|
||||
<Route>
|
||||
<div className={styles['route-content-container']}>
|
||||
{children}
|
||||
</div>
|
||||
</Route>
|
||||
</RouteFocusedProvider>
|
||||
</div>
|
||||
<ToastsContainerProvider>
|
||||
<div className={classnames('routes-container', appStyles['router'])}>
|
||||
<RouteFocusedProvider value={true}>
|
||||
<Route>
|
||||
<div className={styles['route-content-container']}>
|
||||
{children}
|
||||
</div>
|
||||
</Route>
|
||||
</RouteFocusedProvider>
|
||||
</div>
|
||||
</ToastsContainerProvider>
|
||||
</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('./SharePrompt');
|
||||
require('./Stream');
|
||||
require('./Toast');
|
||||
|
|
|
|||
|
|
@ -353,84 +353,159 @@ describe('routesRegexp', () => {
|
|||
});
|
||||
|
||||
describe('player route regexp', () => {
|
||||
it('match /player////', async () => {
|
||||
expect(Array.from('/player////'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player////', '', '', '', '']);
|
||||
it('match /player/////', async () => {
|
||||
expect(Array.from('/player/////'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player/////', '', '', '', '', '']);
|
||||
});
|
||||
|
||||
it('match /player/1///', async () => {
|
||||
expect(Array.from('/player/1///'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player/1///', '1', '', '', '']);
|
||||
it('match /player/1////', async () => {
|
||||
expect(Array.from('/player/1////'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player/1////', '1', '', '', '', '']);
|
||||
});
|
||||
|
||||
it('match /player//2//', async () => {
|
||||
expect(Array.from('/player//2//'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player//2//', '', '2', '', '']);
|
||||
it('match /player//2///', async () => {
|
||||
expect(Array.from('/player//2///'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player//2///', '', '2', '', '', '']);
|
||||
});
|
||||
|
||||
it('match /player///3/', async () => {
|
||||
expect(Array.from('/player///3/'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player///3/', '', '', '3', '']);
|
||||
it('match /player///3//', async () => {
|
||||
expect(Array.from('/player///3//'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player///3//', '', '', '3', '', '']);
|
||||
});
|
||||
|
||||
it('match /player////4', async () => {
|
||||
expect(Array.from('/player////4'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player////4', '', '', '', '4']);
|
||||
it('match /player////4/', async () => {
|
||||
expect(Array.from('/player////4/'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player////4/', '', '', '', '4', '']);
|
||||
});
|
||||
|
||||
it('match /player/1/2//', async () => {
|
||||
expect(Array.from('/player/1/2//'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player/1/2//', '1', '2', '', '']);
|
||||
it('match /player/////5', async () => {
|
||||
expect(Array.from('/player/////5'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player/////5', '', '', '', '', '5']);
|
||||
});
|
||||
|
||||
it('match /player/1//3/', async () => {
|
||||
expect(Array.from('/player/1//3/'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player/1//3/', '1', '', '3', '']);
|
||||
it('match /player/1/2///', async () => {
|
||||
expect(Array.from('/player/1/2///'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player/1/2///', '1', '2', '', '', '']);
|
||||
});
|
||||
|
||||
it('match /player/1///4', async () => {
|
||||
expect(Array.from('/player/1///4'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player/1///4', '1', '', '', '4']);
|
||||
it('match /player/1//3//', async () => {
|
||||
expect(Array.from('/player/1//3//'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player/1//3//', '1', '', '3', '', '']);
|
||||
});
|
||||
|
||||
it('match /player//2/3/', async () => {
|
||||
expect(Array.from('/player//2/3/'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player//2/3/', '', '2', '3', '']);
|
||||
it('match /player/1///4/', async () => {
|
||||
expect(Array.from('/player/1///4/'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player/1///4/', '1', '', '', '4', '']);
|
||||
});
|
||||
|
||||
it('match /player//2//4', async () => {
|
||||
expect(Array.from('/player//2//4'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player//2//4', '', '2', '', '4']);
|
||||
it('match /player/1////5', async () => {
|
||||
expect(Array.from('/player/1////5'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player/1////5', '1', '', '', '', '5']);
|
||||
});
|
||||
|
||||
it('match /player///3/4', async () => {
|
||||
expect(Array.from('/player///3/4'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player///3/4', '', '', '3', '4']);
|
||||
it('match /player//2/3//', async () => {
|
||||
expect(Array.from('/player//2/3//'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player//2/3//', '', '2', '3', '', '']);
|
||||
});
|
||||
|
||||
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//2//4/', async () => {
|
||||
expect(Array.from('/player//2//4/'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player//2//4/', '', '2', '', '4', '']);
|
||||
});
|
||||
|
||||
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//2///5', async () => {
|
||||
expect(Array.from('/player//2///5'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player//2///5', '', '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///3/4/', async () => {
|
||||
expect(Array.from('/player///3/4/'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player///3/4/', '', '', '3', '4', '']);
|
||||
});
|
||||
|
||||
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///3//5', async () => {
|
||||
expect(Array.from('/player///3//5'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player///3//5', '', '', '3', '', '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////4/5', async () => {
|
||||
expect(Array.from('/player////4/5'.match(routesRegexp.player.regexp)))
|
||||
.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 () => {
|
||||
|
|
@ -439,8 +514,8 @@ describe('routesRegexp', () => {
|
|||
});
|
||||
|
||||
it('not match /player/', async () => {
|
||||
expect('/player/'.match(routesRegexp.player.regexp))
|
||||
.toBe(null);
|
||||
expect(Array.from('/player/'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player/', '', undefined, undefined, undefined, undefined]);
|
||||
});
|
||||
|
||||
it('not match /player//', async () => {
|
||||
|
|
@ -453,14 +528,14 @@ describe('routesRegexp', () => {
|
|||
.toBe(null);
|
||||
});
|
||||
|
||||
it('not match /player/////', async () => {
|
||||
expect('/player/////'.match(routesRegexp.player.regexp))
|
||||
it('not match /player//////', async () => {
|
||||
expect('/player//////'.match(routesRegexp.player.regexp))
|
||||
.toBe(null);
|
||||
});
|
||||
|
||||
it('not match /player/1', async () => {
|
||||
expect('/player/1'.match(routesRegexp.player.regexp))
|
||||
.toBe(null);
|
||||
expect(Array.from('/player/1'.match(routesRegexp.player.regexp)))
|
||||
.toEqual(['/player/1', '1', undefined, undefined, undefined, undefined]);
|
||||
});
|
||||
|
||||
it('not match /player/1/', async () => {
|
||||
|
|
@ -473,18 +548,38 @@ describe('routesRegexp', () => {
|
|||
.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 () => {
|
||||
expect('/player//2/'.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///3', async () => {
|
||||
expect('/player///3'.match(routesRegexp.player.regexp))
|
||||
.toBe(null);
|
||||
});
|
||||
|
||||
it('not match /player////4/', async () => {
|
||||
expect('/player////4/'.match(routesRegexp.player.regexp))
|
||||
it('not match /player///3/', async () => {
|
||||
expect('/player///3/'.match(routesRegexp.player.regexp))
|
||||
.toBe(null);
|
||||
});
|
||||
|
||||
it('not match /player////4', async () => {
|
||||
expect('/player////4'.match(routesRegexp.player.regexp))
|
||||
.toBe(null);
|
||||
});
|
||||
|
||||
|
|
@ -498,18 +593,48 @@ describe('routesRegexp', () => {
|
|||
.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 () => {
|
||||
expect('/player/1//3'.match(routesRegexp.player.regexp))
|
||||
.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 () => {
|
||||
expect('/player/1/2/3'.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))
|
||||
it('not match /player/1/2/3/', async () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,8 +6,11 @@ const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
|||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
module.exports = (env, argv) => ({
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
path: path.join(__dirname, 'build')
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
|
|
@ -125,6 +128,10 @@ module.exports = {
|
|||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.EnvironmentPlugin({
|
||||
DEBUG: argv.mode !== 'production',
|
||||
...env
|
||||
}),
|
||||
new webpack.ProgressPlugin(),
|
||||
new CopyWebpackPlugin([
|
||||
{ from: 'node_modules/stremio-core-web/static', to: '' },
|
||||
|
|
@ -142,4 +149,4 @@ module.exports = {
|
|||
cleanAfterEveryBuildPatterns: ['./main.js', './main.css']
|
||||
})
|
||||
]
|
||||
};
|
||||
});
|
||||
|
|
|
|||
62
yarn.lock
62
yarn.lock
|
|
@ -1240,6 +1240,58 @@
|
|||
react-lifecycles-compat "^3.0.4"
|
||||
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":
|
||||
version "0.3.2"
|
||||
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:
|
||||
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:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064"
|
||||
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:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
|
||||
|
|
|
|||
Loading…
Reference in a new issue