mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-21 11:42:05 +00:00
Merge branch 'development' into feature-volume-on-scroll-indicator
This commit is contained in:
commit
dca8d50888
21 changed files with 172 additions and 96 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
# Stremio - Freedom to Stream
|
# Stremio - Freedom to Stream
|
||||||
|
|
||||||

|

|
||||||
[](https://stremio.github.io/stremio-web/development)
|
[](https://stremio.github.io/stremio-web/development)
|
||||||
|
|
||||||
Stremio is a modern media center that's a one-stop solution for your video entertainment. You discover, watch and organize video content from easy to install addons.
|
Stremio is a modern media center that's a one-stop solution for your video entertainment. You discover, watch and organize video content from easy to install addons.
|
||||||
|
|
||||||
|
|
|
||||||
41
package-lock.json
generated
41
package-lock.json
generated
|
|
@ -12,9 +12,9 @@
|
||||||
"@babel/runtime": "7.16.0",
|
"@babel/runtime": "7.16.0",
|
||||||
"@sentry/browser": "6.13.3",
|
"@sentry/browser": "6.13.3",
|
||||||
"@stremio/stremio-colors": "5.0.1",
|
"@stremio/stremio-colors": "5.0.1",
|
||||||
"@stremio/stremio-core-web": "0.46.0",
|
"@stremio/stremio-core-web": "0.46.3",
|
||||||
"@stremio/stremio-icons": "5.0.0-beta.3",
|
"@stremio/stremio-icons": "5.2.0",
|
||||||
"@stremio/stremio-video": "0.0.26",
|
"@stremio/stremio-video": "0.0.33",
|
||||||
"a-color-picker": "1.2.1",
|
"a-color-picker": "1.2.1",
|
||||||
"bowser": "2.11.0",
|
"bowser": "2.11.0",
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
"react-i18next": "^12.1.1",
|
"react-i18next": "^12.1.1",
|
||||||
"react-is": "18.2.0",
|
"react-is": "18.2.0",
|
||||||
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
|
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
|
||||||
"stremio-translations": "github:Stremio/stremio-translations#12b1307f95249496960d2a257b371db5700721e6",
|
"stremio-translations": "github:Stremio/stremio-translations#38d283adf4bbe6d29657b5023778e30af7f6b05a",
|
||||||
"url": "0.11.0",
|
"url": "0.11.0",
|
||||||
"use-long-press": "^3.1.5"
|
"use-long-press": "^3.1.5"
|
||||||
},
|
},
|
||||||
|
|
@ -2969,32 +2969,29 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@stremio/stremio-core-web": {
|
"node_modules/@stremio/stremio-core-web": {
|
||||||
"version": "0.46.0",
|
"version": "0.46.3",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.46.3.tgz",
|
||||||
|
"integrity": "sha512-5us8KQQ0EJ2xjLukcLu3yia7fa8BIjxjOwbMebpSLGEomq6MzVQMmRGp/7rId2ggfIej2Zwf/GBVepZ2/nLE4A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.16.0"
|
"@babel/runtime": "7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@stremio/stremio-icons": {
|
"node_modules/@stremio/stremio-icons": {
|
||||||
"version": "5.0.0-beta.3",
|
"version": "5.2.0",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/@stremio/stremio-icons/-/stremio-icons-5.2.0.tgz",
|
||||||
"workspaces": [
|
"integrity": "sha512-rABlPBTFF17QcSm/4IizVoE/jh+REt+waqA0RvIxuGjQppXlvj7CalqVvTam0CC2wgY00zNG1v/9kVHUDVzo4Q=="
|
||||||
"react",
|
|
||||||
"react-native",
|
|
||||||
"solid",
|
|
||||||
"angularjs"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"node_modules/@stremio/stremio-video": {
|
"node_modules/@stremio/stremio-video": {
|
||||||
"version": "0.0.26",
|
"version": "0.0.33",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.33.tgz",
|
||||||
|
"integrity": "sha512-SaJgKmDrk8+reJprXYEz/hfp66E9GdhqURX20DiRx0OsDZFDYvWIe1fZLqj5B3LjVHtwEq3FIku+Z9qnH4jPDg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
"color": "4.2.3",
|
"color": "4.2.3",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"eventemitter3": "4.0.7",
|
"eventemitter3": "4.0.7",
|
||||||
"hat": "0.0.3",
|
"hat": "0.0.3",
|
||||||
"hls.js": "https://github.com/Stremio/hls.js/releases/download/v1.2.3-patch1/hls.js-1.2.3-patch1.tgz",
|
"hls.js": "https://github.com/Stremio/hls.js/releases/download/v1.5.4-patch1/hls.js-1.5.4-patch1.tgz",
|
||||||
"lodash.clonedeep": "4.5.0",
|
"lodash.clonedeep": "4.5.0",
|
||||||
"magnet-uri": "6.2.0",
|
"magnet-uri": "6.2.0",
|
||||||
"url": "0.11.0",
|
"url": "0.11.0",
|
||||||
|
|
@ -6883,9 +6880,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hls.js": {
|
"node_modules/hls.js": {
|
||||||
"version": "1.2.3-patch1.0.canary.8609",
|
"version": "1.5.5-0.canary.9892",
|
||||||
"resolved": "https://github.com/Stremio/hls.js/releases/download/v1.2.3-patch1/hls.js-1.2.3-patch1.tgz",
|
"resolved": "https://github.com/Stremio/hls.js/releases/download/v1.5.4-patch1/hls.js-1.5.4-patch1.tgz",
|
||||||
"integrity": "sha512-b/WMwSXyV6QvoGYotzzrG0ldRW8mOzqxEhPDd+as4haAx78tmxoVkdYYtVKZ8MiJcMa6j00lfx7ti/2HlO5ByQ==",
|
"integrity": "sha512-YDHKApXQtCM9DgwAN/3KTvvaxz6zbmjACQKFMbFE1nD21hE+jX4gv8UX3sMYE99A0slMht29wjK93NCseuG0fg==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/hpack.js": {
|
"node_modules/hpack.js": {
|
||||||
|
|
@ -12456,8 +12453,8 @@
|
||||||
},
|
},
|
||||||
"node_modules/stremio-translations": {
|
"node_modules/stremio-translations": {
|
||||||
"version": "1.44.5",
|
"version": "1.44.5",
|
||||||
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#12b1307f95249496960d2a257b371db5700721e6",
|
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#38d283adf4bbe6d29657b5023778e30af7f6b05a",
|
||||||
"integrity": "sha512-929O9sIUph3ew4YlUfD/zoMUSAYmwrjRIS+opmft3Gi8qS36/gBrH8RYW8XvgkTon+xrgqr7hC2/QSck4tgrAA==",
|
"integrity": "sha512-O/uFENWQ/pXEw4hQ1XQ8e4g6ZeX80wrhrxZfaGq2svK2I3bC/gCe5et0lbVzFBkVefSP3o1BMrlhgoSqfQssqA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@
|
||||||
"@babel/runtime": "7.16.0",
|
"@babel/runtime": "7.16.0",
|
||||||
"@sentry/browser": "6.13.3",
|
"@sentry/browser": "6.13.3",
|
||||||
"@stremio/stremio-colors": "5.0.1",
|
"@stremio/stremio-colors": "5.0.1",
|
||||||
"@stremio/stremio-core-web": "0.46.0",
|
"@stremio/stremio-core-web": "0.46.3",
|
||||||
"@stremio/stremio-icons": "5.0.0-beta.3",
|
"@stremio/stremio-icons": "5.2.0",
|
||||||
"@stremio/stremio-video": "0.0.26",
|
"@stremio/stremio-video": "0.0.33",
|
||||||
"a-color-picker": "1.2.1",
|
"a-color-picker": "1.2.1",
|
||||||
"bowser": "2.11.0",
|
"bowser": "2.11.0",
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
"react-i18next": "^12.1.1",
|
"react-i18next": "^12.1.1",
|
||||||
"react-is": "18.2.0",
|
"react-is": "18.2.0",
|
||||||
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
|
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
|
||||||
"stremio-translations": "github:Stremio/stremio-translations#12b1307f95249496960d2a257b371db5700721e6",
|
"stremio-translations": "github:Stremio/stremio-translations#38d283adf4bbe6d29657b5023778e30af7f6b05a",
|
||||||
"url": "0.11.0",
|
"url": "0.11.0",
|
||||||
"use-long-press": "^3.1.5"
|
"use-long-press": "^3.1.5"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ const { NotFound } = require('stremio/routes');
|
||||||
const { ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common');
|
const { ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common');
|
||||||
const ServicesToaster = require('./ServicesToaster');
|
const ServicesToaster = require('./ServicesToaster');
|
||||||
const DeepLinkHandler = require('./DeepLinkHandler');
|
const DeepLinkHandler = require('./DeepLinkHandler');
|
||||||
|
const SearchParamsHandler = require('./SearchParamsHandler');
|
||||||
const ErrorDialog = require('./ErrorDialog');
|
const ErrorDialog = require('./ErrorDialog');
|
||||||
const withProtectedRoutes = require('./withProtectedRoutes');
|
const withProtectedRoutes = require('./withProtectedRoutes');
|
||||||
const routerViewsConfig = require('./routerViewsConfig');
|
const routerViewsConfig = require('./routerViewsConfig');
|
||||||
|
|
@ -164,6 +165,7 @@ const App = () => {
|
||||||
<TooltipProvider className={styles['tooltip-container']}>
|
<TooltipProvider className={styles['tooltip-container']}>
|
||||||
<ServicesToaster />
|
<ServicesToaster />
|
||||||
<DeepLinkHandler />
|
<DeepLinkHandler />
|
||||||
|
<SearchParamsHandler />
|
||||||
<RouterWithProtectedRoutes
|
<RouterWithProtectedRoutes
|
||||||
className={styles['router']}
|
className={styles['router']}
|
||||||
viewsConfig={routerViewsConfig}
|
viewsConfig={routerViewsConfig}
|
||||||
|
|
|
||||||
57
src/App/SearchParamsHandler.js
Normal file
57
src/App/SearchParamsHandler.js
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
// Copyright (C) 2017-2023 Smart code 203358507
|
||||||
|
|
||||||
|
const React = require('react');
|
||||||
|
const isEqual = require('lodash.isequal');
|
||||||
|
const { withCoreSuspender, useProfile, useToast } = require('stremio/common');
|
||||||
|
const { useServices } = require('stremio/services');
|
||||||
|
|
||||||
|
const SearchParamsHandler = () => {
|
||||||
|
const { core } = useServices();
|
||||||
|
const profile = useProfile();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const [searchParams, setSearchParams] = React.useState({});
|
||||||
|
|
||||||
|
const onLocationChange = () => {
|
||||||
|
const { origin, hash, search } = window.location;
|
||||||
|
const { searchParams } = new URL(`${origin}${hash.replace('#', '')}${search}`);
|
||||||
|
|
||||||
|
setSearchParams((previousSearchParams) => {
|
||||||
|
const currentSearchParams = Object.fromEntries(searchParams.entries());
|
||||||
|
return isEqual(previousSearchParams, currentSearchParams) ? previousSearchParams : currentSearchParams;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const { streamingServerUrl } = searchParams;
|
||||||
|
|
||||||
|
if (streamingServerUrl) {
|
||||||
|
core.transport.dispatch({
|
||||||
|
action: 'Ctx',
|
||||||
|
args: {
|
||||||
|
action: 'UpdateSettings',
|
||||||
|
args: {
|
||||||
|
...profile.settings,
|
||||||
|
streamingServerUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.show({
|
||||||
|
type: 'success',
|
||||||
|
title: `Using streaming server at ${streamingServerUrl}`,
|
||||||
|
timeout: 4000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
onLocationChange();
|
||||||
|
window.addEventListener('hashchange', onLocationChange);
|
||||||
|
return () => window.removeEventListener('hashchange', onLocationChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = withCoreSuspender(SearchParamsHandler);
|
||||||
|
|
@ -21,7 +21,8 @@
|
||||||
--vertical-nav-bar-size: 6rem;
|
--vertical-nav-bar-size: 6rem;
|
||||||
--focus-outline-size: 2px;
|
--focus-outline-size: 2px;
|
||||||
--color-facebook: #1877F1;
|
--color-facebook: #1877F1;
|
||||||
--color-twitter: #1DA1F2;
|
--color-x: #000000;
|
||||||
|
--color-reddit: #FF4500;
|
||||||
--color-imdb: #f5c518;
|
--color-imdb: #f5c518;
|
||||||
--color-trakt: #ED2224;
|
--color-trakt: #ED2224;
|
||||||
--color-placeholder: #60606080;
|
--color-placeholder: #60606080;
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,10 @@
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const { useServices } = require('stremio/services');
|
const { useServices } = require('stremio/services');
|
||||||
const useNotifications = require('stremio/common/useNotifications');
|
|
||||||
const LibItem = require('stremio/common/LibItem');
|
const LibItem = require('stremio/common/LibItem');
|
||||||
|
|
||||||
const ContinueWatchingItem = ({ _id, deepLinks, ...props }) => {
|
const ContinueWatchingItem = ({ _id, notifications, deepLinks, ...props }) => {
|
||||||
const { core } = useServices();
|
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 onClick = React.useCallback(() => {
|
const onClick = React.useCallback(() => {
|
||||||
if (deepLinks?.metaDetailsVideos ?? deepLinks?.metaDetailsStreams) {
|
if (deepLinks?.metaDetailsVideos ?? deepLinks?.metaDetailsStreams) {
|
||||||
|
|
@ -51,8 +44,9 @@ const ContinueWatchingItem = ({ _id, deepLinks, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<LibItem
|
<LibItem
|
||||||
{...props}
|
{...props}
|
||||||
|
_id={_id}
|
||||||
posterChangeCursor={true}
|
posterChangeCursor={true}
|
||||||
newVideos={newVideos}
|
notifications={notifications}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onPlayClick={onPlayClick}
|
onPlayClick={onPlayClick}
|
||||||
onDismissClick={onDismissClick}
|
onDismissClick={onDismissClick}
|
||||||
|
|
@ -62,6 +56,7 @@ const ContinueWatchingItem = ({ _id, deepLinks, ...props }) => {
|
||||||
|
|
||||||
ContinueWatchingItem.propTypes = {
|
ContinueWatchingItem.propTypes = {
|
||||||
_id: PropTypes.string,
|
_id: PropTypes.string,
|
||||||
|
notifications: PropTypes.object,
|
||||||
deepLinks: PropTypes.shape({
|
deepLinks: PropTypes.shape({
|
||||||
metaDetailsVideos: PropTypes.string,
|
metaDetailsVideos: PropTypes.string,
|
||||||
metaDetailsStreams: PropTypes.string,
|
metaDetailsStreams: PropTypes.string,
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,12 @@ const OPTIONS = [
|
||||||
{ label: 'LIBRARY_REMOVE', value: 'remove' },
|
{ label: 'LIBRARY_REMOVE', value: 'remove' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const LibItem = ({ _id, removable, ...props }) => {
|
const LibItem = ({ _id, removable, notifications, ...props }) => {
|
||||||
const { core } = useServices();
|
const { core } = useServices();
|
||||||
|
const newVideos = React.useMemo(() => {
|
||||||
|
const count = notifications.items?.[_id]?.length ?? 0;
|
||||||
|
return Math.min(Math.max(count, 0), 99);
|
||||||
|
}, [_id, notifications]);
|
||||||
const options = React.useMemo(() => {
|
const options = React.useMemo(() => {
|
||||||
return OPTIONS
|
return OPTIONS
|
||||||
.filter(({ value }) => {
|
.filter(({ value }) => {
|
||||||
|
|
@ -98,6 +102,7 @@ const LibItem = ({ _id, removable, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<MetaItem
|
<MetaItem
|
||||||
{...props}
|
{...props}
|
||||||
|
newVideos={newVideos}
|
||||||
options={options}
|
options={options}
|
||||||
optionOnSelect={optionOnSelect}
|
optionOnSelect={optionOnSelect}
|
||||||
/>
|
/>
|
||||||
|
|
@ -108,6 +113,7 @@ LibItem.propTypes = {
|
||||||
_id: PropTypes.string,
|
_id: PropTypes.string,
|
||||||
removable: PropTypes.bool,
|
removable: PropTypes.bool,
|
||||||
progress: PropTypes.number,
|
progress: PropTypes.number,
|
||||||
|
notifications: PropTypes.object,
|
||||||
deepLinks: PropTypes.shape({
|
deepLinks: PropTypes.shape({
|
||||||
metaDetailsVideos: PropTypes.string,
|
metaDetailsVideos: PropTypes.string,
|
||||||
metaDetailsStreams: PropTypes.string,
|
metaDetailsStreams: PropTypes.string,
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, poste
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
watched ?
|
!newVideos && watched ?
|
||||||
<div className={styles['watched-icon-layer']}>
|
<div className={styles['watched-icon-layer']}>
|
||||||
<Icon className={styles['watched-icon']} name={'checkmark'} />
|
<Icon className={styles['watched-icon']} name={'checkmark'} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const useTranslate = require('stremio/common/useTranslate');
|
||||||
const MetaRowPlaceholder = require('./MetaRowPlaceholder');
|
const MetaRowPlaceholder = require('./MetaRowPlaceholder');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
||||||
const MetaRow = ({ className, title, catalog, message, itemComponent }) => {
|
const MetaRow = ({ className, title, catalog, message, itemComponent, notifications }) => {
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
|
|
||||||
const catalogTitle = React.useMemo(() => {
|
const catalogTitle = React.useMemo(() => {
|
||||||
|
|
@ -56,7 +56,8 @@ const MetaRow = ({ className, title, catalog, message, itemComponent }) => {
|
||||||
return React.createElement(itemComponent, {
|
return React.createElement(itemComponent, {
|
||||||
...item,
|
...item,
|
||||||
key: index,
|
key: index,
|
||||||
className: classnames(styles['meta-item'], styles['poster-shape-poster'], styles[`poster-shape-${item.posterShape}`])
|
className: classnames(styles['meta-item'], styles['poster-shape-poster'], styles[`poster-shape-${item.posterShape}`]),
|
||||||
|
notifications,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
:
|
:
|
||||||
|
|
@ -104,6 +105,7 @@ MetaRow.propTypes = {
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
itemComponent: PropTypes.elementType,
|
itemComponent: PropTypes.elementType,
|
||||||
|
notifications: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = MetaRow;
|
module.exports = MetaRow;
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ const SearchBar = React.memo(({ className, query, active }) => {
|
||||||
const localSearch = useLocalSearch();
|
const localSearch = useLocalSearch();
|
||||||
const { createTorrentFromMagnet } = useTorrent();
|
const { createTorrentFromMagnet } = useTorrent();
|
||||||
|
|
||||||
const [historyOpen, openHistory, closeHistory, ] = useBinaryState(false);
|
const [historyOpen, openHistory, closeHistory, ] = useBinaryState(query === null ? true : false);
|
||||||
const [currentQuery, setCurrentQuery] = React.useState(query || '');
|
const [currentQuery, setCurrentQuery] = React.useState(query || '');
|
||||||
|
|
||||||
const searchInputRef = React.useRef(null);
|
const searchInputRef = React.useRef(null);
|
||||||
|
|
@ -153,7 +153,7 @@ const SearchBar = React.memo(({ className, query, active }) => {
|
||||||
localSearch?.items?.length ?
|
localSearch?.items?.length ?
|
||||||
<div className={styles['items']}>
|
<div className={styles['items']}>
|
||||||
<div className={styles['title']}>
|
<div className={styles['title']}>
|
||||||
<div className={styles['label']}>{ t('Recommendations') }</div>
|
<div className={styles['label']}>{ t('SEARCH_SUGGESTIONS') }</div>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
localSearch.items.map(({ query, deepLinks }, index) => (
|
localSearch.items.map(({ query, deepLinks }, index) => (
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ const { useTranslation } = require('react-i18next');
|
||||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||||
const { useRouteFocused } = require('stremio-router');
|
const { useRouteFocused } = require('stremio-router');
|
||||||
const { useServices } = require('stremio/services');
|
const { useServices } = require('stremio/services');
|
||||||
|
const useToast = require('stremio/common/Toast/useToast');
|
||||||
const Button = require('stremio/common/Button');
|
const Button = require('stremio/common/Button');
|
||||||
const TextInput = require('stremio/common/TextInput');
|
const TextInput = require('stremio/common/TextInput');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
@ -14,6 +15,7 @@ const styles = require('./styles');
|
||||||
const SharePrompt = ({ className, url }) => {
|
const SharePrompt = ({ className, url }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { core } = useServices();
|
const { core } = useServices();
|
||||||
|
const toast = useToast();
|
||||||
const inputRef = React.useRef(null);
|
const inputRef = React.useRef(null);
|
||||||
const routeFocused = useRouteFocused();
|
const routeFocused = useRouteFocused();
|
||||||
const selectInputContent = React.useCallback(() => {
|
const selectInputContent = React.useCallback(() => {
|
||||||
|
|
@ -25,6 +27,11 @@ const SharePrompt = ({ className, url }) => {
|
||||||
if (inputRef.current !== null) {
|
if (inputRef.current !== null) {
|
||||||
inputRef.current.select();
|
inputRef.current.select();
|
||||||
document.execCommand('copy');
|
document.execCommand('copy');
|
||||||
|
toast.show({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Copied to clipboard',
|
||||||
|
timeout: 3000,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -45,11 +52,12 @@ const SharePrompt = ({ className, url }) => {
|
||||||
<div className={styles['buttons-container']}>
|
<div className={styles['buttons-container']}>
|
||||||
<Button className={classnames(styles['button-container'], styles['facebook-button'])} title={'Facebook'} href={`https://www.facebook.com/sharer/sharer.php?u=${url}`} target={'_blank'}>
|
<Button className={classnames(styles['button-container'], styles['facebook-button'])} title={'Facebook'} href={`https://www.facebook.com/sharer/sharer.php?u=${url}`} target={'_blank'}>
|
||||||
<Icon className={styles['icon']} name={'facebook'} />
|
<Icon className={styles['icon']} name={'facebook'} />
|
||||||
<div className={styles['label']}>Facebook</div>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button className={classnames(styles['button-container'], styles['twitter-button'])} title={'Twitter'} href={`https://twitter.com/home?status=${url}`} target={'_blank'}>
|
<Button className={classnames(styles['button-container'], styles['x-button'])} title={'X (Twitter)'} href={`https://twitter.com/intent/tweet?text=${url}`} target={'_blank'}>
|
||||||
<Icon className={styles['icon']} name={'ic_twitter'} />
|
<Icon className={styles['icon']} name={'x'} />
|
||||||
<div className={styles['label']}>Twitter</div>
|
</Button>
|
||||||
|
<Button className={classnames(styles['button-container'], styles['reddit-button'])} title={'Reddit'} href={`https://www.reddit.com/submit?url=${url}`} target={'_blank'}>
|
||||||
|
<Icon className={styles['icon']} name={'reddit'} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['url-container']}>
|
<div className={styles['url-container']}>
|
||||||
|
|
|
||||||
|
|
@ -21,24 +21,12 @@
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
flex: none;
|
flex: none;
|
||||||
height: 1.2rem;
|
height: 1.5rem;
|
||||||
margin-right: 1rem;
|
|
||||||
color: var(--primary-foreground-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 1;
|
|
||||||
flex-basis: auto;
|
|
||||||
max-height: 2.4em;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--primary-foreground-color);
|
color: var(--primary-foreground-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.facebook-button, .twitter-button {
|
.facebook-button, .x-button, .reddit-button {
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
|
|
@ -50,8 +38,12 @@
|
||||||
background-color: var(--color-facebook);
|
background-color: var(--color-facebook);
|
||||||
}
|
}
|
||||||
|
|
||||||
.twitter-button {
|
.x-button {
|
||||||
background-color: var(--color-twitter);
|
background-color: var(--color-x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reddit-button {
|
||||||
|
background-color: var(--color-reddit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const styles = require('./styles');
|
||||||
|
|
||||||
const ToastItem = ({ title, message, dataset, onSelect, onClose, ...props }) => {
|
const ToastItem = ({ title, message, dataset, onSelect, onClose, ...props }) => {
|
||||||
const type = React.useMemo(() => {
|
const type = React.useMemo(() => {
|
||||||
return ['success', 'alert', 'error'].includes(props.type) ?
|
return ['success', 'alert', 'info', 'error'].includes(props.type) ?
|
||||||
props.type
|
props.type
|
||||||
:
|
:
|
||||||
'success';
|
'success';
|
||||||
|
|
@ -17,8 +17,9 @@ const ToastItem = ({ title, message, dataset, onSelect, onClose, ...props }) =>
|
||||||
const icon = React.useMemo(() => {
|
const icon = React.useMemo(() => {
|
||||||
return typeof props.icon === 'string' ? props.icon :
|
return typeof props.icon === 'string' ? props.icon :
|
||||||
type === 'success' ? 'checkmark' :
|
type === 'success' ? 'checkmark' :
|
||||||
type === 'error' ? 'warning' :
|
type === 'error' ? 'close' :
|
||||||
null;
|
type === 'info' ? 'about' :
|
||||||
|
null;
|
||||||
}, [type, props.icon]);
|
}, [type, props.icon]);
|
||||||
const toastOnClick = React.useCallback((event) => {
|
const toastOnClick = React.useCallback((event) => {
|
||||||
if (!event.nativeEvent.selectToastPrevented && typeof onSelect === 'function') {
|
if (!event.nativeEvent.selectToastPrevented && typeof onSelect === 'function') {
|
||||||
|
|
@ -81,7 +82,7 @@ const ToastItem = ({ title, message, dataset, onSelect, onClose, ...props }) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
ToastItem.propTypes = {
|
ToastItem.propTypes = {
|
||||||
type: PropTypes.oneOf(['success', 'alert', 'error']),
|
type: PropTypes.oneOf(['success', 'alert', 'info', 'error']),
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
message: PropTypes.string,
|
message: PropTypes.string,
|
||||||
icon: PropTypes.string,
|
icon: PropTypes.string,
|
||||||
|
|
|
||||||
|
|
@ -5,27 +5,36 @@
|
||||||
.toast-item-container {
|
.toast-item-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
width: 25rem;
|
width: 25rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
background-color: @color-surface-light4;
|
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
box-shadow: 0 0.3rem 0.5rem @color-background-dark5-40,
|
box-shadow: var(--outer-glow);
|
||||||
0 0.6rem 1rem @color-background-dark5-20;
|
background-color: var(--modal-background-color);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: 0.4px solid var(--primary-accent-color);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
padding: 1rem;
|
||||||
&.success {
|
&.success {
|
||||||
.icon-container {
|
.icon-container {
|
||||||
background-color: @color-accent3;
|
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
color: @color-surface-light5-90;
|
color: @color-accent3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
.icon-container {
|
.icon-container {
|
||||||
background-color: @color-accent2;
|
.icon {
|
||||||
|
color: var(--color-trakt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.info {
|
||||||
|
.icon-container {
|
||||||
|
background-color: @color-primary-light2;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
color: @color-surface-light5-90;
|
color: @color-surface-light5-90;
|
||||||
|
|
@ -34,26 +43,25 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-container {
|
.icon-container {
|
||||||
flex: none;
|
border-radius: 3px;
|
||||||
align-self: stretch;
|
background-color: var(--overlay-color);
|
||||||
width: 2.5rem;
|
|
||||||
padding: 0.5rem;
|
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
color: @color-background-dark5-90;
|
max-width: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-container {
|
.info-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
padding: 1rem;
|
padding: 0.2rem 1rem;
|
||||||
|
|
||||||
.title-container {
|
.title-container {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
|
color: var(--primary-foreground-color);
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
margin-bottom: 0.2rem;
|
margin-bottom: 0.2rem;
|
||||||
|
|
@ -62,25 +70,28 @@
|
||||||
|
|
||||||
.message-container {
|
.message-container {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
|
color: var(--primary-foreground-color);
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-button-container {
|
.close-button-container {
|
||||||
flex: none;
|
|
||||||
align-self: flex-start;
|
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
margin: 0.2rem;
|
border-radius: 3px;
|
||||||
padding: 0.5rem;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: @color-surface-light2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
color: var(--primary-foreground-color);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -19,7 +19,7 @@ const useTranslate = () => {
|
||||||
|
|
||||||
const catalogTitle = useCallback(({ addon, id, name, type } = {}, withType = true) => {
|
const catalogTitle = useCallback(({ addon, id, name, type } = {}, withType = true) => {
|
||||||
if (addon && id && name) {
|
if (addon && id && name) {
|
||||||
const partialKey = `${addon.manifest.id}/${id}`;
|
const partialKey = `${addon.manifest.id.replaceAll('.', '_')}_${id}`;
|
||||||
const translatedName = stringWithPrefix(partialKey, 'CATALOG_', name);
|
const translatedName = stringWithPrefix(partialKey, 'CATALOG_', name);
|
||||||
|
|
||||||
if (type && withType) {
|
if (type && withType) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ const React = require('react');
|
||||||
const classnames = require('classnames');
|
const classnames = require('classnames');
|
||||||
const debounce = require('lodash.debounce');
|
const debounce = require('lodash.debounce');
|
||||||
const { useTranslation } = require('react-i18next');
|
const { useTranslation } = require('react-i18next');
|
||||||
const { MainNavBars, MetaRow, ContinueWatchingItem, MetaItem, StreamingServerWarning, useStreamingServer, withCoreSuspender, getVisibleChildrenRange, EventModal } = require('stremio/common');
|
const { MainNavBars, MetaRow, ContinueWatchingItem, MetaItem, StreamingServerWarning, useStreamingServer, useNotifications, withCoreSuspender, getVisibleChildrenRange, EventModal } = require('stremio/common');
|
||||||
const useBoard = require('./useBoard');
|
const useBoard = require('./useBoard');
|
||||||
const useContinueWatchingPreview = require('./useContinueWatchingPreview');
|
const useContinueWatchingPreview = require('./useContinueWatchingPreview');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
@ -16,6 +16,7 @@ const Board = () => {
|
||||||
const streamingServer = useStreamingServer();
|
const streamingServer = useStreamingServer();
|
||||||
const continueWatchingPreview = useContinueWatchingPreview();
|
const continueWatchingPreview = useContinueWatchingPreview();
|
||||||
const [board, loadBoardRows] = useBoard();
|
const [board, loadBoardRows] = useBoard();
|
||||||
|
const notifications = useNotifications();
|
||||||
const boardCatalogsOffset = continueWatchingPreview.items.length > 0 ? 1 : 0;
|
const boardCatalogsOffset = continueWatchingPreview.items.length > 0 ? 1 : 0;
|
||||||
const scrollContainerRef = React.useRef();
|
const scrollContainerRef = React.useRef();
|
||||||
const onVisibleRangeChange = React.useCallback(() => {
|
const onVisibleRangeChange = React.useCallback(() => {
|
||||||
|
|
@ -48,6 +49,7 @@ const Board = () => {
|
||||||
title={t('BOARD_CONTINUE_WATCHING')}
|
title={t('BOARD_CONTINUE_WATCHING')}
|
||||||
catalog={continueWatchingPreview}
|
catalog={continueWatchingPreview}
|
||||||
itemComponent={ContinueWatchingItem}
|
itemComponent={ContinueWatchingItem}
|
||||||
|
notifications={notifications}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,7 @@ const Discover = ({ urlParams, queryParams }) => {
|
||||||
posterShape={metaItem.posterShape}
|
posterShape={metaItem.posterShape}
|
||||||
playname={selectedMetaItemIndex === index}
|
playname={selectedMetaItemIndex === index}
|
||||||
deepLinks={metaItem.deepLinks}
|
deepLinks={metaItem.deepLinks}
|
||||||
|
watched={metaItem.watched}
|
||||||
data-index={index}
|
data-index={index}
|
||||||
onClick={metaItemOnClick}
|
onClick={metaItemOnClick}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
|
||||||
const classnames = require('classnames');
|
const classnames = require('classnames');
|
||||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||||
const NotFound = require('stremio/routes/NotFound');
|
const NotFound = require('stremio/routes/NotFound');
|
||||||
const { Button, DelayedRenderer, Multiselect, MainNavBars, LibItem, Image, ModalDialog, PaginationInput, useProfile, routesRegexp, useBinaryState, withCoreSuspender } = require('stremio/common');
|
const { Button, DelayedRenderer, Multiselect, MainNavBars, LibItem, Image, ModalDialog, PaginationInput, useProfile, useNotifications, routesRegexp, useBinaryState, withCoreSuspender } = require('stremio/common');
|
||||||
const useLibrary = require('./useLibrary');
|
const useLibrary = require('./useLibrary');
|
||||||
const useSelectableInputs = require('./useSelectableInputs');
|
const useSelectableInputs = require('./useSelectableInputs');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
@ -45,6 +45,7 @@ function withModel(Library) {
|
||||||
|
|
||||||
const Library = ({ model, urlParams, queryParams }) => {
|
const Library = ({ model, urlParams, queryParams }) => {
|
||||||
const profile = useProfile();
|
const profile = useProfile();
|
||||||
|
const notifications = useNotifications();
|
||||||
const library = useLibrary(model, urlParams, queryParams);
|
const library = useLibrary(model, urlParams, queryParams);
|
||||||
const [typeSelect, sortSelect, paginationInput] = useSelectableInputs(library);
|
const [typeSelect, sortSelect, paginationInput] = useSelectableInputs(library);
|
||||||
const [inputsModalOpen, openInputsModal, closeInputsModal] = useBinaryState(false);
|
const [inputsModalOpen, openInputsModal, closeInputsModal] = useBinaryState(false);
|
||||||
|
|
@ -108,7 +109,7 @@ const Library = ({ model, urlParams, queryParams }) => {
|
||||||
:
|
:
|
||||||
<div className={classnames(styles['meta-items-container'], 'animation-fade-in')}>
|
<div className={classnames(styles['meta-items-container'], 'animation-fade-in')}>
|
||||||
{library.catalog.map((libItem, index) => (
|
{library.catalog.map((libItem, index) => (
|
||||||
<LibItem {...libItem} removable={model === 'library'} key={index} />
|
<LibItem {...libItem} notifications={notifications} removable={model === 'library'} key={index} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ const mapSelectableInputs = (library, t) => {
|
||||||
options: library.selectable.sorts
|
options: library.selectable.sorts
|
||||||
.map(({ sort, deepLinks }) => ({
|
.map(({ sort, deepLinks }) => ({
|
||||||
value: deepLinks.library,
|
value: deepLinks.library,
|
||||||
label: t.stringWithPrefix(sort, 'SORT_')
|
label: t.stringWithPrefix(sort.toUpperCase(), 'SORT_')
|
||||||
})),
|
})),
|
||||||
selected: library.selectable.sorts
|
selected: library.selectable.sorts
|
||||||
.filter(({ selected }) => selected)
|
.filter(({ selected }) => selected)
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
.select-choices-wrapper {
|
.select-choices-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
margin: 1em 1em 0 1em;
|
margin: 1em 1em 0 1em;
|
||||||
gap: 0 0.5em;
|
gap: 0 0.5em;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue