diff --git a/src/App/App.js b/src/App/App.js index 09245c7d6..c7d23c300 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -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 = () => { { shellInitialized && coreInitialized ? - + - + :
} diff --git a/src/App/styles.less b/src/App/styles.less index 87529fc59..8844fde5e 100644 --- a/src/App/styles.less +++ b/src/App/styles.less @@ -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%; diff --git a/src/common/ColorInput/ColorInput.js b/src/common/ColorInput/ColorInput.js index 7e846a4a8..d64d8938e 100644 --- a/src/common/ColorInput/ColorInput.js +++ b/src/common/ColorInput/ColorInput.js @@ -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 }; diff --git a/src/common/MetaItem/MetaItem.js b/src/common/MetaItem/MetaItem.js index 4d4f6cb1f..ffd75a754 100644 --- a/src/common/MetaItem/MetaItem.js +++ b/src/common/MetaItem/MetaItem.js @@ -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 }; diff --git a/src/common/ModalDialog/ModalDialog.js b/src/common/ModalDialog/ModalDialog.js index abb594c09..441907f10 100644 --- a/src/common/ModalDialog/ModalDialog.js +++ b/src/common/ModalDialog/ModalDialog.js @@ -112,7 +112,7 @@ ModalDialog.propTypes = { PropTypes.arrayOf(PropTypes.node), PropTypes.node ]), - dataset: PropTypes.objectOf(PropTypes.string), + dataset: PropTypes.object, onCloseRequest: PropTypes.func }; diff --git a/src/common/Multiselect/Multiselect.js b/src/common/Multiselect/Multiselect.js index ff7b11eda..0ac42c2d1 100644 --- a/src/common/Multiselect/Multiselect.js +++ b/src/common/Multiselect/Multiselect.js @@ -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, diff --git a/src/common/PaginationInput/PaginationInput.js b/src/common/PaginationInput/PaginationInput.js index 7358c55c7..dd5029117 100644 --- a/src/common/PaginationInput/PaginationInput.js +++ b/src/common/PaginationInput/PaginationInput.js @@ -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 }; diff --git a/src/common/Popup/Popup.js b/src/common/Popup/Popup.js index 59b89f26e..1df4192b4 100644 --- a/src/common/Popup/Popup.js +++ b/src/common/Popup/Popup.js @@ -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 }; diff --git a/src/common/Toast/ToastContext.js b/src/common/Toast/ToastContext.js new file mode 100644 index 000000000..7418affbd --- /dev/null +++ b/src/common/Toast/ToastContext.js @@ -0,0 +1,10 @@ +const React = require('react'); + +const ToastContext = React.createContext({ + show: () => { }, + clear: () => { } +}); + +ToastContext.displayName = 'ToastContext'; + +module.exports = ToastContext; diff --git a/src/common/Toast/ToastItem/ToastItem.js b/src/common/Toast/ToastItem/ToastItem.js new file mode 100644 index 000000000..04ddbebe6 --- /dev/null +++ b/src/common/Toast/ToastItem/ToastItem.js @@ -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 ( + + + ); +}; + +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; diff --git a/src/common/Toast/ToastItem/index.js b/src/common/Toast/ToastItem/index.js new file mode 100644 index 000000000..c00d404b2 --- /dev/null +++ b/src/common/Toast/ToastItem/index.js @@ -0,0 +1,3 @@ +const ToastItem = require('./ToastItem'); + +module.exports = ToastItem; diff --git a/src/common/Toasts/Toast/styles.less b/src/common/Toast/ToastItem/styles.less similarity index 66% rename from src/common/Toasts/Toast/styles.less rename to src/common/Toast/ToastItem/styles.less index 6bb5c6399..4c09f6cff 100644 --- a/src/common/Toasts/Toast/styles.less +++ b/src/common/Toast/ToastItem/styles.less @@ -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); - } } } \ No newline at end of file diff --git a/src/common/Toast/ToastProvider.js b/src/common/Toast/ToastProvider.js new file mode 100644 index 000000000..cb91f34ee --- /dev/null +++ b/src/common/Toast/ToastProvider.js @@ -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 ( + + {container instanceof HTMLElement ? children : null} +
+ {items.map((item, index) => ( + + ))} +
+
+ ); +}; + +ToastProvider.propTypes = { + className: PropTypes.string, + children: PropTypes.node +}; + +module.exports = ToastProvider; diff --git a/src/common/Toast/index.js b/src/common/Toast/index.js new file mode 100644 index 000000000..516e30a1e --- /dev/null +++ b/src/common/Toast/index.js @@ -0,0 +1,7 @@ +const ToastProvider = require('./ToastProvider'); +const useToast = require('./useToast'); + +module.exports = { + ToastProvider, + useToast +}; diff --git a/src/common/Toast/useToast.js b/src/common/Toast/useToast.js new file mode 100644 index 000000000..76ba83914 --- /dev/null +++ b/src/common/Toast/useToast.js @@ -0,0 +1,8 @@ +const React = require('react'); +const ToastContext = require('./ToastContext'); + +const useToast = () => { + return React.useContext(ToastContext); +}; + +module.exports = useToast; diff --git a/src/common/Toasts/Toast/Toast.js b/src/common/Toasts/Toast/Toast.js deleted file mode 100644 index b667a6d55..000000000 --- a/src/common/Toasts/Toast/Toast.js +++ /dev/null @@ -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 ( -
- { - typeof icon === 'string' && icon.length > 0 ? -
- -
- : - null - } -
- { - typeof title === 'string' && title.length > 0 ? -

{title}

- : - null - } - {text} -
- { - closeButton ? - - : - null - } -
- ); -}; - -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; diff --git a/src/common/Toasts/Toast/index.js b/src/common/Toasts/Toast/index.js deleted file mode 100644 index efe31724b..000000000 --- a/src/common/Toasts/Toast/index.js +++ /dev/null @@ -1,3 +0,0 @@ -const Toast = require('./Toast'); - -module.exports = Toast; \ No newline at end of file diff --git a/src/common/Toasts/Toasts.js b/src/common/Toasts/Toasts.js deleted file mode 100644 index d42a047cb..000000000 --- a/src/common/Toasts/Toasts.js +++ /dev/null @@ -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 : ( - - - {toasts.map((toast, index) => ())} - - - ); -}); - -module.exports = Toasts; diff --git a/src/common/Toasts/ToastsContainerContext/ToastsContainerContext.js b/src/common/Toasts/ToastsContainerContext/ToastsContainerContext.js deleted file mode 100644 index fdff8212f..000000000 --- a/src/common/Toasts/ToastsContainerContext/ToastsContainerContext.js +++ /dev/null @@ -1,7 +0,0 @@ -const React = require('react'); - -const ToastsContainerContext = React.createContext(null); - -ToastsContainerContext.displayName = 'ToastsContainerContext'; - -module.exports = ToastsContainerContext; diff --git a/src/common/Toasts/ToastsContainerContext/ToastsContainerProvider.js b/src/common/Toasts/ToastsContainerContext/ToastsContainerProvider.js deleted file mode 100644 index f54635819..000000000 --- a/src/common/Toasts/ToastsContainerContext/ToastsContainerProvider.js +++ /dev/null @@ -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 ( - - {container instanceof HTMLElement ? children : null} -
- - ); -}; - -ToastsContainerProvider.propTypes = { - children: PropTypes.node -}; - -module.exports = ToastsContainerProvider; diff --git a/src/common/Toasts/ToastsContainerContext/index.js b/src/common/Toasts/ToastsContainerContext/index.js deleted file mode 100644 index 55ef960ec..000000000 --- a/src/common/Toasts/ToastsContainerContext/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const ToastsContainerProvider = require('./ToastsContainerProvider'); -const useToastsContainer = require('./useToastsContainer'); - -module.exports = { - ToastsContainerProvider, - useToastsContainer -}; diff --git a/src/common/Toasts/ToastsContainerContext/styles.less b/src/common/Toasts/ToastsContainerContext/styles.less deleted file mode 100644 index 1afd990e6..000000000 --- a/src/common/Toasts/ToastsContainerContext/styles.less +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/src/common/Toasts/ToastsContainerContext/useToastsContainer.js b/src/common/Toasts/ToastsContainerContext/useToastsContainer.js deleted file mode 100644 index 0f32d0d4c..000000000 --- a/src/common/Toasts/ToastsContainerContext/useToastsContainer.js +++ /dev/null @@ -1,8 +0,0 @@ -const React = require('react'); -const ToastsContainerContext = require('./ToastsContainerContext'); - -const useToastsContainer = () => { - return React.useContext(ToastsContainerContext); -}; - -module.exports = useToastsContainer; diff --git a/src/common/Toasts/index.js b/src/common/Toasts/index.js deleted file mode 100644 index b06087a3a..000000000 --- a/src/common/Toasts/index.js +++ /dev/null @@ -1,3 +0,0 @@ -const Toasts = require('./Toasts'); - -module.exports = Toasts; diff --git a/src/common/index.js b/src/common/index.js index 40a3a9e90..bc5bb3c37 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -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, diff --git a/src/routes/Addons/Addon/Addon.js b/src/routes/Addons/Addon/Addon.js index d646ed2b9..3c97cd1bb 100644 --- a/src/routes/Addons/Addon/Addon.js +++ b/src/routes/Addons/Addon/Addon.js @@ -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;