diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59bcd5ee2..ba8e878a3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,5 +24,5 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./build - destination_dir: ${{ github.ref_name != 'development' && github.ref_name || '' }} + destination_dir: ${{ github.ref_name }} allow_empty_commit: true diff --git a/package-lock.json b/package-lock.json index a2aaa3b51..482af7ef4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@babel/runtime": "7.16.0", "@sentry/browser": "6.13.3", "@stremio/stremio-colors": "5.0.1", - "@stremio/stremio-core-web": "0.44.18", + "@stremio/stremio-core-web": "0.44.20", "@stremio/stremio-icons": "4.0.0", "@stremio/stremio-video": "0.0.24", "a-color-picker": "1.2.1", @@ -37,7 +37,8 @@ "react-is": "18.2.0", "spatial-navigation-polyfill": "https://github.com/Stremio/spatial-navigation.git#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", "stremio-translations": "https://github.com/Stremio/stremio-translations.git#92675658de92113c5888cf5e57003e468e8b8c9c", - "url": "0.11.0" + "url": "0.11.0", + "use-long-press": "^3.1.5" }, "devDependencies": { "@babel/core": "7.16.0", @@ -2702,9 +2703,9 @@ "integrity": "sha512-Dt3PYmy1DZ473QNs99KYXVWQPHtpIl37VUY0+gCEvvuCqE1fRrZIJtZ9KbysUKonvO7WwdQDztgcW0iGoc1dEA==" }, "node_modules/@stremio/stremio-core-web": { - "version": "0.44.18", - "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.18.tgz", - "integrity": "sha512-g89XSIfLIsvN+FIscvBP9t5ywLP1uhGT9jED97e37ScXKCVedOL9ibnn1DJIeUj8U+ezJdbHFx4zWpnMfJdU2A==", + "version": "0.44.20", + "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.20.tgz", + "integrity": "sha512-dcqs9svqe9iQHDIyIr7ML42H5Oa2GNjIy3Ngp/TSMPd0UIQ/kJ4ZU/qoZk17r/McI20FEzYIdGFdCcif7c/n9g==", "dependencies": { "@babel/runtime": "7.16.0" } @@ -13664,6 +13665,14 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, + "node_modules/use-long-press": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/use-long-press/-/use-long-press-3.1.5.tgz", + "integrity": "sha512-bnwk2SlvLLpeJPkNYSGkc59q5YNV9V/fLDkSOAF2p7Xt0zw3iYHEmgEGkNYkK7zEIEyRFi5CczKsT7MN99UzVQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/use-sidecar": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", @@ -16795,9 +16804,9 @@ "integrity": "sha512-Dt3PYmy1DZ473QNs99KYXVWQPHtpIl37VUY0+gCEvvuCqE1fRrZIJtZ9KbysUKonvO7WwdQDztgcW0iGoc1dEA==" }, "@stremio/stremio-core-web": { - "version": "0.44.18", - "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.18.tgz", - "integrity": "sha512-g89XSIfLIsvN+FIscvBP9t5ywLP1uhGT9jED97e37ScXKCVedOL9ibnn1DJIeUj8U+ezJdbHFx4zWpnMfJdU2A==", + "version": "0.44.20", + "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.20.tgz", + "integrity": "sha512-dcqs9svqe9iQHDIyIr7ML42H5Oa2GNjIy3Ngp/TSMPd0UIQ/kJ4ZU/qoZk17r/McI20FEzYIdGFdCcif7c/n9g==", "requires": { "@babel/runtime": "7.16.0" } @@ -25104,6 +25113,12 @@ } } }, + "use-long-press": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/use-long-press/-/use-long-press-3.1.5.tgz", + "integrity": "sha512-bnwk2SlvLLpeJPkNYSGkc59q5YNV9V/fLDkSOAF2p7Xt0zw3iYHEmgEGkNYkK7zEIEyRFi5CczKsT7MN99UzVQ==", + "requires": {} + }, "use-sidecar": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", diff --git a/package.json b/package.json index 771453a81..0f3efc6c6 100755 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@babel/runtime": "7.16.0", "@sentry/browser": "6.13.3", "@stremio/stremio-colors": "5.0.1", - "@stremio/stremio-core-web": "0.44.18", + "@stremio/stremio-core-web": "0.44.20", "@stremio/stremio-icons": "4.0.0", "@stremio/stremio-video": "0.0.24", "a-color-picker": "1.2.1", @@ -40,7 +40,8 @@ "react-is": "18.2.0", "spatial-navigation-polyfill": "https://github.com/Stremio/spatial-navigation.git#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", "stremio-translations": "https://github.com/Stremio/stremio-translations.git#92675658de92113c5888cf5e57003e468e8b8c9c", - "url": "0.11.0" + "url": "0.11.0", + "use-long-press": "^3.1.5" }, "devDependencies": { "@babel/core": "7.16.0", diff --git a/src/App/App.js b/src/App/App.js index 5be4754b7..6a685802a 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -6,14 +6,17 @@ const { useTranslation } = require('react-i18next'); const { Router } = require('stremio-router'); const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services'); const { NotFound } = require('stremio/routes'); -const { ToastProvider, CONSTANTS } = require('stremio/common'); +const { ToastProvider, CONSTANTS, withCoreSuspender } = require('stremio/common'); const ServicesToaster = require('./ServicesToaster'); const DeepLinkHandler = require('./DeepLinkHandler'); const DefaultSettingsHandler = require('./DefaultSettingsHandler'); const ErrorDialog = require('./ErrorDialog'); +const withProtectedRoutes = require('./withProtectedRoutes'); const routerViewsConfig = require('./routerViewsConfig'); const styles = require('./styles'); +const RouterWithProtectedRoutes = withCoreSuspender(withProtectedRoutes(Router)); + const App = () => { const { i18n } = useTranslation(); const onPathNotMatch = React.useCallback(() => { @@ -154,7 +157,7 @@ const App = () => { - { + return function withProtectedRoutes(props) { + const profile = useProfile(); + const previousAuthRef = React.useRef(profile.auth); + React.useEffect(() => { + if (previousAuthRef.current !== null && profile.auth === null) { + window.location = '#/intro'; + } + previousAuthRef.current = profile.auth; + }, [profile]); + const onRouteChange = React.useCallback((routeConfig) => { + if (profile.auth !== null && routeConfig.component === Intro) { + window.location.replace('#/'); + return true; + } + }, [profile]); + return ( + + ); + }; +}; + +module.exports = withProtectedRoutes; diff --git a/src/common/Button/Button.js b/src/common/Button/Button.js index 7235860a8..9d5ef7c1e 100644 --- a/src/common/Button/Button.js +++ b/src/common/Button/Button.js @@ -4,15 +4,20 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const styles = require('./styles'); +const { useLongPress } = require('use-long-press'); -const Button = React.forwardRef(({ className, href, disabled, children, ...props }, ref) => { +const Button = React.forwardRef(({ className, href, disabled, children, onLongPress, ...props }, ref) => { + const longPress = useLongPress(onLongPress, { detect: 'pointer' }); const onKeyDown = React.useCallback((event) => { if (typeof props.onKeyDown === 'function') { props.onKeyDown(event); } - if (event.key === 'Enter' && !event.nativeEvent.buttonClickPrevented) { - event.currentTarget.click(); + if (event.key === 'Enter') { + event.preventDefault(); + if (!event.nativeEvent.buttonClickPrevented) { + event.currentTarget.click(); + } } }, [props.onKeyDown]); const onMouseDown = React.useCallback((event) => { @@ -36,7 +41,8 @@ const Button = React.forwardRef(({ className, href, disabled, children, ...props className: classnames(className, styles['button-container'], { 'disabled': disabled }), href, onKeyDown, - onMouseDown + onMouseDown, + ...longPress() }, children ); @@ -50,7 +56,8 @@ Button.propTypes = { disabled: PropTypes.bool, children: PropTypes.node, onKeyDown: PropTypes.func, - onMouseDown: PropTypes.func + onMouseDown: PropTypes.func, + onLongPress: PropTypes.func, }; module.exports = Button; diff --git a/src/common/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js b/src/common/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js index 64cc16efe..459e39ee2 100644 --- a/src/common/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js +++ b/src/common/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js @@ -53,7 +53,7 @@ const NavMenuContent = ({ onClick }) => {
{profile.auth === null ? t('ANONYMOUS_USER') : profile.auth.user.email}
- diff --git a/src/common/Popup/Popup.js b/src/common/Popup/Popup.js index 7c694e6d5..aad02bdf3 100644 --- a/src/common/Popup/Popup.js +++ b/src/common/Popup/Popup.js @@ -47,16 +47,23 @@ const Popup = ({ open, direction, renderLabel, renderMenu, dataset, onCloseReque onCloseRequest(closeEvent); } break; + case 'pointerdown': + if (event.target !== document.documentElement && !labelRef.current.contains(event.target)) { + onCloseRequest(closeEvent); + } + break; } } }; if (routeFocused && open) { window.addEventListener('keydown', onCloseEvent); window.addEventListener('mousedown', onCloseEvent); + window.addEventListener('pointerdown', onCloseEvent); } return () => { window.removeEventListener('keydown', onCloseEvent); window.removeEventListener('mousedown', onCloseEvent); + window.removeEventListener('pointerdown', onCloseEvent); }; }, [routeFocused, open, onCloseRequest, dataset]); React.useLayoutEffect(() => { diff --git a/src/common/Popup/styles.less b/src/common/Popup/styles.less index d906f31ee..902fd550f 100644 --- a/src/common/Popup/styles.less +++ b/src/common/Popup/styles.less @@ -3,6 +3,10 @@ @import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; .label-container { + // IOS specific + // prevents showing the default context-menu when long pressing an anchor in safari. + -webkit-touch-callout: none !important; + position: relative; overflow: visible; diff --git a/src/router/Router/Router.js b/src/router/Router/Router.js index d442827c8..db4c9e957 100644 --- a/src/router/Router/Router.js +++ b/src/router/Router/Router.js @@ -11,7 +11,7 @@ const Route = require('../Route'); const routeConfigForPath = require('./routeConfigForPath'); const urlParamsForPath = require('./urlParamsForPath'); -const Router = ({ className, onPathNotMatch, ...props }) => { +const Router = ({ className, onPathNotMatch, onRouteChange, ...props }) => { const viewsConfig = React.useMemo(() => props.viewsConfig, []); const [views, setViews] = React.useState(() => { return Array(viewsConfig.length).fill(null); @@ -42,37 +42,40 @@ const Router = ({ className, onPathNotMatch, ...props }) => { const urlParams = urlParamsForPath(routeConfig, typeof pathname === 'string' ? pathname : ''); const routeViewIndex = viewsConfig.findIndex((vc) => vc.includes(routeConfig)); const routeIndex = viewsConfig[routeViewIndex].findIndex((rc) => rc === routeConfig); - setViews((views) => { - return views - .slice(0, viewsConfig.length) - .map((view, index) => { - if (index < routeViewIndex) { - return view; - } else if (index === routeViewIndex) { - return { - key: `${routeViewIndex}${routeIndex}`, - component: routeConfig.component, - urlParams: view !== null && isEqual(view.urlParams, urlParams) ? - view.urlParams - : - urlParams, - queryParams: view !== null && isEqual(Array.from(view.queryParams.entries()), Array.from(queryParams.entries())) ? - view.queryParams - : - queryParams - }; - } else { - return null; - } - }); - }); + const handled = typeof onRouteChange === 'function' && onRouteChange(routeConfig, urlParams, queryParams); + if (!handled) { + setViews((views) => { + return views + .slice(0, viewsConfig.length) + .map((view, index) => { + if (index < routeViewIndex) { + return view; + } else if (index === routeViewIndex) { + return { + key: `${routeViewIndex}${routeIndex}`, + component: routeConfig.component, + urlParams: view !== null && isEqual(view.urlParams, urlParams) ? + view.urlParams + : + urlParams, + queryParams: view !== null && isEqual(Array.from(view.queryParams.entries()), Array.from(queryParams.entries())) ? + view.queryParams + : + queryParams + }; + } else { + return null; + } + }); + }); + } }; window.addEventListener('hashchange', onLocationHashChange); onLocationHashChange(); return () => { window.removeEventListener('hashchange', onLocationHashChange); }; - }, [onPathNotMatch]); + }, [onPathNotMatch, onRouteChange]); return (
{ @@ -93,6 +96,7 @@ const Router = ({ className, onPathNotMatch, ...props }) => { Router.propTypes = { className: PropTypes.string, onPathNotMatch: PropTypes.func, + onRouteChange: PropTypes.func, viewsConfig: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.exact({ regexp: PropTypes.instanceOf(RegExp).isRequired, urlParamsNames: PropTypes.arrayOf(PropTypes.string).isRequired, diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index aadd947e0..a7fa51ba6 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -19,6 +19,12 @@ const Discover = ({ urlParams, queryParams }) => { const [inputsModalOpen, openInputsModal, closeInputsModal] = useBinaryState(false); const [addonModalOpen, openAddonModal, closeAddonModal] = useBinaryState(false); const [selectedMetaItemIndex, setSelectedMetaItemIndex] = React.useState(0); + const metasContainerRef = React.useRef(); + React.useEffect(() => { + if (discover.catalog?.content.type === 'Loading') { + metasContainerRef.current.scrollTop = 0; + } + }, [discover.catalog]); const selectedMetaItem = React.useMemo(() => { return discover.catalog !== null && discover.catalog.content.type === 'Ready' && @@ -122,7 +128,7 @@ const Discover = ({ urlParams, queryParams }) => {
: discover.catalog.content.type === 'Loading' ? -
+
{Array(CONSTANTS.CATALOG_PAGE_SIZE).fill(null).map((_, index) => (
@@ -133,7 +139,7 @@ const Discover = ({ urlParams, queryParams }) => { ))}
: -
+
{discover.catalog.content.content.map((metaItem, index) => ( { released={selectedMetaItem.released} description={selectedMetaItem.description} deepLinks={selectedMetaItem.deepLinks} + links={selectedMetaItem.links} trailerStreams={selectedMetaItem.trailerStreams} inLibrary={selectedMetaItem.inLibrary} toggleInLibrary={selectedMetaItem.inLibrary ? removeFromLibrary : addToLibrary} diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js index 11da3a39a..92dc3db5d 100644 --- a/src/routes/Intro/Intro.js +++ b/src/routes/Intro/Intro.js @@ -144,12 +144,6 @@ const Intro = ({ queryParams }) => { dispatch({ type: 'error', error: 'You must accept the Terms of Service' }); return; } - core.transport.dispatch({ - action: 'Ctx', - args: { - action: 'Logout' - } - }); window.location = '#/'; }, [state.termsAccepted]); const signup = React.useCallback(() => { diff --git a/src/routes/MetaDetails/VideosList/Video/Video.js b/src/routes/MetaDetails/VideosList/Video/Video.js index b36bb5aa8..fd252d5ce 100644 --- a/src/routes/MetaDetails/VideosList/Video/Video.js +++ b/src/routes/MetaDetails/VideosList/Video/Video.js @@ -3,6 +3,7 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); +const { t } = require('i18next'); const { useServices } = require('stremio/services'); const { useRouteFocused } = require('stremio-router'); const Icon = require('@stremio/stremio-icons/dom'); @@ -14,27 +15,36 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w const { core } = useServices(); const routeFocused = useRouteFocused(); const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); - const popupLabelOnClick = React.useCallback((event) => { - if (!event.nativeEvent.togglePopupPrevented && event.nativeEvent.ctrlKey) { - event.preventDefault(); - toggleMenu(); + const popupLabelOnMouseUp = React.useCallback((event) => { + if (!event.nativeEvent.togglePopupPrevented) { + if (event.nativeEvent.ctrlKey || event.nativeEvent.button === 2) { + event.preventDefault(); + toggleMenu(); + } } }, []); - const popupLabelOnKeyDown = React.useCallback((event) => { - event.nativeEvent.buttonClickPrevented = true; - }, []); const popupLabelOnContextMenu = React.useCallback((event) => { if (!event.nativeEvent.togglePopupPrevented && !event.nativeEvent.ctrlKey) { event.preventDefault(); + } + }, [toggleMenu]); + const popupLabelOnLongPress = React.useCallback((event) => { + if (event.nativeEvent.pointerType !== 'mouse' && !event.nativeEvent.togglePopupPrevented) { toggleMenu(); } }, [toggleMenu]); + const popupMenuOnPointerDown = React.useCallback((event) => { + event.nativeEvent.togglePopupPrevented = true; + }, []); const popupMenuOnContextMenu = React.useCallback((event) => { event.nativeEvent.togglePopupPrevented = true; }, []); const popupMenuOnClick = React.useCallback((event) => { event.nativeEvent.togglePopupPrevented = true; }, []); + const popupMenuOnKeyDown = React.useCallback((event) => { + event.nativeEvent.buttonClickPrevented = true; + }, []); const toggleWatchedOnClick = React.useCallback((event) => { event.preventDefault(); closeMenu(); @@ -132,12 +142,12 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w }, []); const renderMenu = React.useMemo(() => function renderMenu() { return ( -
+
); @@ -161,8 +171,8 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w scheduled={scheduled} href={href} {...props} - onClick={popupLabelOnClick} - onKeyDown={popupLabelOnKeyDown} + onMouseUp={popupLabelOnMouseUp} + onLongPress={popupLabelOnLongPress} onContextMenu={popupLabelOnContextMenu} open={menuOpen} onCloseRequest={closeMenu} diff --git a/src/routes/MetaDetails/VideosList/Video/styles.less b/src/routes/MetaDetails/VideosList/Video/styles.less index 67bb6e17d..0c63b960d 100644 --- a/src/routes/MetaDetails/VideosList/Video/styles.less +++ b/src/routes/MetaDetails/VideosList/Video/styles.less @@ -27,6 +27,7 @@ flex: none; .thumbnail { + pointer-events: none; display: block; width: 7.5rem; height: 5rem; diff --git a/src/routes/MetaDetails/styles.less b/src/routes/MetaDetails/styles.less index db51e4a2d..a906c7a99 100644 --- a/src/routes/MetaDetails/styles.less +++ b/src/routes/MetaDetails/styles.less @@ -57,6 +57,7 @@ } .background-image { + pointer-events: none; display: block; width: 100%; height: 100%; diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index d7d6ddcd2..69da9ebbc 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -585,11 +585,24 @@ const Player = ({ urlParams, queryParams }) => { } } }; + const onWheel = ({ deltaY }) => { + if (deltaY > 0) { + if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && videoState.volume !== null) { + onVolumeChangeRequested(videoState.volume - 5); + } + } else { + if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && videoState.volume !== null) { + onVolumeChangeRequested(videoState.volume + 5); + } + } + }; if (routeFocused) { window.addEventListener('keydown', onKeyDown); + window.addEventListener('wheel', onWheel); } return () => { window.removeEventListener('keydown', onKeyDown); + window.removeEventListener('wheel', onWheel); }; }, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, videosMenuOpen, speedMenuOpen, optionsMenuOpen, statisticsMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, videoState.playbackSpeed, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu, toggleStatisticsMenu]); React.useLayoutEffect(() => { diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 9a001594b..cd3c8bab1 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -223,7 +223,7 @@ const Settings = () => {
{ profile.auth !== null ? - : @@ -237,7 +237,7 @@ const Settings = () => { { profile.auth === null ?
-