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 a9c0e5fa3..9579d9c06 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.19", + "@stremio/stremio-core-web": "0.44.21", "@stremio/stremio-icons": "4.0.0", "@stremio/stremio-video": "0.0.24", "a-color-picker": "1.2.1", @@ -2703,9 +2703,9 @@ "integrity": "sha512-Dt3PYmy1DZ473QNs99KYXVWQPHtpIl37VUY0+gCEvvuCqE1fRrZIJtZ9KbysUKonvO7WwdQDztgcW0iGoc1dEA==" }, "node_modules/@stremio/stremio-core-web": { - "version": "0.44.19", - "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.19.tgz", - "integrity": "sha512-5yQOD5dxKbyTjDNj2b2tHpo/aL6i21FXF2AGiEgnfUao+v92JmTwIpC10NSp/I0Jg7NoKA6ZY6LzttW3hrNajQ==", + "version": "0.44.21", + "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.21.tgz", + "integrity": "sha512-xVCE9A/ZWLJ8un1x6VYSDY4fYclxq4rV98UIgUcc9SZlleHOoB92kqy5TIXhQ6v+Ym9EX9OU2uLBv+d2fi6KHA==", "dependencies": { "@babel/runtime": "7.16.0" } @@ -16804,9 +16804,9 @@ "integrity": "sha512-Dt3PYmy1DZ473QNs99KYXVWQPHtpIl37VUY0+gCEvvuCqE1fRrZIJtZ9KbysUKonvO7WwdQDztgcW0iGoc1dEA==" }, "@stremio/stremio-core-web": { - "version": "0.44.19", - "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.19.tgz", - "integrity": "sha512-5yQOD5dxKbyTjDNj2b2tHpo/aL6i21FXF2AGiEgnfUao+v92JmTwIpC10NSp/I0Jg7NoKA6ZY6LzttW3hrNajQ==", + "version": "0.44.21", + "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.21.tgz", + "integrity": "sha512-xVCE9A/ZWLJ8un1x6VYSDY4fYclxq4rV98UIgUcc9SZlleHOoB92kqy5TIXhQ6v+Ym9EX9OU2uLBv+d2fi6KHA==", "requires": { "@babel/runtime": "7.16.0" } diff --git a/package.json b/package.json index 73b806ed6..dfa055820 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.19", + "@stremio/stremio-core-web": "0.44.21", "@stremio/stremio-icons": "4.0.0", "@stremio/stremio-video": "0.0.24", "a-color-picker": "1.2.1", diff --git a/src/App/App.js b/src/App/App.js index 77a4b5824..be59767b7 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -6,13 +6,16 @@ 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 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(() => { @@ -126,6 +129,12 @@ const App = () => { action: 'SyncLibraryWithAPI' } }); + services.core.transport.dispatch({ + action: 'Ctx', + args: { + action: 'PullNotifications' + } + }); }; if (services.core.active) { onWindowFocus(); @@ -152,7 +161,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/LibItem/LibItem.js b/src/common/LibItem/LibItem.js index 8cc174266..431359071 100644 --- a/src/common/LibItem/LibItem.js +++ b/src/common/LibItem/LibItem.js @@ -4,6 +4,7 @@ const React = require('react'); const { useServices } = require('stremio/services'); const PropTypes = require('prop-types'); const MetaItem = require('stremio/common/MetaItem'); +const useNotifications = require('stremio/common/useNotifications'); const { t } = require('i18next'); const OPTIONS = [ @@ -15,6 +16,11 @@ const OPTIONS = [ const LibItem = ({ _id, removable, ...props }) => { const { core } = useServices(); + const notifications = useNotifications(); + const newVideos = React.useMemo(() => { + const count = notifications.items?.[_id]?.length ?? 0; + return Math.min(Math.max(count, 0), 99); + }, [_id, notifications.items]); const options = React.useMemo(() => { return OPTIONS .filter(({ value }) => { @@ -68,6 +74,13 @@ const LibItem = ({ _id, removable, ...props }) => { args: _id } }); + core.transport.dispatch({ + action: 'Ctx', + args: { + action: 'DismissNotificationItem', + args: _id + } + }); } break; @@ -91,6 +104,7 @@ const LibItem = ({ _id, removable, ...props }) => { return ( diff --git a/src/common/MetaItem/MetaItem.js b/src/common/MetaItem/MetaItem.js index 39a140cd3..d0c12df92 100644 --- a/src/common/MetaItem/MetaItem.js +++ b/src/common/MetaItem/MetaItem.js @@ -13,7 +13,7 @@ const useBinaryState = require('stremio/common/useBinaryState'); const { ICON_FOR_TYPE } = require('stremio/common/CONSTANTS'); const styles = require('./styles'); -const MetaItem = React.memo(({ className, type, name, poster, posterShape, playIcon, progress, options, deepLinks, dataset, optionOnSelect, ...props }) => { +const MetaItem = React.memo(({ className, type, name, poster, posterShape, playIcon, progress, newVideos, options, deepLinks, dataset, optionOnSelect, ...props }) => { const [menuOpen, onMenuOpen, onMenuClose] = useBinaryState(false); const href = React.useMemo(() => { return deepLinks ? @@ -89,6 +89,18 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, playI : null } + { + newVideos > 0 ? +
+
+
+
+ +{newVideos} +
+
+ : + null + }
{ (typeof name === 'string' && name.length > 0) || (Array.isArray(options) && options.length > 0) ? @@ -129,6 +141,7 @@ MetaItem.propTypes = { posterShape: PropTypes.oneOf(['poster', 'landscape', 'square']), playIcon: PropTypes.bool, progress: PropTypes.number, + newVideos: PropTypes.number, options: PropTypes.array, deepLinks: PropTypes.shape({ metaDetailsVideos: PropTypes.string, diff --git a/src/common/MetaItem/styles.less b/src/common/MetaItem/styles.less index 17ee1f8cf..75f09b555 100644 --- a/src/common/MetaItem/styles.less +++ b/src/common/MetaItem/styles.less @@ -118,6 +118,45 @@ background-color: @color-primaryvariant1; } } + + .new-videos { + z-index: -1; + position: absolute; + top: 0; + right: 0; + overflow: visible; + + .layer { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + height: 1.6rem; + width: 2.75rem; + border-radius: 0.25rem; + font-size: 1rem; + font-weight: 600; + color: @color-background-dark2-90; + + &:nth-child(1) { + top: 0.5rem; + right: 0.5rem; + background-color: @color-surface-light5-40; + } + + &:nth-child(2) { + top: 0.75rem; + right: 0.75rem; + background-color: @color-surface-light5-60; + } + + &:nth-child(3) { + top: 1rem; + right: 1rem; + background-color: @color-surface-light5; + } + } + } } .title-bar-container { 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/index.js b/src/common/index.js index 9dbc551a6..f267be575 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -36,6 +36,7 @@ const useBinaryState = require('./useBinaryState'); const useFullscreen = require('./useFullscreen'); const useLiveRef = require('./useLiveRef'); const useModelState = require('./useModelState'); +const useNotifications = require('./useNotifications'); const useOnScrollToBottom = require('./useOnScrollToBottom'); const useProfile = require('./useProfile'); const useStreamingServer = require('./useStreamingServer'); @@ -83,6 +84,7 @@ module.exports = { useFullscreen, useLiveRef, useModelState, + useNotifications, useOnScrollToBottom, useProfile, useStreamingServer, diff --git a/src/common/useNotifications.d.ts b/src/common/useNotifications.d.ts new file mode 100644 index 000000000..7a6943654 --- /dev/null +++ b/src/common/useNotifications.d.ts @@ -0,0 +1,2 @@ +declare const useNotifcations: () => Notifications; +export = useNotifcations; \ No newline at end of file diff --git a/src/common/useNotifications.js b/src/common/useNotifications.js new file mode 100644 index 000000000..b0b7e7858 --- /dev/null +++ b/src/common/useNotifications.js @@ -0,0 +1,11 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +const useModelState = require('stremio/common/useModelState'); + +const map = (ctx) => ctx.notifications; + +const useNotifications = () => { + return useModelState({ model: 'ctx', map }); +}; + +module.exports = useNotifications; 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/Board/Board.js b/src/routes/Board/Board.js index c8043b0a2..64afb7900 100644 --- a/src/routes/Board/Board.js +++ b/src/routes/Board/Board.js @@ -16,7 +16,7 @@ const Board = () => { const streamingServer = useStreamingServer(); const continueWatchingPreview = useContinueWatchingPreview(); const [board, loadBoardRows] = useBoard(); - const boardCatalogsOffset = continueWatchingPreview.libraryItems.length > 0 ? 1 : 0; + const boardCatalogsOffset = continueWatchingPreview.items.length > 0 ? 1 : 0; const scrollContainerRef = React.useRef(); const onVisibleRangeChange = React.useCallback(() => { const range = getVisibleChildrenRange(scrollContainerRef.current); @@ -41,11 +41,11 @@ const Board = () => {
{ - continueWatchingPreview.libraryItems.length > 0 ? + continueWatchingPreview.items.length > 0 ? 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/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js index 681351473..084ca3f58 100644 --- a/src/routes/MetaDetails/MetaDetails.js +++ b/src/routes/MetaDetails/MetaDetails.js @@ -61,6 +61,17 @@ const MetaDetails = ({ urlParams, queryParams }) => { } }); }, [metaDetails]); + const toggleNotifications = React.useCallback(() => { + if (metaDetails.libraryItem) { + core.transport.dispatch({ + action: 'Ctx', + args: { + action: 'ToggleLibraryItemNotifications', + args: [metaDetails.libraryItem._id, !metaDetails.libraryItem.state.noNotif], + } + }); + } + }, [metaDetails.libraryItem]); const seasonOnSelect = React.useCallback((event) => { setSeason(event.value); }, [setSeason]); @@ -157,8 +168,10 @@ const MetaDetails = ({ urlParams, queryParams }) => { : null diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/styles.less b/src/routes/MetaDetails/VideosList/SeasonsBar/styles.less index 4c044ea15..42326acfc 100644 --- a/src/routes/MetaDetails/VideosList/SeasonsBar/styles.less +++ b/src/routes/MetaDetails/VideosList/SeasonsBar/styles.less @@ -13,7 +13,6 @@ display: flex; flex-direction: row; justify-content: space-between; - padding: 1rem; overflow: visible; .prev-season-button, .next-season-button { diff --git a/src/routes/MetaDetails/VideosList/Video/Video.js b/src/routes/MetaDetails/VideosList/Video/Video.js index 6ce900537..fd252d5ce 100644 --- a/src/routes/MetaDetails/VideosList/Video/Video.js +++ b/src/routes/MetaDetails/VideosList/Video/Video.js @@ -15,18 +15,17 @@ 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 popupLabelOnContextMenu = React.useCallback((event) => { if (!event.nativeEvent.togglePopupPrevented && !event.nativeEvent.ctrlKey) { event.preventDefault(); - if (event.nativeEvent.pointerType === 'mouse') { - toggleMenu(); - } } }, [toggleMenu]); const popupLabelOnLongPress = React.useCallback((event) => { @@ -172,7 +171,7 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w scheduled={scheduled} href={href} {...props} - onClick={popupLabelOnClick} + onMouseUp={popupLabelOnMouseUp} onLongPress={popupLabelOnLongPress} onContextMenu={popupLabelOnContextMenu} open={menuOpen} diff --git a/src/routes/MetaDetails/VideosList/VideosList.js b/src/routes/MetaDetails/VideosList/VideosList.js index b1928eba8..e344c6480 100644 --- a/src/routes/MetaDetails/VideosList/VideosList.js +++ b/src/routes/MetaDetails/VideosList/VideosList.js @@ -4,13 +4,15 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { t } = require('i18next'); -const Image = require('stremio/common/Image'); -const SearchBar = require('stremio/common/SearchBar'); +const { Image, SearchBar, Checkbox } = require('stremio/common'); const SeasonsBar = require('./SeasonsBar'); const Video = require('./Video'); const styles = require('./styles'); -const VideosList = ({ className, metaItem, season, seasonOnSelect }) => { +const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect, toggleNotifications }) => { + const showNotificationsToggle = React.useMemo(() => { + return metaItem?.content?.content?.inLibrary && metaItem?.content?.content?.videos?.length; + }, [metaItem]); const videos = React.useMemo(() => { return metaItem && metaItem.content.type === 'Ready' ? metaItem.content.content.videos @@ -80,6 +82,14 @@ const VideosList = ({ className, metaItem, season, seasonOnSelect }) => {
: + { + showNotificationsToggle && libraryItem ? + + {t('DETAIL_RECEIVE_NOTIF_SERIES')} + + : + null + } { seasons.length > 0 ? { VideosList.propTypes = { className: PropTypes.string, metaItem: PropTypes.object, + libraryItem: PropTypes.object, season: PropTypes.number, - seasonOnSelect: PropTypes.func + seasonOnSelect: PropTypes.func, + toggleNotifications: PropTypes.func, }; module.exports = VideosList; diff --git a/src/routes/MetaDetails/VideosList/styles.less b/src/routes/MetaDetails/VideosList/styles.less index 4a1cc37ef..103d8c139 100644 --- a/src/routes/MetaDetails/VideosList/styles.less +++ b/src/routes/MetaDetails/VideosList/styles.less @@ -6,6 +6,7 @@ .videos-list-container { display: flex; flex-direction: column; + padding-top: 0.5rem; .message-container { flex: 1; @@ -35,9 +36,22 @@ } } + .notifications-checkbox { + flex: none; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 1rem; + height: 3rem; + padding: 0 1.5rem; + color: @color-surface-light5-90; + } + .seasons-bar { flex: none; align-self: stretch; + margin: 0.5rem 1rem 1rem 1rem; } .search-bar { 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/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 ?
-
diff --git a/src/types/models/Ctx.d.ts b/src/types/models/Ctx.d.ts index 83fe66c84..fd3cb2766 100644 --- a/src/types/models/Ctx.d.ts +++ b/src/types/models/Ctx.d.ts @@ -42,6 +42,19 @@ type Profile = { settings: Settings, }; +type Notifications = { + uid: string, + created: string, + items: Record, +}; + +type NotificationItem = { + metaId: string, + videoId: string, + videoReleased: string, +} + type Ctx = { profile: Profile, + notifications: Notifications, }; \ No newline at end of file diff --git a/src/types/models/MetaDetails.d.ts b/src/types/models/MetaDetails.d.ts index 4b18a3612..efa7efc33 100644 --- a/src/types/models/MetaDetails.d.ts +++ b/src/types/models/MetaDetails.d.ts @@ -14,6 +14,7 @@ type MetaDetails = { addon: Addon, content: Loadable, } | null, + libraryItem: LibraryItem | null, selected: { metaPath: ResourceRequestPath, streamPath: ResourceRequestPath,