mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 17:15:48 +00:00
commit
be144496e1
26 changed files with 221 additions and 191 deletions
|
|
@ -2,7 +2,7 @@ require('spatial-navigation-polyfill');
|
|||
const React = require('react');
|
||||
const { Router } = require('stremio-router');
|
||||
const { Core, KeyboardNavigation, ServicesProvider, Shell } = require('stremio/services');
|
||||
const { ToastsContainerProvider } = require('stremio/common/Toasts/ToastsContainerContext');
|
||||
const { ToastProvider } = require('stremio/common');
|
||||
const routerViewsConfig = require('./routerViewsConfig');
|
||||
const styles = require('./styles');
|
||||
|
||||
|
|
@ -51,14 +51,14 @@ const App = () => {
|
|||
<ServicesProvider services={services}>
|
||||
{
|
||||
shellInitialized && coreInitialized ?
|
||||
<ToastsContainerProvider>
|
||||
<ToastProvider className={styles['toasts-container']}>
|
||||
<Router
|
||||
className={styles['router']}
|
||||
homePath={'/'}
|
||||
viewsConfig={routerViewsConfig}
|
||||
onPathNotMatch={onPathNotMatch}
|
||||
/>
|
||||
</ToastsContainerProvider>
|
||||
</ToastProvider>
|
||||
:
|
||||
<div className={styles['app-loader']} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
|||
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;
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
.toast-container {
|
||||
.toast-item-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 25rem;
|
||||
min-height: 6rem;
|
||||
margin-bottom: 1rem;
|
||||
border: thin solid;
|
||||
color: var(--color-backgrounddarker);
|
||||
fill: var(--color-backgrounddarker);
|
||||
background-color: var(--color-surfacelighter);
|
||||
overflow: visible;
|
||||
pointer-events: all;
|
||||
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);
|
||||
|
|
@ -26,10 +27,10 @@
|
|||
}
|
||||
|
||||
.icon-container {
|
||||
width: 5rem;
|
||||
padding: 1rem;
|
||||
padding-right: 0;
|
||||
overflow: visible;
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
width: 4.5rem;
|
||||
padding: 1.2rem 0 1.2rem 1.2rem;
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
|
|
@ -38,33 +39,35 @@
|
|||
}
|
||||
}
|
||||
|
||||
.message-container {
|
||||
.info-container {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
padding: 1rem;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
.title-container {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.message-caption {
|
||||
font-weight: bold;
|
||||
.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%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-surfacelight);
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
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 Toast = ({ type, title, text, icon, closeButton, onClick, onClose }) => {
|
||||
return (
|
||||
<div className={classnames(styles['toast-container'], styles[type])}>
|
||||
{
|
||||
typeof icon === 'string' && icon.length > 0 ?
|
||||
<div className={styles['icon-container']}>
|
||||
<Icon className={styles['icon']} icon={icon} />
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={classnames(styles['message-container'], { [styles.clickable]: typeof onClick === 'function' })} onClick={onClick}>
|
||||
{
|
||||
typeof title === 'string' && title.length > 0 ?
|
||||
<h1>{title}</h1>
|
||||
:
|
||||
null
|
||||
}
|
||||
{text}
|
||||
</div>
|
||||
{
|
||||
closeButton ?
|
||||
<Button className={styles['close-button-container']} title={'Close'} onClick={onClose}>
|
||||
<Icon className={styles['icon']} icon={'ic_x'} />
|
||||
</Button>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Toast.propTypes = {
|
||||
type: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
closeButton: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports = Toast;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
const Toast = require('./Toast');
|
||||
|
||||
module.exports = Toast;
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
const React = require('react');
|
||||
const { Modal } = require('stremio-router');
|
||||
const ModalsContainerContext = require('stremio-router/ModalsContainerContext/ModalsContainerContext');
|
||||
const { useToastsContainer } = require('./ToastsContainerContext');
|
||||
const Toast = require('./Toast');
|
||||
|
||||
const DEFAULT_TIMEOUT = 2000;
|
||||
|
||||
const Toasts = React.forwardRef(({ className }, ref) => {
|
||||
const toastsContainer = useToastsContainer();
|
||||
const [toasts, dispatch] = React.useReducer(
|
||||
(state, action) => {
|
||||
switch (action.type) {
|
||||
case 'add':
|
||||
return state.concat([action.item]);
|
||||
case 'remove':
|
||||
return state.filter(item => item !== action.item);
|
||||
case 'removeAll':
|
||||
state.forEach(item => clearTimeout(item.timerId));
|
||||
return [];
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}, []
|
||||
);
|
||||
const hideAll = React.useCallback(() => {
|
||||
dispatch({ type: 'removeAll' });
|
||||
}, []);
|
||||
const show = React.useCallback(({ type, icon, title, text, closeButton, timeout, onClick }) => {
|
||||
timeout = timeout !== null && !isNaN(timeout) ? timeout : DEFAULT_TIMEOUT;
|
||||
const close = () => {
|
||||
clearTimeout(newItem.timerId);
|
||||
dispatch({ type: 'remove', item: newItem });
|
||||
};
|
||||
|
||||
const newItem = { type, icon, title, text, closeButton, timeout, onClick, onClose: close };
|
||||
|
||||
if (timeout !== 0) {
|
||||
newItem.timerId = setTimeout(close, timeout);
|
||||
}
|
||||
dispatch({ type: 'add', item: newItem });
|
||||
return close;
|
||||
}, []);
|
||||
React.useImperativeHandle(ref, () => ({ show, hideAll }));
|
||||
|
||||
return toasts.length === 0 ? null : (
|
||||
<ModalsContainerContext.Provider value={toastsContainer}>
|
||||
<Modal className={className} disabled={true}>
|
||||
{toasts.map((toast, index) => (<Toast {...toast} key={index} />))}
|
||||
</Modal>
|
||||
</ModalsContainerContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
module.exports = Toasts;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
const React = require('react');
|
||||
|
||||
const ToastsContainerContext = React.createContext(null);
|
||||
|
||||
ToastsContainerContext.displayName = 'ToastsContainerContext';
|
||||
|
||||
module.exports = ToastsContainerContext;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const ToastsContainerContext = require('./ToastsContainerContext');
|
||||
const styles = require('./styles');
|
||||
|
||||
const ToastsContainerProvider = ({ children }) => {
|
||||
const [container, setContainer] = React.useState(null);
|
||||
return (
|
||||
<ToastsContainerContext.Provider value={container}>
|
||||
{container instanceof HTMLElement ? children : null}
|
||||
<div ref={setContainer} className={styles['toasts-container']} />
|
||||
</ToastsContainerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
ToastsContainerProvider.propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
module.exports = ToastsContainerProvider;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
const ToastsContainerProvider = require('./ToastsContainerProvider');
|
||||
const useToastsContainer = require('./useToastsContainer');
|
||||
|
||||
module.exports = {
|
||||
ToastsContainerProvider,
|
||||
useToastsContainer
|
||||
};
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
.toasts-container {
|
||||
position: absolute;
|
||||
top: var(--nav-bar-size);
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: auto !important;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
const React = require('react');
|
||||
const ToastsContainerContext = require('./ToastsContainerContext');
|
||||
|
||||
const useToastsContainer = () => {
|
||||
return React.useContext(ToastsContainerContext);
|
||||
};
|
||||
|
||||
module.exports = useToastsContainer;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
const Toasts = require('./Toasts');
|
||||
|
||||
module.exports = Toasts;
|
||||
|
|
@ -16,7 +16,7 @@ const Popup = require('./Popup');
|
|||
const SharePrompt = require('./SharePrompt');
|
||||
const Slider = require('./Slider');
|
||||
const TextInput = require('./TextInput');
|
||||
const Toasts = require('./Toasts');
|
||||
const { ToastProvider, useToast } = require('./Toast');
|
||||
const routesRegexp = require('./routesRegexp');
|
||||
const useAnimationFrame = require('./useAnimationFrame');
|
||||
const useBinaryState = require('./useBinaryState');
|
||||
|
|
@ -47,7 +47,8 @@ module.exports = {
|
|||
SharePrompt,
|
||||
Slider,
|
||||
TextInput,
|
||||
Toasts,
|
||||
ToastProvider,
|
||||
useToast,
|
||||
routesRegexp,
|
||||
useAnimationFrame,
|
||||
useBinaryState,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue