mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
Fix conflicts
This commit is contained in:
commit
8465766270
24 changed files with 232 additions and 63 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
<ToastProvider className={styles['toasts-container']}>
|
||||
<ServicesToaster />
|
||||
<DeepLinkHandler />
|
||||
<Router
|
||||
<RouterWithProtectedRoutes
|
||||
className={styles['router']}
|
||||
viewsConfig={routerViewsConfig}
|
||||
onPathNotMatch={onPathNotMatch}
|
||||
|
|
|
|||
29
src/App/withProtectedRoutes.js
Normal file
29
src/App/withProtectedRoutes.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const { Intro } = require('stremio/routes');
|
||||
const { useProfile } = require('stremio/common');
|
||||
|
||||
const withProtectedRoutes = (Component) => {
|
||||
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 (
|
||||
<Component {...props} onRouteChange={onRouteChange} />
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = withProtectedRoutes;
|
||||
|
|
@ -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 (
|
||||
<MetaItem
|
||||
{...props}
|
||||
newVideos={newVideos}
|
||||
options={options}
|
||||
optionOnSelect={optionOnSelect}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 ?
|
||||
<div className={styles['new-videos']}>
|
||||
<div className={styles['layer']} />
|
||||
<div className={styles['layer']} />
|
||||
<div className={styles['layer']}>
|
||||
+{newVideos}
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
{
|
||||
(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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ const NavMenuContent = ({ onClick }) => {
|
|||
<div className={styles['email-container']}>
|
||||
<div className={styles['email-label']}>{profile.auth === null ? t('ANONYMOUS_USER') : profile.auth.user.email}</div>
|
||||
</div>
|
||||
<Button className={styles['logout-button-container']} title={profile.auth === null ? `${t('LOG_IN')} / ${t('SIGN_UP')}` : t('LOG_OUT')} href={'#/intro'} onClick={logoutButtonOnClick}>
|
||||
<Button className={styles['logout-button-container']} title={profile.auth === null ? `${t('LOG_IN')} / ${t('SIGN_UP')}` : t('LOG_OUT')} href={profile.auth === null ? '#/intro' : null} onClick={profile.auth !== null ? logoutButtonOnClick : null}>
|
||||
<div className={styles['logout-label']}>{profile.auth === null ? `${t('LOG_IN')} / ${t('SIGN_UP')}` : t('LOG_OUT')}</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
2
src/common/useNotifications.d.ts
vendored
Normal file
2
src/common/useNotifications.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
declare const useNotifcations: () => Notifications;
|
||||
export = useNotifcations;
|
||||
11
src/common/useNotifications.js
Normal file
11
src/common/useNotifications.js
Normal file
|
|
@ -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;
|
||||
|
|
@ -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 (
|
||||
<div className={classnames(className, 'routes-container')}>
|
||||
{
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
<MainNavBars className={styles['board-content-container']} route={'board'}>
|
||||
<div ref={scrollContainerRef} className={styles['board-content']} onScroll={onScroll}>
|
||||
{
|
||||
continueWatchingPreview.libraryItems.length > 0 ?
|
||||
continueWatchingPreview.items.length > 0 ?
|
||||
<MetaRow
|
||||
className={classnames(styles['board-row'], styles['continue-watching-row'], 'animation-fade-in')}
|
||||
title={t('BOARD_CONTINUE_WATCHING')}
|
||||
items={continueWatchingPreview.libraryItems}
|
||||
items={continueWatchingPreview.items}
|
||||
itemComponent={LibItem}
|
||||
deepLinks={continueWatchingPreview.deepLinks}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
|||
<VideosList
|
||||
className={styles['videos-list']}
|
||||
metaItem={metaDetails.metaItem}
|
||||
libraryItem={metaDetails.libraryItem}
|
||||
season={season}
|
||||
seasonOnSelect={seasonOnSelect}
|
||||
toggleNotifications={toggleNotifications}
|
||||
/>
|
||||
:
|
||||
null
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
overflow: visible;
|
||||
|
||||
.prev-season-button, .next-season-button {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
|||
</div>
|
||||
:
|
||||
<React.Fragment>
|
||||
{
|
||||
showNotificationsToggle && libraryItem ?
|
||||
<Checkbox className={styles['notifications-checkbox']} checked={!libraryItem.state.noNotif} onClick={toggleNotifications}>
|
||||
{t('DETAIL_RECEIVE_NOTIF_SERIES')}
|
||||
</Checkbox>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
seasons.length > 0 ?
|
||||
<SeasonsBar
|
||||
|
|
@ -133,8 +143,10 @@ const VideosList = ({ className, metaItem, season, seasonOnSelect }) => {
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
}
|
||||
|
||||
.background-image {
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ const Settings = () => {
|
|||
</div>
|
||||
{
|
||||
profile.auth !== null ?
|
||||
<Button className={styles['logout-button-container']} title={ t('LOG_OUT') } href={'#/intro'} onClick={logoutButtonOnClick}>
|
||||
<Button className={styles['logout-button-container']} title={ t('LOG_OUT') } onClick={logoutButtonOnClick}>
|
||||
<div className={styles['logout-label']}>{ t('LOG_OUT') }</div>
|
||||
</Button>
|
||||
:
|
||||
|
|
@ -237,7 +237,7 @@ const Settings = () => {
|
|||
{
|
||||
profile.auth === null ?
|
||||
<div className={styles['option-container']}>
|
||||
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={`${t('LOG_IN')} / ${t('SIGN_UP')}`} href={'#/intro'} onClick={logoutButtonOnClick}>
|
||||
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={`${t('LOG_IN')} / ${t('SIGN_UP')}`} href={'#/intro'}>
|
||||
<div className={styles['label']}>{ t('LOG_IN') } / { t('SIGN_UP') }</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
13
src/types/models/Ctx.d.ts
vendored
13
src/types/models/Ctx.d.ts
vendored
|
|
@ -42,6 +42,19 @@ type Profile = {
|
|||
settings: Settings,
|
||||
};
|
||||
|
||||
type Notifications = {
|
||||
uid: string,
|
||||
created: string,
|
||||
items: Record<string, NotificationItem[]>,
|
||||
};
|
||||
|
||||
type NotificationItem = {
|
||||
metaId: string,
|
||||
videoId: string,
|
||||
videoReleased: string,
|
||||
}
|
||||
|
||||
type Ctx = {
|
||||
profile: Profile,
|
||||
notifications: Notifications,
|
||||
};
|
||||
1
src/types/models/MetaDetails.d.ts
vendored
1
src/types/models/MetaDetails.d.ts
vendored
|
|
@ -14,6 +14,7 @@ type MetaDetails = {
|
|||
addon: Addon,
|
||||
content: Loadable<MetaItemMetaDetails>,
|
||||
} | null,
|
||||
libraryItem: LibraryItem | null,
|
||||
selected: {
|
||||
metaPath: ResourceRequestPath,
|
||||
streamPath: ResourceRequestPath,
|
||||
|
|
|
|||
Loading…
Reference in a new issue