diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..0c513bad1 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,85 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:react/recommended" + ], + "settings": { + "react": { + "version": "detect" + } + }, + "globals": { + "YT": "readonly", + "FB": "readonly" + }, + "env": { + "commonjs": true, + "browser": true, + "es6": true + }, + "parserOptions": { + "ecmaVersion": 9, + "ecmaFeatures": { + "jsx": true + } + }, + "ignorePatterns": [ + "/*", + "!/src", + "src/routes/Settings", + "src/routes/Player", + "src/video" + ], + "rules": { + "arrow-parens": "error", + "arrow-spacing": "error", + "block-spacing": "error", + "comma-spacing": "error", + "eol-last": "error", + "eqeqeq": "error", + "func-call-spacing": "error", + "indent": [ + "error", + 4, + { + "SwitchCase": 1 + } + ], + "no-console": "error", + "no-extra-semi": "error", + "no-eq-null": "error", + "no-multi-spaces": "error", + "no-multiple-empty-lines": [ + "error", + { + "max": 1 + } + ], + "no-template-curly-in-string": "error", + "no-trailing-spaces": "error", + "no-useless-concat": "error", + "no-unreachable": "error", + "no-unused-vars": "error", + "prefer-const": "error", + "quotes": [ + "error", + "single" + ], + "quote-props": [ + "error", + "as-needed", + { + "unnecessary": false + } + ], + "semi": "error", + "semi-spacing": "error", + "space-before-blocks": "error", + "valid-typeof": [ + "error", + { + "requireStringLiterals": true + } + ] + } +} \ No newline at end of file diff --git a/package.json b/package.json index 17da07d2d..46392dc00 100755 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "start": "webpack-dev-server --mode development", "build": "webpack --mode production", "storybook": "start-storybook --ci --config-dir ./storybook --static-dir ./ --port 6060", - "test": "jest" + "test": "jest", + "lint": "eslint src" }, "dependencies": { "a-color-picker": "1.2.1", @@ -24,19 +25,19 @@ "react": "16.12.0", "react-dom": "16.12.0", "react-focus-lock": "2.2.1", - "spatial-navigation-polyfill": "git+ssh://git@github.com/NikolaBorislavovHristov/spatial-navigation.git#964d09bf2b0853e27af6c25924b595d6621a019d", + "spatial-navigation-polyfill": "git+ssh://git@github.com/Stremio/spatial-navigation.git#381b4f37d138e66ae8ea025e240af3704245e5dc", "stremio-colors": "git+ssh://git@github.com/Stremio/stremio-colors.git#v2.0.4", "stremio-core-web": "git+ssh://git@github.com/stremio/stremio-core-web.git#da5b37865004d0ae140518c4f276d1ed1a1483d9", "stremio-icons": "git+ssh://git@github.com/Stremio/stremio-icons.git#v1.0.11", "vtt.js": "0.13.0" }, "devDependencies": { - "@babel/core": "7.7.4", + "@babel/core": "7.7.5", "@babel/plugin-proposal-class-properties": "7.7.4", "@babel/plugin-proposal-object-rest-spread": "7.7.4", - "@babel/preset-env": "7.7.4", + "@babel/preset-env": "7.7.6", "@babel/preset-react": "7.7.4", - "@babel/runtime": "7.7.4", + "@babel/runtime": "7.7.6", "@storybook/addon-actions": "5.2.8", "@storybook/addon-console": "1.2.1", "@storybook/addons": "5.2.8", @@ -45,10 +46,12 @@ "@testing-library/react-hooks": "3.2.1", "babel-loader": "8.0.6", "clean-webpack-plugin": "3.0.0", - "copy-webpack-plugin": "5.0.5", - "css-loader": "3.2.1", + "copy-webpack-plugin": "5.1.1", + "css-loader": "3.3.2", "cssnano": "4.1.10", "cssnano-preset-advanced": "4.0.7", + "eslint": "6.7.2", + "eslint-plugin-react": "7.17.0", "html-webpack-plugin": "3.2.0", "jest": "24.9.0", "less": "3.10.3", @@ -57,8 +60,8 @@ "postcss-loader": "3.0.0", "react-test-renderer": "16.12.0", "storybook-addon-jsx": "7.1.13", - "terser-webpack-plugin": "2.2.1", - "webpack": "4.41.2", + "terser-webpack-plugin": "2.3.0", + "webpack": "4.41.3", "webpack-cli": "3.3.10", "webpack-dev-server": "3.9.0" } diff --git a/src/common/Button/Button.js b/src/common/Button/Button.js index 21512b041..89d7d342d 100644 --- a/src/common/Button/Button.js +++ b/src/common/Button/Button.js @@ -1,4 +1,5 @@ const React = require('react'); +const PropTypes = require('prop-types'); const classnames = require('classnames'); const styles = require('./styles'); @@ -41,4 +42,13 @@ const Button = React.forwardRef(({ className, href, disabled, children, ...props Button.displayName = 'Button'; +Button.propTypes = { + className: PropTypes.string, + href: PropTypes.string, + disabled: PropTypes.bool, + children: PropTypes.node, + onKeyDown: PropTypes.func, + onMouseDown: PropTypes.func +}; + module.exports = Button; diff --git a/src/common/Checkbox/Checkbox.js b/src/common/Checkbox/Checkbox.js index 96c29f334..6fd3f3a72 100644 --- a/src/common/Checkbox/Checkbox.js +++ b/src/common/Checkbox/Checkbox.js @@ -1,4 +1,5 @@ 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'); @@ -22,4 +23,10 @@ const Checkbox = React.forwardRef(({ className, checked, children, ...props }, r Checkbox.displayName = 'Checkbox'; +Checkbox.propTypes = { + className: PropTypes.string, + checked: PropTypes.bool, + children: PropTypes.node +}; + module.exports = Checkbox; diff --git a/src/common/ColorInput/ColorInput.js b/src/common/ColorInput/ColorInput.js index f493a11f1..7e846a4a8 100644 --- a/src/common/ColorInput/ColorInput.js +++ b/src/common/ColorInput/ColorInput.js @@ -16,9 +16,8 @@ const ColorInput = ({ className, value, dataset, onChange, ...props }) => { return parseColor(value); }); const labelButtonStyle = React.useMemo(() => ({ - ...props.style, backgroundColor: value - }), [props.style, value]); + }), [value]); const labelButtonOnClick = React.useCallback((event) => { if (typeof props.onClick === 'function') { props.onClick(event); @@ -76,9 +75,11 @@ const ColorInput = ({ className, value, dataset, onChange, ...props }) => { }; ColorInput.propTypes = { + className: PropTypes.string, value: PropTypes.string, - dataset: PropTypes.objectOf(String), - onChange: PropTypes.func + dataset: PropTypes.objectOf(PropTypes.string), + onChange: PropTypes.func, + onClick: PropTypes.func }; module.exports = ColorInput; diff --git a/src/common/Image/Image.js b/src/common/Image/Image.js index 520fade03..8091c2615 100644 --- a/src/common/Image/Image.js +++ b/src/common/Image/Image.js @@ -27,7 +27,8 @@ Image.propTypes = { src: PropTypes.string, alt: PropTypes.string, fallbackSrc: PropTypes.string, - renderFallback: PropTypes.func + renderFallback: PropTypes.func, + onError: PropTypes.func }; module.exports = Image; diff --git a/src/common/MainNavBar/MainNavBar.js b/src/common/MainNavBar/MainNavBar.js index c6920ce42..507f8d229 100644 --- a/src/common/MainNavBar/MainNavBar.js +++ b/src/common/MainNavBar/MainNavBar.js @@ -23,6 +23,8 @@ const MainNavBar = React.memo(({ className }) => { ); }); +MainNavBar.displayName = 'MainNavBar'; + MainNavBar.propTypes = { className: PropTypes.string }; diff --git a/src/common/MetaItem/MetaItem.js b/src/common/MetaItem/MetaItem.js index 5c130933a..4d4f6cb1f 100644 --- a/src/common/MetaItem/MetaItem.js +++ b/src/common/MetaItem/MetaItem.js @@ -121,8 +121,9 @@ MetaItem.propTypes = { playIcon: PropTypes.bool, progress: PropTypes.number, options: PropTypes.array, - dataset: PropTypes.objectOf(String), - optionOnSelect: PropTypes.func + dataset: PropTypes.objectOf(PropTypes.string), + optionOnSelect: PropTypes.func, + onClick: PropTypes.func }; module.exports = MetaItem; diff --git a/src/common/MetaPreview/MetaPreview.js b/src/common/MetaPreview/MetaPreview.js index 9cd1b7d8e..0aca83e9a 100644 --- a/src/common/MetaPreview/MetaPreview.js +++ b/src/common/MetaPreview/MetaPreview.js @@ -179,7 +179,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele null } { - linksGroups.hasOwnProperty(IMDB_LINK_CATEGORY) ? + typeof linksGroups[IMDB_LINK_CATEGORY] === 'object' ? { - const [menuOpen, openMenu, closeMenu, toggleMenu] = useBinaryState(false); + const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); const options = React.useMemo(() => { return Array.isArray(props.options) ? props.options.filter((option) => { @@ -143,12 +143,13 @@ Multiselect.propTypes = { })), selected: PropTypes.arrayOf(PropTypes.string), disabled: PropTypes.bool, - dataset: PropTypes.objectOf(String), + dataset: PropTypes.objectOf(PropTypes.string), renderLabelContent: PropTypes.func, renderLabelText: PropTypes.func, onOpen: PropTypes.func, onClose: PropTypes.func, - onSelect: PropTypes.func + onSelect: PropTypes.func, + onClick: PropTypes.func }; module.exports = Multiselect; diff --git a/src/common/NavBar/NavMenu/NavMenu.js b/src/common/NavBar/NavMenu/NavMenu.js index 60871a0a2..2509185a2 100644 --- a/src/common/NavBar/NavMenu/NavMenu.js +++ b/src/common/NavBar/NavMenu/NavMenu.js @@ -10,7 +10,7 @@ const useUser = require('stremio/common/useUser'); const styles = require('./styles'); const NavMenu = ({ className }) => { - const [menuOpen, openMenu, closeMenu, toggleMenu] = useBinaryState(false); + const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen(); const [user, logout] = useUser(); const popupLabelOnClick = React.useCallback((event) => { @@ -41,7 +41,7 @@ const NavMenu = ({ className }) => { className={styles['avatar-container']} style={{ backgroundImage: user === null ? - `url('/images/anonymous.png')` + 'url(\'/images/anonymous.png\')' : `url('${user.avatar}'), url('/images/default_avatar.png')` }} diff --git a/src/common/NavBar/NotificationsMenu/NotificationsList/Notification/Notification.js b/src/common/NavBar/NotificationsMenu/NotificationsList/Notification/Notification.js index 53c7b0e81..8bba1d435 100644 --- a/src/common/NavBar/NotificationsMenu/NotificationsList/Notification/Notification.js +++ b/src/common/NavBar/NotificationsMenu/NotificationsList/Notification/Notification.js @@ -80,7 +80,7 @@ const Notification = ({ className, id, type, name, poster, thumbnail, season, ep } ); -} +}; Notification.propTypes = { className: PropTypes.string, diff --git a/src/common/NavBar/NotificationsMenu/NotificationsList/NotificationPlaceholder/NotificationPlaceholder.js b/src/common/NavBar/NotificationsMenu/NotificationsList/NotificationPlaceholder/NotificationPlaceholder.js index 3bbac1538..0ee602b64 100644 --- a/src/common/NavBar/NotificationsMenu/NotificationsList/NotificationPlaceholder/NotificationPlaceholder.js +++ b/src/common/NavBar/NotificationsMenu/NotificationsList/NotificationPlaceholder/NotificationPlaceholder.js @@ -1,7 +1,6 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); -const Icon = require('stremio-icons/dom'); const styles = require('./styles'); const NotificationPlaceholder = ({ className }) => { @@ -20,6 +19,6 @@ const NotificationPlaceholder = ({ className }) => { NotificationPlaceholder.propTypes = { className: PropTypes.string -} +}; module.exports = NotificationPlaceholder; diff --git a/src/common/NavBar/NotificationsMenu/NotificationsList/NotificationsList.js b/src/common/NavBar/NotificationsMenu/NotificationsList/NotificationsList.js index e54bacb68..3fca82728 100644 --- a/src/common/NavBar/NotificationsMenu/NotificationsList/NotificationsList.js +++ b/src/common/NavBar/NotificationsMenu/NotificationsList/NotificationsList.js @@ -57,7 +57,7 @@ const NotificationsList = ({ className, metaItems }) => { } ); -} +}; NotificationsList.propTypes = { className: PropTypes.string, diff --git a/src/common/NavBar/NotificationsMenu/NotificationsMenu.js b/src/common/NavBar/NotificationsMenu/NotificationsMenu.js index b0ea6d6d8..7e1a1d6e7 100644 --- a/src/common/NavBar/NotificationsMenu/NotificationsMenu.js +++ b/src/common/NavBar/NotificationsMenu/NotificationsMenu.js @@ -5,13 +5,13 @@ const Icon = require('stremio-icons/dom'); const Button = require('stremio/common/Button'); const Popup = require('stremio/common/Popup'); const NotificationsList = require('./NotificationsList'); -const useNotifications = require('./useNotifications'); +// const useNotifications = require('./useNotifications'); // const useCatalogs = require('stremio/routes/Board/useCatalogs'); const useBinaryState = require('stremio/common/useBinaryState'); const styles = require('./styles'); const NotificationsMenu = ({ className, onClearButtonClicked }) => { - const [menuOpen, openMenu, closeMenu, toggleMenu] = useBinaryState(false); + const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); //TODO use useNotifications hook instead of useCatalogs const metaItems = []; //useCatalogs(); diff --git a/src/common/PaginationInput/PaginationInput.js b/src/common/PaginationInput/PaginationInput.js index 77bb38f5c..7358c55c7 100644 --- a/src/common/PaginationInput/PaginationInput.js +++ b/src/common/PaginationInput/PaginationInput.js @@ -35,6 +35,7 @@ const PaginationInput = ({ className, label, dataset, onSelect, ...props }) => { PaginationInput.propTypes = { className: PropTypes.string, label: PropTypes.string, + dataset: PropTypes.objectOf(PropTypes.string), onSelect: PropTypes.func }; diff --git a/src/common/PaginationInput/index.js b/src/common/PaginationInput/index.js index 99f9edf62..4500ec5a6 100644 --- a/src/common/PaginationInput/index.js +++ b/src/common/PaginationInput/index.js @@ -1,3 +1,3 @@ const PaginationInput = require('./PaginationInput'); -module.exports = PaginationInput; \ No newline at end of file +module.exports = PaginationInput; diff --git a/src/common/Popup/Popup.js b/src/common/Popup/Popup.js index d0b26fa03..59b89f26e 100644 --- a/src/common/Popup/Popup.js +++ b/src/common/Popup/Popup.js @@ -96,14 +96,14 @@ const Popup = ({ open, direction, renderLabel, renderMenu, dataset, onCloseReque : null }); -} +}; Popup.propTypes = { open: PropTypes.bool, direction: PropTypes.oneOf(['top-left', 'bottom-left', 'top-right', 'bottom-right']), renderLabel: PropTypes.func.isRequired, renderMenu: PropTypes.func.isRequired, - dataset: PropTypes.objectOf(String), + dataset: PropTypes.objectOf(PropTypes.string), onCloseRequest: PropTypes.func }; diff --git a/src/common/TextInput/TextInput.js b/src/common/TextInput/TextInput.js index 4f7db50f1..8b9606d6f 100644 --- a/src/common/TextInput/TextInput.js +++ b/src/common/TextInput/TextInput.js @@ -1,4 +1,5 @@ const React = require('react'); +const PropTypes = require('prop-types'); const classnames = require('classnames'); const styles = require('./styles'); @@ -30,4 +31,11 @@ const TextInput = React.forwardRef((props, ref) => { TextInput.displayName = 'TextInput'; +TextInput.propTypes = { + className: PropTypes.string, + disabled: PropTypes.bool, + onKeyDown: PropTypes.func, + onSubmit: PropTypes.func +}; + module.exports = TextInput; diff --git a/src/common/routesRegexp.js b/src/common/routesRegexp.js index df7f4e069..f4128765b 100644 --- a/src/common/routesRegexp.js +++ b/src/common/routesRegexp.js @@ -8,23 +8,23 @@ const routesRegexp = { urlParamsNames: [] }, discover: { - regexp: /^\/discover(?:\/([^\/]*)\/([^\/]*)\/([^\/]*))?$/, + regexp: /^\/discover(?:\/([^/]*)\/([^/]*)\/([^/]*))?$/, urlParamsNames: ['addonTransportUrl', 'type', 'catalogId'] }, library: { - regexp: /^\/library(?:\/([^\/]*))?$/, - urlParamsNames: ['type'] + regexp: /^\/library(?:\/([^/]*)\/([^/]*))?$/, + urlParamsNames: ['type', 'sort'] }, search: { regexp: /^\/search$/, urlParamsNames: [] }, metadetails: { - regexp: /^\/metadetails\/([^\/]*)\/([^\/]*)(?:\/([^\/]*))?$/, + regexp: /^\/metadetails\/([^/]*)\/([^/]*)(?:\/([^/]*))?$/, urlParamsNames: ['type', 'id', 'videoId'] }, addons: { - regexp: /^\/addons(?:\/([^\/]*)\/([^\/]*)\/([^\/]*))?$/, + regexp: /^\/addons(?:\/([^/]*)\/([^/]*)\/([^/]*))?$/, urlParamsNames: ['addonTransportUrl', 'catalogId', 'type'] }, settings: { @@ -32,7 +32,7 @@ const routesRegexp = { urlParamsNames: [] }, player: { - regexp: /^\/player\/([^\/]*)\/([^\/]*)\/([^\/]*)\/([^\/]*)$/, + regexp: /^\/player\/([^/]*)\/([^/]*)\/([^/]*)\/([^/]*)$/, urlParamsNames: ['type', 'id', 'videoId', 'stream'] } }; diff --git a/src/common/useInLibrary.js b/src/common/useInLibrary.js index 2f595ed15..f9de835c0 100644 --- a/src/common/useInLibrary.js +++ b/src/common/useInLibrary.js @@ -1,12 +1,60 @@ -const useBinaryState = require('stremio/common/useBinaryState'); +const React = require('react'); +const { useServices } = require('stremio/services'); +const useModelState = require('stremio/common/useModelState'); -const useInLibrary = (id) => { - const [inLibrary, addToLibrary, removeFromLibrary, toggleInLibrary] = useBinaryState(false); - if (typeof id === 'string') { - return [inLibrary, addToLibrary, removeFromLibrary, toggleInLibrary]; - } else { - return [false, null, null, null]; - } +const useInLibrary = (metaItem) => { + const { core } = useServices(); + const initLibraryItemsState = React.useCallback(() => { + return core.getState('library_items'); + }, []); + const libraryItems = useModelState({ + model: 'library_items', + init: initLibraryItemsState + }); + const addToLibrary = React.useCallback((metaItem) => { + core.dispatch({ + action: 'UserOp', + args: { + userOp: 'AddToLibrary', + args: { + meta_item: metaItem, + now: new Date() + } + } + }); + }, []); + const removeFromLibrary = React.useCallback((id) => { + core.dispatch({ + action: 'UserOp', + args: { + userOp: 'RemoveFromLibrary', + args: { + id, + now: new Date() + } + } + }); + }, []); + const inLibrary = React.useMemo(() => { + return typeof metaItem === 'object' && metaItem !== null ? + libraryItems.ids.includes(metaItem.id) + : + false; + }, [metaItem, libraryItems]); + const toggleInLibrary = React.useMemo(() => { + if (typeof metaItem !== 'object' || metaItem === null) { + return null; + } + + return () => { + if (inLibrary) { + removeFromLibrary(metaItem.id); + } else { + addToLibrary(metaItem); + } + }; + }, [metaItem, inLibrary]); + return [inLibrary, toggleInLibrary]; }; module.exports = useInLibrary; diff --git a/src/router/Router/Router.js b/src/router/Router/Router.js index dd8321e70..60fc0334b 100644 --- a/src/router/Router/Router.js +++ b/src/router/Router/Router.js @@ -95,7 +95,7 @@ const Router = ({ className, onPathNotMatch, ...props }) => {
{ views - .filter(view => view !== null) + .filter((view) => view !== null) .map(({ key, component, urlParams, queryParams }, index, views) => ( diff --git a/src/routes/Addons/Addon/Addon.js b/src/routes/Addons/Addon/Addon.js index 023c17732..d646ed2b9 100644 --- a/src/routes/Addons/Addon/Addon.js +++ b/src/routes/Addons/Addon/Addon.js @@ -2,24 +2,54 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const Icon = require('stremio-icons/dom'); -const { Button } = require('stremio/common'); +const { Button, Image } = require('stremio/common'); const styles = require('./styles'); -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); +const Addon = ({ className, id, name, version, logo, description, types, installed, onToggle, onShare, dataset }) => { + const toggleButtonOnClick = React.useCallback((event) => { + if (typeof onToggle === 'function') { + onToggle({ + type: 'toggle', + nativeEvent: event.nativeEvent, + reactEvent: event, + dataset: dataset + }); } - }, [toggle]); + }, [onToggle, dataset]); + const shareButtonOnClick = React.useCallback((event) => { + if (typeof onShare === 'function') { + onShare({ + type: 'share', + nativeEvent: event.nativeEvent, + reactEvent: event, + dataset: dataset + }); + } + }, [onShare, dataset]); + const onKeyDown = React.useCallback((event) => { + if (event.key === 'Enter' && typeof onToggle === 'function') { + onToggle({ + type: 'toggle', + nativeEvent: event.nativeEvent, + reactEvent: event, + dataset: dataset + }); + } + }, [onToggle, dataset]); + const renderLogoFallback = React.useMemo(() => () => { + return ( + + ); + }, []); return ( - - @@ -68,14 +98,14 @@ Addon.propTypes = { className: PropTypes.string, id: PropTypes.string, name: PropTypes.string, + version: PropTypes.string, logo: PropTypes.string, description: PropTypes.string, types: PropTypes.arrayOf(PropTypes.string), - version: PropTypes.string, - transportUrl: PropTypes.string, installed: PropTypes.bool, - toggle: PropTypes.func, - onShareButtonClicked: PropTypes.func + onToggle: PropTypes.func, + onShare: PropTypes.func, + dataset: PropTypes.objectOf(PropTypes.string) }; module.exports = Addon; diff --git a/src/routes/Addons/Addon/styles.less b/src/routes/Addons/Addon/styles.less index 2ae172544..e57eea428 100644 --- a/src/routes/Addons/Addon/styles.less +++ b/src/routes/Addons/Addon/styles.less @@ -1,7 +1,6 @@ .addon-container { display: flex; flex-direction: row; - flex-wrap: wrap; align-items: flex-start; padding: 1rem; background-color: var(--color-backgroundlighter); @@ -17,6 +16,7 @@ display: block; width: 100%; height: 100%; + padding: 0.5rem; object-fit: contain; object-position: center; } @@ -31,14 +31,13 @@ } .info-container { - flex-grow: 1000; + flex-grow: 1; flex-shrink: 1; flex-basis: 0; display: flex; flex-direction: row; flex-wrap: wrap; align-items: baseline; - min-width: 40rem; padding: 0 0.5rem; .name-container { @@ -47,7 +46,7 @@ flex-basis: auto; padding: 0 0.5rem; max-height: 3.6em; - font-size: 1.5rem; + font-size: 1.6rem; color: var(--color-surfacelighter); } @@ -55,6 +54,7 @@ flex-grow: 1; flex-shrink: 1; flex-basis: auto; + margin-top: 0.5rem; padding: 0 0.5rem; max-height: 2.4em; color: var(--color-surfacelight); @@ -83,22 +83,14 @@ } .buttons-container { - flex-grow: 1; - flex-shrink: 0; - flex-basis: 0; - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: flex-end; - min-width: 17rem; + flex: none; + width: 17rem; .install-button-container, .uninstall-button-container, .share-button-container { - flex: none; display: flex; flex-direction: row; align-items: center; justify-content: center; - width: 17rem; height: 3.5rem; padding: 0 1rem; @@ -106,13 +98,8 @@ margin-top: 1rem; } - &:not(:last-child) { - margin-right: 1rem; - } - .icon { flex: none; - display: block; width: 1.5rem; height: 1.5rem; margin-right: 1rem; diff --git a/src/routes/Addons/Addons.js b/src/routes/Addons/Addons.js index 0a8152a17..3f7874666 100644 --- a/src/routes/Addons/Addons.js +++ b/src/routes/Addons/Addons.js @@ -1,49 +1,93 @@ const React = require('react'); +const PropTypes = require('prop-types'); const Icon = require('stremio-icons/dom'); -const { Button, Multiselect, NavBar, TextInput, SharePrompt, ModalDialog } = require('stremio/common'); +const { Button, Multiselect, NavBar, TextInput, SharePrompt, ModalDialog, useBinaryState } = require('stremio/common'); const Addon = require('./Addon'); -const AddonPrompt = require('./AddonPrompt'); const useAddons = require('./useAddons'); -const useSelectedAddon = require('./useSelectedAddon'); +const useSelectableInputs = require('./useSelectableInputs'); const styles = require('./styles'); +const navigateToAddonDetails = (addonsCatalogRequest, transportUrl) => { + const queryParams = new URLSearchParams([['addon', transportUrl]]); + if (addonsCatalogRequest !== null) { + const addonTransportUrl = encodeURIComponent(addonsCatalogRequest.base); + const catalogId = encodeURIComponent(addonsCatalogRequest.path.id); + const type = encodeURIComponent(addonsCatalogRequest.path.type_name); + window.location.replace(`#/addons/${addonTransportUrl}/${catalogId}/${type}?${queryParams}`); + } else { + window.location.replace(`#/addons?${queryParams}`); + } +}; + 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, setSelectedAddon, installedAddons, error], installSelectedAddon, uninstallSelectedAddon] = useAddons(urlParams, queryParams); - const [addAddonModalOpened, setAddAddonModalOpened] = React.useState(false); - const [selectedAddon, clearSelectedAddon] = useSelectedAddon(queryParams.get('addon')); - 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 addons = useAddons(urlParams); + const selectInputs = useSelectableInputs(addons); + const [addAddonModalOpen, openAddAddonModal, closeAddAddonModal] = useBinaryState(false); + const addAddonUrlInputRef = React.useRef(null); + const addAddonOnSubmit = React.useCallback(() => { + if (addAddonUrlInputRef.current !== null) { + const addonsCatalogRequest = addons.catalog_resource !== null ? + addons.catalog_resource.request + : + null; + navigateToAddonDetails(addonsCatalogRequest, addAddonUrlInputRef.current.value); } - }, [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]); + }, [addons]); + const addAddonModalButtons = React.useMemo(() => { + return [ + { + className: styles['cancel-button'], + label: 'Cancel', + props: { + onClick: closeAddAddonModal + } + }, + { + label: 'Add', + props: { + onClick: addAddonOnSubmit + } + } + ]; + }, [addAddonOnSubmit]); + const [search, setSearch] = React.useState(''); + const searchInputOnChange = React.useCallback((event) => { + setSearch(event.currentTarget.value); + }, []); + const [sharedTransportUrl, setSharedTransportUrl] = React.useState(null); + const clearSharedTransportUrl = React.useCallback(() => { + setSharedTransportUrl(null); + }, []); + const onAddonShare = React.useCallback((event) => { + setSharedTransportUrl(event.dataset.transportUrl); + }, []); + const onAddonToggle = React.useCallback((event) => { + const addonsCatalogRequest = addons.catalog_resource !== null ? + addons.catalog_resource.request + : + null; + navigateToAddonDetails(addonsCatalogRequest, event.dataset.transportUrl); + }, [addons]); + React.useLayoutEffect(() => { + closeAddAddonModal(); + setSearch(''); + clearSharedTransportUrl(); + }, [urlParams, queryParams]); return (
-
- - {dropdowns.map((dropdown, index) => ( - + {selectInputs.map((selectInput, index) => ( + ))}
-
- { - error !== null ? + { + addons.selectable.catalogs.length === 0 && addons.catalog_resource === null ? +
+ No addons +
+ : + addons.catalog_resource === null ?
- {error.type}{error.type === 'Other' ? ` - ${error.content}` : null} + No select
: - 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)} - /> - )) - : + addons.catalog_resource.content.type === 'Err' ?
- Loading + Addons could not be loaded
- } -
- { - addAddonModalOpened ? - setAddAddonModalOpened(false) - } - }, - { - label: 'Add', - props: { - title: 'Add', - onClick: onAddButtonClicked - } - } - ]} - onCloseRequest={() => setAddAddonModalOpened(false)} - > - - - : - null - } - { - selectedAddon !== null ? - - - - : - null - } - { - sharedAddon !== null ? - setSharedAddon(null)}> - setSharedAddon(null)} - /> - - : - null + : + addons.catalog_resource.content.type === 'Loading' ? +
+ Loading +
+ : +
+ { + addons.catalog_resource.content.content + .filter((addon) => { + return search.length === 0 || + ( + (typeof addon.manifest.name === 'string' && addon.manifest.name.toLowerCase().includes(search.toLowerCase())) || + (typeof addon.manifest.description === 'string' && addon.manifest.description.toLowerCase().includes(search.toLowerCase())) + ); + }) + .map((addon, index) => ( + + )) + } +
}
+ { + addAddonModalOpen ? + + + + : + null + } + { + typeof sharedTransportUrl === 'string' ? + + + + : + null + }
); }; +Addons.propTypes = { + urlParams: PropTypes.exact({ + addonTransportUrl: PropTypes.string, + catalogId: PropTypes.string, + type: PropTypes.string + }), + queryParams: PropTypes.instanceOf(URLSearchParams) +}; + module.exports = Addons; diff --git a/src/routes/Addons/styles.less b/src/routes/Addons/styles.less index ff3b7f2c1..f7ef52dcd 100644 --- a/src/routes/Addons/styles.less +++ b/src/routes/Addons/styles.less @@ -1,3 +1,7 @@ +:import('~stremio/common/Multiselect/styles.less') { + multiselect-menu-container: menu-container; +} + .addons-container { display: flex; flex-direction: column; @@ -16,11 +20,12 @@ display: flex; flex-direction: column; - .top-bar-container { + .selectable-inputs-container { flex: none; + align-self: stretch; display: flex; flex-direction: row; - margin: 2rem; + padding: 1.5rem; overflow: visible; .add-button-container { @@ -30,7 +35,7 @@ align-items: center; height: 3rem; max-width: 15rem; - margin-right: 1rem; + margin-right: 1.5rem; padding: 0 1rem; background-color: var(--color-signal5); @@ -40,8 +45,8 @@ .icon { flex: none; - width: 1.5rem; - height: 1.5rem; + width: 1.2rem; + height: 1.2rem; margin-right: 1rem; fill: var(--color-surfacelighter); } @@ -56,12 +61,17 @@ } } - .dropdown { + .select-input-container { flex-grow: 0; flex-shrink: 1; flex-basis: 15rem; height: 3rem; - margin-right: 1rem; + margin-right: 1.5rem; + + .multiselect-menu-container { + max-height: calc(3.2rem * 7); + overflow: auto; + } } .search-bar-container { @@ -72,7 +82,6 @@ flex-direction: row; align-items: center; height: 3rem; - margin-right: 1rem; padding: 0 1rem; background-color: var(--color-backgroundlighter); cursor: text; @@ -82,7 +91,7 @@ } .icon { - display: block; + flex: none; width: 1.2rem; height: 1.2rem; margin-right: 1rem; @@ -91,7 +100,6 @@ .search-input { flex: 1; - align-self: stretch; color: var(--color-surfacelighter); &::placeholder { @@ -103,32 +111,31 @@ } } + .message-container { + flex: 1; + align-self: stretch; + padding: 0 1.5rem; + font-size: 2rem; + color: var(--color-surfacelighter); + } + .addons-list-container { flex: 1; align-self: stretch; - padding: 0 2rem; + padding: 0 1.5rem; overflow-y: auto; .addon { - width: 100%; - margin-bottom: 2rem; - } - - .message-container { - padding: 0 2rem; - font-size: 2rem; - color: var(--color-surfacelighter); + margin-bottom: 1.5rem; } } } } -.add-addon-prompt-container { - .url-content { - flex: 1; - width: 100%; - padding: 0.5rem; - font-size: 0.9rem; +.add-addon-modal-container { + .addon-url-input { + width: 25rem; + padding: 0.5rem 1rem; color: var(--color-surfacedark); border: thin solid var(--color-surface); } @@ -138,8 +145,8 @@ } } -.addon-prompt-container { - .cancel-button { - background-color: var(--color-surfacedark); +.share-modal-container { + .share-prompt-container { + width: 25rem; } } \ No newline at end of file diff --git a/src/routes/Addons/useAddonDetails.js b/src/routes/Addons/useAddonDetails.js new file mode 100644 index 000000000..afa45eceb --- /dev/null +++ b/src/routes/Addons/useAddonDetails.js @@ -0,0 +1,48 @@ +const React = require('react'); +const { useModelState } = require('stremio/common'); + +const initAddonDetailsState = () => ({ + descriptor: null +}); + +const mapAddonDetailsStateWithCtx = (addonDetails, ctx) => { + const descriptor = addonDetails.descriptor !== null && addonDetails.descriptor.content.type === 'Ready' ? + { + ...addonDetails.descriptor, + content: { + ...addonDetails.descriptor.content, + installed: ctx.content.addons.some((addon) => addon.transportUrl === addonDetails.descriptor.transport_url), + } + } + : + addonDetails.descriptor; + return { descriptor }; +}; + +const useAddonDetails = (queryParams) => { + const loadAddonDetailsAction = React.useMemo(() => { + if (queryParams.has('addon')) { + return { + action: 'Load', + args: { + load: 'AddonDetails', + args: { + transport_url: queryParams.get('addon') + } + } + }; + } else { + return { + action: 'Unload' + }; + } + }, [queryParams]); + return useModelState({ + model: 'addon_details', + action: loadAddonDetailsAction, + mapWithCtx: mapAddonDetailsStateWithCtx, + init: initAddonDetailsState, + }); +}; + +module.exports = useAddonDetails; diff --git a/src/routes/Addons/useAddons.js b/src/routes/Addons/useAddons.js index b29b23cd4..57be65b45 100644 --- a/src/routes/Addons/useAddons.js +++ b/src/routes/Addons/useAddons.js @@ -15,8 +15,28 @@ const initAddonsState = () => ({ const mapAddonsStateWithCtx = (addons, ctx) => { const selectable = addons.selectable; - const catalog_resource = addons.catalog_resource; - // TODO add MY catalogId replace catalog content if resource catalog id is MY + // TODO replace catalog content if resource catalog id is MY + const catalog_resource = addons.catalog_resource !== null && addons.catalog_resource.content.type === 'Ready' ? + { + ...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), + 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 + } + })) + } + } + : + addons.catalog_resource; return { selectable, catalog_resource }; }; diff --git a/src/routes/Addons/useSelectableInputs.js b/src/routes/Addons/useSelectableInputs.js index 8376fa846..1b1c4dd15 100644 --- a/src/routes/Addons/useSelectableInputs.js +++ b/src/routes/Addons/useSelectableInputs.js @@ -15,18 +15,6 @@ const equalWithouExtra = (request1, request2) => { }; const mapSelectableInputs = (addons) => { - const selectedCatalogRequest = addons.catalog_resource !== null ? - addons.catalog_resource.request - : - { - base: null, - path: { - resource: 'addon_catalog', - id: null, - type_name: null, - extra: [] - } - }; const catalogSelect = { title: 'Select catalog', options: addons.selectable.catalogs @@ -36,7 +24,8 @@ const mapSelectableInputs = (addons) => { })), selected: addons.selectable.catalogs .filter(({ load_request: { path: { id } } }) => { - return id === selectedCatalogRequest.path.id; + return addons.catalog_resource !== null && + addons.catalog_resource.request.path.id === id; }) .map(({ load_request }) => JSON.stringify(load_request)), onSelect: (event) => { @@ -52,7 +41,8 @@ const mapSelectableInputs = (addons) => { })), selected: addons.selectable.types .filter(({ load_request }) => { - return equalWithouExtra(load_request, selectedCatalogRequest); + return addons.catalog_resource !== null && + equalWithouExtra(addons.catalog_resource.request, load_request); }) .map(({ load_request }) => JSON.stringify(load_request)), onSelect: (event) => { diff --git a/src/routes/Addons/useSelectedAddon.js b/src/routes/Addons/useSelectedAddon.js deleted file mode 100644 index 5bffaec37..000000000 --- a/src/routes/Addons/useSelectedAddon.js +++ /dev/null @@ -1,35 +0,0 @@ -const React = require('react'); -const UrlUtils = require('url'); -const { routesRegexp, useLocationHash, useRouteActive } = require('stremio/common'); - -const useSelectedAddon = (transportUrl) => { - const [addon, setAddon] = React.useState(null); - const locationHash = useLocationHash(); - const active = useRouteActive(routesRegexp.addons.regexp); - React.useEffect(() => { - if (typeof transportUrl !== 'string') { - setAddon(null); - return; - } - - fetch(transportUrl) // TODO - .then((resp) => resp.json()) - .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 || ''); - queryParams.delete('addon'); - if ([...queryParams].length !== 0) { - window.location.replace(`#${pathname}?${queryParams.toString()}`); - } else { - window.location.replace(`#${pathname}`); - } - setAddon(null); - } - }, [active, locationHash]); - return [addon, clear, setAddon]; -}; - -module.exports = useSelectedAddon; diff --git a/src/routes/Board/Board.js b/src/routes/Board/Board.js index 33b6638ae..47f293f45 100644 --- a/src/routes/Board/Board.js +++ b/src/routes/Board/Board.js @@ -32,7 +32,7 @@ const Board = () => { {board.catalog_resources.map((catalog_resource, index) => { const title = `${catalog_resource.addon_name} - ${catalog_resource.request.path.id} ${catalog_resource.request.path.type_name}`; switch (catalog_resource.content.type) { - case 'Ready': + case 'Ready': { return ( { limit={10} /> ); - case 'Err': + } + case 'Err': { const message = `Error(${catalog_resource.content.content.type})${typeof catalog_resource.content.content.content === 'string' ? ` - ${catalog_resource.content.content.content}` : ''}`; return ( { limit={10} /> ); - case 'Loading': + } + case 'Loading': { return ( { limit={10} /> ); + } } })}
diff --git a/src/routes/Board/useItemOptions.js b/src/routes/Board/useItemOptions.js index 870c14936..b4da90921 100644 --- a/src/routes/Board/useItemOptions.js +++ b/src/routes/Board/useItemOptions.js @@ -10,7 +10,7 @@ const DISMISS_OPTION = { value: 'dismiss' }; -const onSelect = (event) => { +const onSelect = () => { // TODO {{event.value}} {{event.dataset}} }; diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index 0db14b05e..4f8358b32 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -1,4 +1,5 @@ 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'); @@ -135,4 +136,13 @@ const Discover = ({ urlParams, queryParams }) => { ); }; +Discover.propTypes = { + urlParams: PropTypes.exact({ + addonTransportUrl: PropTypes.string, + type: PropTypes.string, + catalogId: PropTypes.string + }), + queryParams: PropTypes.instanceOf(URLSearchParams) +}; + module.exports = Discover; diff --git a/src/routes/Intro/ConsentCheckbox/ConsentCheckbox.js b/src/routes/Intro/ConsentCheckbox/ConsentCheckbox.js index fe1898b4e..df05b7e60 100644 --- a/src/routes/Intro/ConsentCheckbox/ConsentCheckbox.js +++ b/src/routes/Intro/ConsentCheckbox/ConsentCheckbox.js @@ -4,16 +4,20 @@ const classnames = require('classnames'); const { Button, Checkbox } = require('stremio/common'); const styles = require('./styles'); -const ConsentCheckbox = React.forwardRef(({ className, checked, label, link, href, toggle, ...props }, ref) => { +const ConsentCheckbox = React.forwardRef(({ className, checked, label, link, href, onToggle, ...props }, ref) => { const checkboxOnClick = React.useCallback((event) => { if (typeof props.onClick === 'function') { props.onClick(event); } - if (!event.nativeEvent.togglePrevented && typeof toggle === 'function') { - toggle(event); + if (!event.nativeEvent.togglePrevented && typeof onToggle === 'function') { + onToggle({ + type: 'toggle', + reactEvent: event, + nativeEvent: event.nativeEvent + }); } - }, [toggle]); + }, [onToggle]); const linkOnClick = React.useCallback((event) => { event.nativeEvent.togglePrevented = true; }, []); @@ -42,7 +46,8 @@ ConsentCheckbox.propTypes = { label: PropTypes.string, link: PropTypes.string, href: PropTypes.string, - toggle: PropTypes.func + onToggle: PropTypes.func, + onClick: PropTypes.func }; module.exports = ConsentCheckbox; diff --git a/src/routes/Intro/CredentialsTextInput/CredentialsTextInput.js b/src/routes/Intro/CredentialsTextInput/CredentialsTextInput.js index d3c7696cf..43a2bb087 100644 --- a/src/routes/Intro/CredentialsTextInput/CredentialsTextInput.js +++ b/src/routes/Intro/CredentialsTextInput/CredentialsTextInput.js @@ -1,4 +1,5 @@ const React = require('react'); +const PropTypes = require('prop-types'); const { TextInput } = require('stremio/common'); const CredentialsTextInput = React.forwardRef((props, ref) => { @@ -23,4 +24,8 @@ const CredentialsTextInput = React.forwardRef((props, ref) => { CredentialsTextInput.displayName = 'CredentialsTextInput'; +CredentialsTextInput.propTypes = { + onKeyDown: PropTypes.func +}; + module.exports = CredentialsTextInput; diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js index 6b05700ab..3e8347db0 100644 --- a/src/routes/Intro/Intro.js +++ b/src/routes/Intro/Intro.js @@ -1,4 +1,5 @@ const React = require('react'); +const PropTypes = require('prop-types'); const classnames = require('classnames'); const Icon = require('stremio-icons/dom'); const { useRouteFocused } = require('stremio-router'); @@ -73,14 +74,17 @@ const Intro = ({ queryParams }) => { React.useEffect(() => { const onEvent = ({ event, args }) => { switch (event) { - case 'CtxActionErr': - const [_action, error] = args; + case 'CtxActionErr': { + const [, error] = args; dispatch({ type: 'error', error: error.args.message }); - case 'CtxChanged': + break; + } + case 'CtxChanged': { const state = core.getState(); if (state.ctx.content.auth !== null) { window.location.replace('#/'); } + } } }; if (routeFocused) { @@ -113,9 +117,7 @@ const Intro = ({ queryParams }) => { } }); }) - .catch((err) => { - console.error(err); - }); + .catch(() => { }); } }); }, [state.email, state.password]); @@ -256,7 +258,7 @@ const Intro = ({ queryParams }) => {
Continue with Facebook
-
We won't post anything on your behalf
+
We won't post anything on your behalf
{ link={'Terms and conditions'} href={'https://www.stremio.com/tos'} checked={state.termsAccepted} - toggle={toggleTermsAccepted} + onToggle={toggleTermsAccepted} /> { link={'Privacy Policy'} href={'https://www.stremio.com/privacy'} checked={state.privacyPolicyAccepted} - toggle={togglePrivacyPolicyAccepted} + onToggle={togglePrivacyPolicyAccepted} /> : @@ -343,4 +345,8 @@ const Intro = ({ queryParams }) => { ); }; +Intro.propTypes = { + queryParams: PropTypes.instanceOf(URLSearchParams) +}; + module.exports = Intro; diff --git a/src/routes/Library/Library.js b/src/routes/Library/Library.js index 0f3013cfb..378612678 100644 --- a/src/routes/Library/Library.js +++ b/src/routes/Library/Library.js @@ -1,4 +1,5 @@ const React = require('react'); +const PropTypes = require('prop-types'); const classnames = require('classnames'); const { Button, Multiselect, MainNavBar, MetaItem } = require('stremio/common'); const useLibrary = require('./useLibrary'); @@ -6,8 +7,8 @@ const useSelectableInputs = require('./useSelectableInputs'); const useItemOptions = require('./useItemOptions'); const styles = require('./styles'); -const Library = ({ urlParams, queryParams }) => { - const library = useLibrary(urlParams, queryParams); +const Library = ({ urlParams }) => { + const library = useLibrary(urlParams); const [typeSelect, sortPropSelect] = useSelectableInputs(library); const [options, optionOnSelect] = useItemOptions(); return ( @@ -67,6 +68,13 @@ const Library = ({ urlParams, queryParams }) => { ); -} +}; + +Library.propTypes = { + urlParams: PropTypes.exact({ + type: PropTypes.string, + sort: PropTypes.string + }) +}; module.exports = Library; diff --git a/src/routes/Library/useItemOptions.js b/src/routes/Library/useItemOptions.js index 870c14936..b4da90921 100644 --- a/src/routes/Library/useItemOptions.js +++ b/src/routes/Library/useItemOptions.js @@ -10,7 +10,7 @@ const DISMISS_OPTION = { value: 'dismiss' }; -const onSelect = (event) => { +const onSelect = () => { // TODO {{event.value}} {{event.dataset}} }; diff --git a/src/routes/Library/useLibrary.js b/src/routes/Library/useLibrary.js index 4acaa6e78..69c63f56e 100644 --- a/src/routes/Library/useLibrary.js +++ b/src/routes/Library/useLibrary.js @@ -46,20 +46,17 @@ const onNewLibraryState = (library) => { } }; -const useLibrary = (urlParams, queryParams) => { +const useLibrary = (urlParams) => { const { core } = useServices(); const loadLibraryAction = React.useMemo(() => { - if (typeof urlParams.type === 'string') { + if (typeof urlParams.type === 'string' && typeof urlParams.sort === 'string') { return { action: 'Load', args: { load: 'LibraryFiltered', args: { type_name: urlParams.type, - sort_prop: queryParams.has('sort_prop') ? - queryParams.get('sort_prop') - : - null + sort_prop: urlParams.sort } } }; @@ -82,7 +79,7 @@ const useLibrary = (urlParams, queryParams) => { }; } } - }, [urlParams, queryParams]); + }, [urlParams]); return useModelState({ model: 'library', action: loadLibraryAction, @@ -90,6 +87,6 @@ const useLibrary = (urlParams, queryParams) => { init: initLibraryState, onNewState: onNewLibraryState }); -} +}; module.exports = useLibrary; diff --git a/src/routes/Library/useSelectableInputs.js b/src/routes/Library/useSelectableInputs.js index 601ce9731..645db1457 100644 --- a/src/routes/Library/useSelectableInputs.js +++ b/src/routes/Library/useSelectableInputs.js @@ -16,13 +16,7 @@ const mapSelectableInputs = (library) => { options: library.type_names .map((type) => ({ label: type, value: type })), onSelect: (event) => { - const queryParams = new URLSearchParams( - library.selected !== null ? - [['sort_prop', library.selected.sort_prop]] - : - [] - ); - window.location.replace(`#/library/${encodeURIComponent(event.value)}?${queryParams.toString()}`); + window.location.replace(`#/library/${encodeURIComponent(event.value)}/${encodeURIComponent(library.selected.sort_prop)}`); } }; const sortPropSelect = { @@ -33,9 +27,8 @@ const mapSelectableInputs = (library) => { [], options: SORT_PROP_OPTIONS, onSelect: (event) => { - const queryParams = new URLSearchParams([['sort_prop', event.value]]); if (library.selected !== null) { - window.location.replace(`#/library/${encodeURIComponent(library.selected.type_name)}?${queryParams.toString()}`); + window.location.replace(`#/library/${encodeURIComponent(library.selected.type_name)}/${encodeURIComponent(event.value)}`); } } }; diff --git a/src/routes/MetaDetails/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js index a19227a6c..3cd804345 100644 --- a/src/routes/MetaDetails/MetaDetails.js +++ b/src/routes/MetaDetails/MetaDetails.js @@ -1,4 +1,5 @@ const React = require('react'); +const PropTypes = require('prop-types'); const { NavBar, MetaPreview, useInLibrary } = require('stremio/common'); const VideosList = require('./VideosList'); const StreamsList = require('./StreamsList'); @@ -11,7 +12,20 @@ const MetaDetails = ({ urlParams }) => { const [metaResourceRef, metaResources, selectedMetaResource] = useSelectableResource(metaDetails.selected.meta_resource_ref, metaDetails.meta_resources); const streamsResourceRef = metaDetails.selected.streams_resource_ref; const streamsResources = metaDetails.streams_resources; - const [inLibrary, , , toggleInLibrary] = useInLibrary(metaResourceRef !== null ? metaResourceRef.id : null); + const metaItem = React.useMemo(() => { + return selectedMetaResource !== null ? + selectedMetaResource.content.content + : + metaResourceRef !== null ? + { + id: metaResourceRef.id, + type: metaResourceRef.type_name, + name: '' + } + : + null; + }, [metaResourceRef, selectedMetaResource]); + const [inLibrary, toggleInLibrary] = useInLibrary(metaItem); return (
{ ); }; +MetaDetails.propTypes = { + urlParams: PropTypes.exact({ + type: PropTypes.string, + id: PropTypes.string, + videoId: PropTypes.string + }) +}; + module.exports = MetaDetails; diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index 01163b29a..2e0f8de86 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -44,7 +44,7 @@ const StreamsList = ({ className, streamsResources }) => {
); -} +}; StreamsList.propTypes = { className: PropTypes.string, diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js index cfe24abe3..d830d710d 100644 --- a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js +++ b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js @@ -16,11 +16,9 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => { const selected = React.useMemo(() => { return [String(season)]; }, [season]); - const renderMultiselectLabelContent = React.useMemo(() => { - return () => ( -
Season {season}
- ); - }, [season]); + const renderMultiselectLabelContent = React.useMemo(() => () => ( +
Season {season}
+ ), [season]); const prevNextButtonOnClick = React.useCallback((event) => { if (typeof onSelect === 'function') { const seasonIndex = seasons.indexOf(season); diff --git a/src/routes/MetaDetails/VideosList/Video/Video.js b/src/routes/MetaDetails/VideosList/Video/Video.js index 2bcd22f27..4b13e30e6 100644 --- a/src/routes/MetaDetails/VideosList/Video/Video.js +++ b/src/routes/MetaDetails/VideosList/Video/Video.js @@ -6,7 +6,7 @@ const Icon = require('stremio-icons/dom'); const VideoPlaceholder = require('./VideoPlaceholder'); const styles = require('./styles'); -const Video = ({ className, title, thumbnail, episode, released, upcoming, watched, progress, ...props }) => { +const Video = ({ className, id, title, thumbnail, episode, released, upcoming, watched, progress, ...props }) => { return (