diff --git a/src/common/ModalDialog/ModalDialog.js b/src/common/ModalDialog/ModalDialog.js index 20128aa25..bb632fc6b 100644 --- a/src/common/ModalDialog/ModalDialog.js +++ b/src/common/ModalDialog/ModalDialog.js @@ -7,38 +7,48 @@ const { Modal } = require('stremio-router'); const styles = require('./styles'); const ModalDialog = ({ className, children, title, buttons, onCloseRequest }) => { - const dispatchCloseRequestEvent = React.useCallback(event => { - if (typeof onCloseRequest === 'function') { - onCloseRequest({ - type: 'closeRequest', - reactEvent: event, - nativeEvent: event.nativeEvent - }); - } - }, [onCloseRequest]); React.useEffect(() => { const onKeyDown = (event) => { if (event.key === 'Escape') { - dispatchCloseRequestEvent(event); + onCloseRequest({ + type: 'close', + nativeEvent: event + }); } }; window.addEventListener('keydown', onKeyDown); return () => { window.removeEventListener('keydown', onKeyDown); }; - }, [dispatchCloseRequestEvent]); - const onModalContainerMouseDown = React.useCallback(event => { + }, [onCloseRequest]); + const closeButtonOnClick = React.useCallback((event) => { + onCloseRequest({ + type: 'close', + reactEvent: event, + nativeEvent: event.nativeEvent + }); + }, [onCloseRequest]); + const onModalContainerMouseDown = React.useCallback((event) => { if (event.target === event.currentTarget) { - dispatchCloseRequestEvent(event); + onCloseRequest({ + type: 'close', + reactEvent: event, + nativeEvent: event.nativeEvent + }); } - }, [dispatchCloseRequestEvent]); + }, [onCloseRequest]); return (
- -

{title}

+ { + typeof title === 'string' && title.length > 0 ? +

{title}

+ : + null + }
{children}
diff --git a/src/common/ModalDialog/styles.less b/src/common/ModalDialog/styles.less index 09196210b..9eb93b01f 100644 --- a/src/common/ModalDialog/styles.less +++ b/src/common/ModalDialog/styles.less @@ -45,15 +45,20 @@ } h1 { + margin-bottom: 1rem; font-size: 1.2rem; } .modal-dialog-content { - margin-top: 1rem; + padding: 1rem; + + >:not(:first-child) { + margin-top: 1rem; + } } .modal-dialog-buttons { - margin-top: 1rem; + margin: 1rem; display: flex; flex-direction: row; } @@ -94,6 +99,6 @@ } &:not(:last-child) { - margin-right: 1rem; + margin-right: 2rem; } } \ No newline at end of file diff --git a/src/common/SharePrompt/SharePrompt.js b/src/common/SharePrompt/SharePrompt.js index 4a1abc80b..8a8dbefef 100644 --- a/src/common/SharePrompt/SharePrompt.js +++ b/src/common/SharePrompt/SharePrompt.js @@ -2,55 +2,34 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const Icon = require('stremio-icons/dom'); -const { useFocusable } = require('stremio-router'); const Button = require('stremio/common/Button'); const TextInput = require('stremio/common/TextInput'); const styles = require('./styles'); -const SharePrompt = ({ className, label, url, close }) => { +const SharePrompt = ({ className, url }) => { const inputRef = React.useRef(null); - const focusable = useFocusable(); const copyToClipboard = React.useCallback(() => { inputRef.current.select(); document.execCommand('copy'); }, []); - React.useEffect(() => { - const onKeyUp = (event) => { - if (event.key === 'Escape' && typeof close === 'function') { - close(); - } - }; - if (focusable) { - window.addEventListener('keyup', onKeyUp); - } - return () => { - window.removeEventListener('keyup', onKeyUp); - }; - }, [close, focusable]); return (
- -
-
{label}
-
- - -
-
- - -
+
+ + +
+
+ +
); @@ -58,9 +37,7 @@ const SharePrompt = ({ className, label, url, close }) => { SharePrompt.propTypes = { className: PropTypes.string, - label: PropTypes.string.isRequired, - url: PropTypes.string.isRequired, - close: PropTypes.func + url: PropTypes.string.isRequired }; module.exports = SharePrompt; diff --git a/src/common/SharePrompt/styles.less b/src/common/SharePrompt/styles.less index 36c97ce7f..712f9d7cf 100644 --- a/src/common/SharePrompt/styles.less +++ b/src/common/SharePrompt/styles.less @@ -1,133 +1,107 @@ .share-prompt-container { - position: relative; - z-index: 0; - display: flex; - flex-direction: column; - padding: 2.4rem 0; - background-color: var(--color-surfacelighter); + .buttons-container { + flex: none; + align-self: stretch; + display: flex; + flex-direction: row; - .close-button-container { - position: absolute; - top: 0.4rem; - right: 0.4rem; - z-index: 1; - width: 2rem; - height: 2rem; - padding: 0.4rem; - - &:hover { - background-color: var(--color-surfacelight); - } - - .icon { - display: block; - width: 100%; - height: 100%; - fill: var(--color-backgrounddarker); - } - } - - .share-prompt-content { - padding: 0 2.4rem; - - .share-prompt-label { - font-size: 1.3rem; - color: var(--color-backgrounddarker); - } - - .buttons-container { - flex: none; - align-self: stretch; + .button-container { + flex-grow: 0; + flex-shrink: 1; + flex-basis: 14rem; display: flex; flex-direction: row; - margin: 1.4rem 0; + align-items: center; + justify-content: center; + padding: 0.6rem 1rem; - .button-container { - flex-grow: 0; - flex-shrink: 1; - flex-basis: 14rem; - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - padding: 0.6rem 1rem; - - .icon { - flex: none; - width: 1.4rem; - height: 1.4rem; - margin-right: 0.6rem; - fill: var(--color-surfacelighter); - } - - .label { - flex-grow: 0; - flex-shrink: 1; - flex-basis: auto; - font-size: 0.8rem; - font-weight: 500; - color: var(--color-surfacelighter); - text-align: center; - } - - &:hover, &:focus { - filter: brightness(1.2); - } - - &:not(:last-child) { - margin-right: 2rem; - } + .icon { + flex: none; + width: 1.4rem; + height: 1.4rem; + margin-right: 0.6rem; + fill: var(--color-surfacelighter); } - .facebook-button { - background-color: var(--color-facebook); - } - - .twitter-button { - background-color: var(--color-twitter); - } - } - - .url-container { - display: flex; - flex-direction: row; - border: thin solid var(--color-surface); - - .url-content { - flex: 1; - min-width: 12rem; - padding: 0.6rem 1rem; - font-size: 0.9rem; - color: var(--color-surfacedark); - text-align: center; - } - - .copy-button { + .label { flex-grow: 0; flex-shrink: 1; flex-basis: auto; - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - padding: 0.6rem 1rem; - background-color: var(--color-surface); + font-size: 0.8rem; + font-weight: 500; + color: var(--color-surfacelighter); + text-align: center; + } - .icon { - flex: none; - width: 1.4rem; - height: 1.4rem; - margin-right: 0.6rem; - fill: var(--color-surfacedarker); - } + &:hover { + filter: brightness(1.2); + } - .label { - color: var(--color-surfacedarker); - } + &:focus { + outline: calc(1.5 * var(--focus-outline-size)) solid var(--color-surfacelighter); + outline-offset: calc(-2 * var(--focus-outline-size)); + } - &:hover, &:focus { - filter: brightness(1.2); - } + &:not(:last-child) { + margin-right: 2rem; + } + } + + .facebook-button { + background-color: var(--color-facebook); + } + + .twitter-button { + background-color: var(--color-twitter); + } + } + + .url-container { + display: flex; + flex-direction: row; + margin-top: 2rem; + border: thin solid var(--color-surface); + + .url-content { + flex: 1; + min-width: 12rem; + padding: 0.6rem 1rem; + font-size: 0.9rem; + color: var(--color-surfacedark); + text-align: center; + border-right: thin solid var(--color-surface); + } + + .copy-button { + flex-grow: 0; + flex-shrink: 1; + flex-basis: auto; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding: 0.6rem 1rem; + background-color: var(--color-surface); + + .icon { + flex: none; + width: 1.4rem; + height: 1.4rem; + margin-right: 0.6rem; + fill: var(--color-surfacedarker); + } + + .label { + color: var(--color-surfacedarker); + } + + &:hover { + filter: brightness(1.2); + } + + &:focus { + outline: calc(1.5 * var(--focus-outline-size)) solid var(--color-surfacelighter); + outline-offset: calc(-1.5 * var(--focus-outline-size)); } } } diff --git a/src/common/routesRegexp.js b/src/common/routesRegexp.js index 5e8eae4d1..62f1f2887 100644 --- a/src/common/routesRegexp.js +++ b/src/common/routesRegexp.js @@ -24,7 +24,7 @@ const routesRegexp = { urlParamsNames: ['type', 'id', 'videoId'] }, addons: { - regexp: /^\/addons(?:\/([^\/]*?))?(?:\/([^\/]*?))?\/?$/i, + regexp: /^\/addons(?:\/([^\/]*?))?(?:\/([^\/]*?))?\/?$/i, // TODO both are required or none urlParamsNames: ['category', 'type'] }, settings: { diff --git a/src/routes/Addons/Addon/Addon.js b/src/routes/Addons/Addon/Addon.js index 496a27168..023c17732 100644 --- a/src/routes/Addons/Addon/Addon.js +++ b/src/routes/Addons/Addon/Addon.js @@ -5,7 +5,7 @@ const Icon = require('stremio-icons/dom'); const { Button } = require('stremio/common'); const styles = require('./styles'); -const Addon = ({ className, id, name, logo, description, types, version, transportUrl, installed, toggle }) => { +const Addon = ({ className, id, name, logo, description, types, version, transportUrl, installed, toggle, onShareButtonClicked }) => { const onKeyUp = React.useCallback((event) => { if (event.key === 'Enter' && typeof toggle === 'function') { toggle(event); @@ -55,7 +55,7 @@ const Addon = ({ className, id, name, logo, description, types, version, transpo - @@ -74,7 +74,8 @@ Addon.propTypes = { version: PropTypes.string, transportUrl: PropTypes.string, installed: PropTypes.bool, - toggle: PropTypes.func + toggle: PropTypes.func, + onShareButtonClicked: PropTypes.func }; module.exports = Addon; diff --git a/src/routes/Addons/AddonPrompt/AddonPrompt.js b/src/routes/Addons/AddonPrompt/AddonPrompt.js index a5511c71e..4d6f9ffc8 100644 --- a/src/routes/Addons/AddonPrompt/AddonPrompt.js +++ b/src/routes/Addons/AddonPrompt/AddonPrompt.js @@ -1,116 +1,86 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); -const Icon = require('stremio-icons/dom'); -const { useFocusable } = require('stremio-router'); -const { Button } = require('stremio/common'); const styles = require('./styles'); -const AddonPrompt = ({ className, id, name, logo, description, types, catalogs, version, transportUrl, installed, official, cancel }) => { - const focusable = useFocusable(); - React.useEffect(() => { - const onKeyUp = (event) => { - if (event.key === 'Escape') { - cancel(); - } - }; - if (focusable) { - window.addEventListener('keyup', onKeyUp); - } - return () => { - window.removeEventListener('keyup', onKeyUp); - }; - }, [cancel, focusable]); +const AddonPrompt = ({ className, id, name, logo, description, types, catalogs, version, transportUrl, official }) => { return (
- -
-
0 })}> - { - typeof logo === 'string' && logo.length > 0 ? -
- {' -
- : - null - } - {typeof name === 'string' && name.length > 0 ? name : id} - {' '} - { - typeof version === 'string' && version.length > 0 ? - v.{version} - : - null - } -
+
0 })}> { - typeof description === 'string' && description.length > 0 ? -
- {description} + typeof logo === 'string' && logo.length > 0 ? +
+ {'
: null } + {typeof name === 'string' && name.length > 0 ? name : id} + {' '} { - typeof transportUrl === 'string' && transportUrl.length > 0 ? -
- URL: - {transportUrl} -
- : - null - } - { - Array.isArray(types) && types.length > 0 ? -
- Supported types: - - { - types.length === 1 ? - types[0] - : - types.slice(0, -1).join(', ') + ' & ' + types[types.length - 1] - } - -
- : - null - } - { - Array.isArray(catalogs) && catalogs.length > 0 ? -
- Supported catalogs: - - { - catalogs.length === 1 ? - catalogs[0].name - : - catalogs.slice(0, -1).map(({ name }) => name).join(', ') + ' & ' + catalogs[catalogs.length - 1].name - } - -
- : - null - } - { - !official ? -
-
Using third-party add-ons will always be subject to your responsibility and the governing law of the jurisdiction you are located.
-
+ typeof version === 'string' && version.length > 0 ? + v.{version} : null }
-
- - -
+ { + typeof description === 'string' && description.length > 0 ? +
+ {description} +
+ : + null + } + { + typeof transportUrl === 'string' && transportUrl.length > 0 ? +
+ URL: + {transportUrl} +
+ : + null + } + { + Array.isArray(types) && types.length > 0 ? +
+ Supported types: + + { + types.length === 1 ? + types[0] + : + types.slice(0, -1).join(', ') + ' & ' + types[types.length - 1] + } + +
+ : + null + } + { + Array.isArray(catalogs) && catalogs.length > 0 ? +
+ Supported catalogs: + + { + catalogs.length === 1 ? + catalogs[0].name + : + catalogs.slice(0, -1).map(({ name }) => name).join(', ') + ' & ' + catalogs[catalogs.length - 1].name + } + +
+ : + null + } + { + !official ? +
+
Using third-party add-ons will always be subject to your responsibility and the governing law of the jurisdiction you are located.
+
+ : + null + }
); }; @@ -127,9 +97,7 @@ AddonPrompt.propTypes = { })), version: PropTypes.string, transportUrl: PropTypes.string, - installed: PropTypes.bool, - official: PropTypes.bool, - cancel: PropTypes.func + official: PropTypes.bool }; module.exports = AddonPrompt; diff --git a/src/routes/Addons/AddonPrompt/styles.less b/src/routes/Addons/AddonPrompt/styles.less index 11249348a..b58aafceb 100644 --- a/src/routes/Addons/AddonPrompt/styles.less +++ b/src/routes/Addons/AddonPrompt/styles.less @@ -1,153 +1,54 @@ .addon-prompt-container { - position: relative; - z-index: 0; - display: flex; - flex-direction: column; - padding: 3rem 0; - background-color: var(--color-surfacelighter); + .title-container { + font-size: 3rem; + font-weight: 300; + word-break: break-all; - .close-button-container { - position: absolute; - top: 0.5rem; - right: 0.5rem; - z-index: 1; - width: 2.5rem; - height: 2.5rem; - padding: 0.5rem; - - &:hover { - background-color: var(--color-surfacelight); + &.title-with-logo-container { + &::first-line { + line-height: 5rem; + } } - .icon { - display: block; - width: 100%; - height: 100%; - fill: var(--color-backgrounddarker); + .logo-container { + width: 5rem; + height: 5rem; + margin-right: 0.5rem; + background-color: var(--color-surfacelight20); + float: left; + + .logo { + display: block; + width: 100%; + height: 100%; + object-fit: contain; + object-position: center; + } + } + + .version-container { + font-size: 1.5rem; + font-weight: 400; } } - .addon-prompt-content { - flex-grow: 0; - flex-shrink: 1; - flex-basis: auto; - align-self: stretch; - padding: 0 3rem; - overflow-y: auto; + .section-container { + margin-top: 1rem; - .title-container { - font-size: 3rem; + .section-header { + font-size: 1.2rem; + } + + .section-label { + font-size: 1.2rem; font-weight: 300; - word-break: break-all; - &.title-with-logo-container { - &::first-line { - line-height: 5rem; - } + &.transport-url-label { + user-select: text; } - .logo-container { - width: 5rem; - height: 5rem; - margin-right: 0.5rem; - background-color: var(--color-surfacelight20); - float: left; - - .logo { - display: block; - width: 100%; - height: 100%; - object-fit: contain; - object-position: center; - } - } - - .version-container { - font-size: 1.5rem; - font-weight: 400; - } - } - - .section-container { - margin-top: 1rem; - - .section-header { - font-size: 1.2rem; - } - - .section-label { - font-size: 1.2rem; - font-weight: 300; - - &.transport-url-label { - user-select: text; - } - - &.disclaimer-label { - font-style: italic; - } - } - } - } - - .buttons-container { - flex: none; - align-self: stretch; - display: flex; - flex-direction: row; - margin-top: 2rem; - padding: 0 3rem; - - .button-container { - flex: 1; - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - height: 4rem; - padding: 0 1rem; - - &:first-child { - margin-right: 2rem; - } - - .label { - flex-grow: 0; - flex-shrink: 1; - flex-basis: auto; - max-height: 2.4em; - font-size: 1.3rem; - font-weight: 500; - text-align: center; - } - } - - .cancel-button, .uninstall-button { - outline-color: var(--color-surfacedark); - outline-style: solid; - - &:hover, &:focus { - background-color: var(--color-surfacelight); - } - - .label { - color: var(--color-backgrounddarker); - } - } - - .install-button { - background-color: var(--color-signal5); - - &:hover, &:focus { - filter: brightness(1.2); - } - - &:focus { - outline-color: var(--color-surfacedarker); - } - - .label { - color: var(--color-surfacelighter); + &.disclaimer-label { + font-style: italic; } } } diff --git a/src/routes/Addons/Addons.js b/src/routes/Addons/Addons.js index 5146bc713..894fec2d7 100644 --- a/src/routes/Addons/Addons.js +++ b/src/routes/Addons/Addons.js @@ -1,7 +1,6 @@ const React = require('react'); const Icon = require('stremio-icons/dom'); -const { Modal } = require('stremio-router'); -const { Button, Dropdown, NavBar, TextInput } = require('stremio/common'); +const { Button, Multiselect, NavBar, TextInput, SharePrompt, ModalDialog } = require('stremio/common'); const Addon = require('./Addon'); const AddonPrompt = require('./AddonPrompt'); const useAddons = require('./useAddons'); @@ -9,31 +8,42 @@ const useSelectedAddon = require('./useSelectedAddon'); const styles = require('./styles'); const Addons = ({ urlParams, queryParams }) => { + const inputRef = React.useRef(null); const [query, setQuery] = React.useState(''); const queryOnChange = React.useCallback((event) => { setQuery(event.currentTarget.value); }, []); - const [addons, dropdowns] = useAddons(urlParams.category, urlParams.type); + const [[addons, dropdowns, setSelectedAddon, installedAddons, error], installSelectedAddon, uninstallSelectedAddon] = useAddons(urlParams, queryParams); + const [addAddonModalOpened, setAddAddonModalOpened] = React.useState(false); const [selectedAddon, clearSelectedAddon] = useSelectedAddon(queryParams.get('addon')); - const addonPromptModalBackgroundOnClick = React.useCallback((event) => { - if (!event.nativeEvent.clearSelectedAddonPrevented) { - clearSelectedAddon(); + const [sharedAddon, setSharedAddon] = React.useState(null); + const onAddAddonButtonClicked = React.useCallback(() => { + setAddAddonModalOpened(true); + }, []); + const onAddButtonClicked = React.useCallback(() => { + if (inputRef.current.value.length > 0) { + setSelectedAddon(inputRef.current.value); + setAddAddonModalOpened(false); } - }, []); - const addonPromptOnClick = React.useCallback((event) => { - event.nativeEvent.clearSelectedAddonPrevented = true; - }, []); + }, [setSelectedAddon]); + const installedAddon = React.useCallback((currentAddon) => { + return installedAddons.some((installedAddon) => installedAddon.transportUrl === currentAddon.transportUrl); + }, [installedAddons]); + const toggleAddon = React.useCallback(() => { + installedAddon(selectedAddon) ? uninstallSelectedAddon(selectedAddon) : installSelectedAddon(selectedAddon); + clearSelectedAddon(); + }, [selectedAddon]); return (
- - {dropdowns.map((dropdown) => ( - + {dropdowns.map((dropdown, index) => ( + ))}
-
+
{ - addons.filter(({ name }) => query.length === 0 || (typeof name === 'string' && name.includes(query))) - .map((addon) => ( - - )) + error !== null ? +
+ {error.type}{error.type === 'Other' ? ` - ${error.content}` : null} +
+ : + Array.isArray(addons) ? + addons.filter((addon) => query.length === 0 || + ((typeof addon.manifest.name === 'string' && addon.manifest.name.toLowerCase().includes(query.toLowerCase())) || + (typeof addon.manifest.description === 'string' && addon.manifest.description.toLowerCase().includes(query.toLowerCase())) + )) + .map((addon, index) => ( + setSelectedAddon(addon.transportUrl)} + onShareButtonClicked={() => setSharedAddon(addon)} + /> + )) + : +
+ Loading +
}
+ { + addAddonModalOpened ? + setAddAddonModalOpened(false) + } + }, + { + label: 'Add', + props: { + title: 'Add', + onClick: onAddButtonClicked + } + } + ]} + onCloseRequest={() => setAddAddonModalOpened(false)} + > + + + : + null + } { selectedAddon !== null ? - -
- -
-
+ + + + : + null + } + { + sharedAddon !== null ? + setSharedAddon(null)}> + setSharedAddon(null)} + /> + : null } diff --git a/src/routes/Addons/styles.less b/src/routes/Addons/styles.less index 5ba0a6f00..d97a130ea 100644 --- a/src/routes/Addons/styles.less +++ b/src/routes/Addons/styles.less @@ -21,6 +21,7 @@ display: flex; flex-direction: row; margin: 2rem; + overflow: visible; .add-button-container { flex: none; @@ -112,29 +113,41 @@ width: 100%; margin-bottom: 2rem; } + + .message-container { + padding: 0 2rem; + font-size: 2rem; + color: var(--color-surfacelighter); + } } } } -.addon-prompt-modal-container { - display: flex; - align-items: center; - justify-content: center; - background-color: var(--color-background60); +.add-addon-prompt-container { + width: 30rem; - .addon-prompt-container { - flex: none; - display: flex; - flex-direction: column; - justify-content: center; - width: 50rem; - height: 80%; - - .addon-prompt { - flex-grow: 0; - flex-shrink: 1; - flex-basis: auto; - align-self: stretch; - } + .url-content { + flex: 1; + width: 100%; + padding: 0.5rem; + font-size: 0.9rem; + color: var(--color-surfacedark); + border: thin solid var(--color-surface); } + + .cancel-button { + background-color: var(--color-surfacedark); + } +} + +.addon-prompt-container { + width: 50rem; + + .cancel-button { + background-color: var(--color-surfacedark); + } +} + +.share-prompt-container { + width: 30rem; } \ No newline at end of file diff --git a/src/routes/Addons/useAddons.js b/src/routes/Addons/useAddons.js index 3435c0dae..154af9940 100644 --- a/src/routes/Addons/useAddons.js +++ b/src/routes/Addons/useAddons.js @@ -1,72 +1,129 @@ const React = require('react'); +const { useServices } = require('stremio/services'); -const CATEGORIES = ['official', 'community', 'my']; -const DEFAULT_CATEGORY = 'community'; -const DEFAULT_TYPE = 'all'; +const DEFAULT_TYPE = 'movie'; +const DEFAULT_CATEGORY = 'thirdparty'; -const useAddons = (category, type) => { - category = CATEGORIES.includes(category) ? category : DEFAULT_CATEGORY; - type = typeof type === 'string' && type.length > 0 ? type : DEFAULT_TYPE; - const addons = React.useMemo(() => { - return [ - { - id: 'com.linvo.cinemeta', - name: 'Cinemeta', - description: 'The official add-on for movie and series catalogs', - types: ['movie', 'series'], - version: '2.12.1', - transportUrl: 'https://v3-cinemeta.strem.io/manifest.json', - installed: true, - official: true - }, - { - id: 'com.linvo.cinemeta2', - name: 'Cinemeta2', - logo: '/images/intro_background.jpg', - description: 'The official add-on for movie and series catalogs', - types: ['movie', 'series'], - version: '2.12.2', - transportUrl: 'https://v2-cinemeta.strem.io/manifest.json', - installed: false, - official: false +const useAddons = (urlParams, queryParams) => { + const { core } = useServices(); + const [addons, setAddons] = React.useState([[], [], [], [], null]); + const installAddon = React.useCallback(descriptor => { + core.dispatch({ + action: 'AddonOp', + args: { + addonOp: 'Install', + args: descriptor } - ]; + }); }, []); - const onSelect = React.useCallback((event) => { - const { name, value } = event.currentTarget.dataset; - if (name === 'category') { - const nextCategory = CATEGORIES.includes(value) ? value : ''; - window.location.replace(`#/addons/${nextCategory}/${type}`); - } else if (name === 'type') { - const nextType = typeof value === 'string' ? value : ''; - window.location.replace(`#/addons/${category}/${nextType}`); - } - }, [category, type]); - const categoryDropdown = React.useMemo(() => { - const selected = CATEGORIES.includes(category) ? [category] : []; - const options = CATEGORIES - .map((category) => ({ label: category, value: category })); - return { - name: 'category', - selected, - options, - onSelect + const uninstallAddon = React.useCallback(descriptor => { + core.dispatch({ + action: 'AddonOp', + args: { + addonOp: 'Remove', + args: { + transport_url: descriptor.transportUrl + } + } + }); + }, []); + React.useEffect(() => { + const type = typeof urlParams.type === 'string' && urlParams.type.length > 0 ? urlParams.type : DEFAULT_TYPE; + const category = typeof urlParams.category === 'string' && urlParams.category.length > 0 ? urlParams.category : DEFAULT_CATEGORY; + const onNewState = () => { + const state = core.getState(); + [...new Set( + ['all'].concat(...state.ctx.content.addons.map(addon => addon.manifest.types)) + )] + .map((type) => ( + { + is_selected: urlParams.category === 'my' && urlParams.type === type, + name: 'my', + load: { + base: 'https://v3-cinemeta.strem.io/manifest.json', + path: { + resource: 'addon_catalog', + type_name: type, + id: 'my', + extra: [] + } + } + }) + ) + .forEach(addon => state.addons.catalogs.push(addon)); + const selectAddon = (transportUrl) => { + window.location = `#/addons/${category}/${type}?addon=${transportUrl}`; + }; + const selectInputs = [ + { + selected: state.addons.catalogs + .filter(({ is_selected }) => is_selected) + .map(({ load }) => load.path.id), + options: state.addons.catalogs + .filter((catalog, index, catalogs) => { + return catalogs.map(ctg => ctg.name).indexOf(catalog.name) === index; + }) + .map(({ name, load }) => ({ + value: load.path.id, + label: name + })), + onSelect: (event) => { + const load = state.addons.catalogs.find(({ load: { path: { id } } }) => { + return id === event.value; + }).load; + window.location = `#/addons/${encodeURIComponent(load.path.id)}/${encodeURIComponent(load.path.type_name)}`; + } + }, + { + selected: state.addons.catalogs + .filter(({ is_selected }) => is_selected) + .map(({ load }) => JSON.stringify(load)), + options: state.addons.catalogs + .filter(({ load: { path: { id } } }) => { + return id === category; + }) + .map(({ load }) => ({ + value: JSON.stringify(load), + label: load.path.type_name + })), + onSelect: (event) => { + const load = JSON.parse(event.value); + window.location = `#/addons/${encodeURIComponent(load.path.id)}/${encodeURIComponent(load.path.type_name)}`; + } + } + ]; + const installedAddons = state.ctx.is_loaded ? state.ctx.content.addons : []; + const addonsItems = urlParams.category === 'my' ? + installedAddons.filter(addon => urlParams.type === 'all' || addon.manifest.types.includes(urlParams.type)) + : + state.addons.content.type === 'Ready' ? + state.addons.content.content + : + []; + const error = state.addons.content.type === 'Err' && !state.ctx.is_loaded ? state.addons.content.content : null; + setAddons([addonsItems, selectInputs, selectAddon, installedAddons, error]); }; - }, [category, onSelect]); - const typeDropdown = React.useMemo(() => { - const selected = typeof type === 'string' && type.length > 0 ? [type] : []; - const options = ['all', 'movie', 'series', 'channel'] - .concat(selected) - .filter((type, index, types) => types.indexOf(type) === index) - .map((type) => ({ label: type, value: type })); - return { - name: 'type', - selected, - options, - onSelect + core.on('NewModel', onNewState); + core.dispatch({ + action: 'Load', + args: { + load: 'CatalogFiltered', + args: { + base: 'https://v3-cinemeta.strem.io/manifest.json', + path: { + resource: 'addon_catalog', + type_name: type, + id: category, + extra: [] + } + } + } + }); + return () => { + core.off('NewModel', onNewState); }; - }, [type, onSelect]); - return [addons, [categoryDropdown, typeDropdown]]; + }, [urlParams, queryParams]); + return [addons, installAddon, uninstallAddon]; }; module.exports = useAddons; diff --git a/src/routes/Addons/useSelectedAddon.js b/src/routes/Addons/useSelectedAddon.js index 501b1754e..5bffaec37 100644 --- a/src/routes/Addons/useSelectedAddon.js +++ b/src/routes/Addons/useSelectedAddon.js @@ -12,19 +12,24 @@ const useSelectedAddon = (transportUrl) => { return; } - fetch(transportUrl) + fetch(transportUrl) // TODO .then((resp) => resp.json()) - .then((manifest) => setAddon({ ...manifest, transportUrl })); + .then((manifest) => setAddon({ manifest, transportUrl, flags: {} })); }, [transportUrl]); const clear = React.useCallback(() => { if (active) { const { pathname, search } = UrlUtils.parse(locationHash.slice(1)); - const queryParams = new URLSearchParams(search); + const queryParams = new URLSearchParams(search || ''); queryParams.delete('addon'); - window.location.replace(`#${pathname}?${queryParams.toString()}`); + if ([...queryParams].length !== 0) { + window.location.replace(`#${pathname}?${queryParams.toString()}`); + } else { + window.location.replace(`#${pathname}`); + } + setAddon(null); } - }, [active]); - return [addon, clear]; + }, [active, locationHash]); + return [addon, clear, setAddon]; }; module.exports = useSelectedAddon; diff --git a/storybook/stories/Addon/InstalledAddon.js b/storybook/stories/Addon/InstalledAddon.js index 65970946a..24400fdd7 100644 --- a/storybook/stories/Addon/InstalledAddon.js +++ b/storybook/stories/Addon/InstalledAddon.js @@ -1,6 +1,20 @@ const React = require('react'); const { storiesOf } = require('@storybook/react'); +const { action } = require('@storybook/addon-actions'); +const Addon = require('stremio/routes/Addons/Addon'); +const styles = require('./styles'); storiesOf('Addon', module).add('Installed', () => ( -
Installed addon
+ )); diff --git a/storybook/stories/Addon/NotInstalledAddon.js b/storybook/stories/Addon/NotInstalledAddon.js index 9c9a6a8de..5d5130198 100644 --- a/storybook/stories/Addon/NotInstalledAddon.js +++ b/storybook/stories/Addon/NotInstalledAddon.js @@ -1,6 +1,20 @@ const React = require('react'); const { storiesOf } = require('@storybook/react'); +const { action } = require('@storybook/addon-actions'); +const Addon = require('stremio/routes/Addons/Addon'); +const styles = require('./styles'); storiesOf('Addon', module).add('NotInstalled', () => ( -
Not installed addon
+ )); diff --git a/storybook/stories/Addon/styles.less b/storybook/stories/Addon/styles.less new file mode 100644 index 000000000..f300a05f0 --- /dev/null +++ b/storybook/stories/Addon/styles.less @@ -0,0 +1,3 @@ +.installed-addon-container, .not-installed-addon-container { + margin: 10px; +} \ No newline at end of file