mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
Merge branch 'development' of https://github.com/Stremio/stremio-web into feature-mark-library-item-as-watched
This commit is contained in:
commit
eb5e42fbf7
15 changed files with 139 additions and 125 deletions
8
package-lock.json
generated
8
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.46.3",
|
||||
"@stremio/stremio-core-web": "0.47.0",
|
||||
"@stremio/stremio-icons": "5.2.0",
|
||||
"@stremio/stremio-video": "0.0.38",
|
||||
"a-color-picker": "1.2.1",
|
||||
|
|
@ -2969,9 +2969,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@stremio/stremio-core-web": {
|
||||
"version": "0.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.46.3.tgz",
|
||||
"integrity": "sha512-5us8KQQ0EJ2xjLukcLu3yia7fa8BIjxjOwbMebpSLGEomq6MzVQMmRGp/7rId2ggfIej2Zwf/GBVepZ2/nLE4A==",
|
||||
"version": "0.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.47.0.tgz",
|
||||
"integrity": "sha512-//wXNpVFnKjVegPjAKPLz+SanGvk/p+4cuDAvZst5igi+wCw/wbsxOssq8HZbq1QvaeFQdpttW0HXbkzLcyf8Q==",
|
||||
"dependencies": {
|
||||
"@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.46.3",
|
||||
"@stremio/stremio-core-web": "0.47.0",
|
||||
"@stremio/stremio-icons": "5.2.0",
|
||||
"@stremio/stremio-video": "0.0.38",
|
||||
"a-color-picker": "1.2.1",
|
||||
|
|
|
|||
|
|
@ -184,6 +184,10 @@ html {
|
|||
.toasts-container {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.tooltip-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ function withRemoteAndLocalAddon(AddonDetails) {
|
|||
id={addon.manifest.id}
|
||||
name={addon.manifest.name}
|
||||
version={addon.manifest.version}
|
||||
background={addon.manifest.background}
|
||||
logo={addon.manifest.logo}
|
||||
description={addon.manifest.description}
|
||||
types={addon.manifest.types}
|
||||
|
|
@ -132,8 +133,11 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
|
|||
null;
|
||||
return toggleButton !== null ? configureButton ? [cancelButton, configureButton, toggleButton] : [cancelButton, toggleButton] : [cancelButton];
|
||||
}, [addonDetails, onCloseRequest]);
|
||||
const modalBackground = React.useMemo(() => {
|
||||
return addonDetails.remoteAddon?.content.type === 'Ready' ? addonDetails.remoteAddon.content.content.manifest.background : null;
|
||||
}, [addonDetails.remoteAddon]);
|
||||
return (
|
||||
<ModalDialog className={styles['addon-details-modal-container']} title={'Stremio addon'} buttons={modalButtons} onCloseRequest={onCloseRequest}>
|
||||
<ModalDialog className={styles['addon-details-modal-container']} title={'Stremio addon'} buttons={modalButtons} background={modalBackground} onCloseRequest={onCloseRequest}>
|
||||
{
|
||||
addonDetails.selected === null ?
|
||||
<div className={styles['addon-details-message-container']}>
|
||||
|
|
|
|||
|
|
@ -81,6 +81,11 @@ const EXTERNAL_PLAYERS = [
|
|||
value: 'outplayer',
|
||||
platforms: ['ios'],
|
||||
},
|
||||
{
|
||||
label: 'M3U Playlist',
|
||||
value: 'm3u',
|
||||
platforms: ['ios', 'android', 'windows', 'linux', 'macos'],
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, poste
|
|||
{
|
||||
progress > 0 ?
|
||||
<div className={styles['progress-bar-layer']}>
|
||||
<div className={styles['progress-bar']} style={{ width: `${Math.max(0, Math.min(1, progress)) * 100}%` }} />
|
||||
<div className={styles['progress-bar']} style={{ width: `${progress}%` }} />
|
||||
<div className={styles['progress-bar-background']} />
|
||||
</div>
|
||||
:
|
||||
|
|
|
|||
|
|
@ -317,11 +317,14 @@
|
|||
|
||||
.title-label {
|
||||
flex: 1;
|
||||
max-height: 2.4em;
|
||||
padding-left: 1.5rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
color: var(--primary-foreground-color);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
|
||||
&:only-child {
|
||||
padding: 0 0.5rem;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const { default: Icon } = require('@stremio/stremio-icons/react');
|
|||
const { Modal } = require('stremio-router');
|
||||
const styles = require('./styles');
|
||||
|
||||
const ModalDialog = ({ className, title, buttons, children, dataset, onCloseRequest, ...props }) => {
|
||||
const ModalDialog = ({ className, title, buttons, children, dataset, onCloseRequest, background, ...props }) => {
|
||||
const routeFocused = useRouteFocused();
|
||||
const modalsContainer = useModalsContainer();
|
||||
const modalContainerRef = React.useRef(null);
|
||||
|
|
@ -59,41 +59,44 @@ const ModalDialog = ({ className, title, buttons, children, dataset, onCloseRequ
|
|||
return (
|
||||
<Modal ref={modalContainerRef} {...props} className={classnames(className, styles['modal-container'])} onMouseDown={onModalContainerMouseDown}>
|
||||
<div className={styles['modal-dialog-container']} onMouseDown={onModalDialogContainerMouseDown}>
|
||||
<div className={styles['modal-dialog-background']} style={{backgroundImage: `url('${background}')`}} />
|
||||
<Button className={styles['close-button-container']} title={'Close'} onClick={closeButtonOnClick}>
|
||||
<Icon className={styles['icon']} name={'close'} />
|
||||
</Button>
|
||||
{
|
||||
typeof title === 'string' && title.length > 0 ?
|
||||
<div className={styles['title-container']} title={title}>{title}</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles['modal-dialog-content']}>
|
||||
{children}
|
||||
{
|
||||
typeof title === 'string' && title.length > 0 ?
|
||||
<div className={styles['title-container']} title={title}>{title}</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles['modal-dialog-content']}>
|
||||
{children}
|
||||
</div>
|
||||
{
|
||||
Array.isArray(buttons) && buttons.length > 0 ?
|
||||
<div className={styles['buttons-container']}>
|
||||
{buttons.map(({ className, label, icon, props }, index) => (
|
||||
<Button title={label} {...props} key={index} className={classnames(className, styles['action-button'])}>
|
||||
{
|
||||
typeof icon === 'string' && icon.length > 0 ?
|
||||
<Icon className={styles['icon']} name={icon} />
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
typeof label === 'string' && label.length > 0 ?
|
||||
<div className={styles['label']}>{label}</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
{
|
||||
Array.isArray(buttons) && buttons.length > 0 ?
|
||||
<div className={styles['buttons-container']}>
|
||||
{buttons.map(({ className, label, icon, props }, index) => (
|
||||
<Button title={label} {...props} key={index} className={classnames(className, styles['action-button'])}>
|
||||
{
|
||||
typeof icon === 'string' && icon.length > 0 ?
|
||||
<Icon className={styles['icon']} name={icon} />
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
typeof label === 'string' && label.length > 0 ?
|
||||
<div className={styles['label']}>{label}</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
|
@ -102,6 +105,7 @@ const ModalDialog = ({ className, title, buttons, children, dataset, onCloseRequ
|
|||
ModalDialog.propTypes = {
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
background: PropTypes.string,
|
||||
buttons: PropTypes.arrayOf(PropTypes.shape({
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,18 @@
|
|||
background-color: var(--modal-background-color);
|
||||
box-shadow: var(--outer-glow);
|
||||
|
||||
.modal-dialog-background {
|
||||
z-index: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.close-button-container {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
|
|
@ -38,7 +50,8 @@
|
|||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
&:hover,
|
||||
&:focus {
|
||||
.icon {
|
||||
opacity: 1;
|
||||
color: var(--primary-foreground-color);
|
||||
|
|
@ -50,36 +63,43 @@
|
|||
}
|
||||
}
|
||||
|
||||
.title-container {
|
||||
flex: 1 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 4.5rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
.modal-dialog-content {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
overflow-y: auto;
|
||||
padding: 2rem 0;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 2rem;
|
||||
.title-container {
|
||||
flex: 1 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 4.5rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
.buttons-container {
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
.modal-dialog-content {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
overflow-y: auto;
|
||||
padding: 2rem 0;
|
||||
|
||||
&:last-child {
|
||||
margin: 2rem 0;
|
||||
&:last-child {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons-container {
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&:last-child {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const useBoard = () => {
|
|||
}
|
||||
}, 'board');
|
||||
}, []);
|
||||
const board = useModelState({ model: 'board', timeout: 1500, action });
|
||||
const board = useModelState({ model: 'board', action });
|
||||
return [board, loadRange];
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ const PropTypes = require('prop-types');
|
|||
const classnames = require('classnames');
|
||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||
const NotFound = require('stremio/routes/NotFound');
|
||||
const { Button, DelayedRenderer, Multiselect, MainNavBars, LibItem, Image, ModalDialog, PaginationInput, useProfile, useNotifications, routesRegexp, useBinaryState, withCoreSuspender } = require('stremio/common');
|
||||
const { Button, DelayedRenderer, Multiselect, MainNavBars, LibItem, Image, ModalDialog, useProfile, useNotifications, routesRegexp, useOnScrollToBottom, useBinaryState, withCoreSuspender } = require('stremio/common');
|
||||
const useLibrary = require('./useLibrary');
|
||||
const useSelectableInputs = require('./useSelectableInputs');
|
||||
const styles = require('./styles');
|
||||
|
||||
const SCROLL_TO_BOTTOM_TRESHOLD = 400;
|
||||
|
||||
function withModel(Library) {
|
||||
const withModel = ({ urlParams, queryParams }) => {
|
||||
const model = React.useMemo(() => {
|
||||
|
|
@ -46,9 +48,21 @@ function withModel(Library) {
|
|||
const Library = ({ model, urlParams, queryParams }) => {
|
||||
const profile = useProfile();
|
||||
const notifications = useNotifications();
|
||||
const library = useLibrary(model, urlParams, queryParams);
|
||||
const [typeSelect, sortSelect, paginationInput] = useSelectableInputs(library);
|
||||
const [library, loadNextPage] = useLibrary(model, urlParams, queryParams);
|
||||
const [typeSelect, sortSelect, hasNextPage] = useSelectableInputs(library);
|
||||
const [inputsModalOpen, openInputsModal, closeInputsModal] = useBinaryState(false);
|
||||
const scrollContainerRef = React.useRef(null);
|
||||
const onScrollToBottom = React.useCallback(() => {
|
||||
if (hasNextPage) {
|
||||
loadNextPage();
|
||||
}
|
||||
}, [hasNextPage, loadNextPage]);
|
||||
const onScroll = useOnScrollToBottom(onScrollToBottom, SCROLL_TO_BOTTOM_TRESHOLD);
|
||||
React.useLayoutEffect(() => {
|
||||
if (profile.auth !== null && library.selected && library.selected.request.page === 1) {
|
||||
scrollContainerRef.current.scrollTop = 0;
|
||||
}
|
||||
}, [profile.auth, library.selected]);
|
||||
return (
|
||||
<MainNavBars className={styles['library-container']} route={model}>
|
||||
<div className={styles['library-content']}>
|
||||
|
|
@ -58,12 +72,6 @@ const Library = ({ model, urlParams, queryParams }) => {
|
|||
<Multiselect {...typeSelect} className={styles['select-input-container']} />
|
||||
<Multiselect {...sortSelect} className={styles['select-input-container']} />
|
||||
<div className={styles['spacing']} />
|
||||
{
|
||||
paginationInput !== null ?
|
||||
<PaginationInput {...paginationInput} className={styles['pagination-input']} />
|
||||
:
|
||||
null
|
||||
}
|
||||
<Button className={styles['filter-container']} title={'All filters'} onClick={openInputsModal}>
|
||||
<Icon className={styles['filter-icon']} name={'filters'} />
|
||||
</Button>
|
||||
|
|
@ -107,7 +115,7 @@ const Library = ({ model, urlParams, queryParams }) => {
|
|||
<div className={styles['message-label']}>Empty {model === 'library' ? 'Library' : 'Continue Watching'}</div>
|
||||
</div>
|
||||
:
|
||||
<div className={classnames(styles['meta-items-container'], 'animation-fade-in')}>
|
||||
<div ref={scrollContainerRef} className={classnames(styles['meta-items-container'], 'animation-fade-in')} onScroll={onScroll}>
|
||||
{library.catalog.map((libItem, index) => (
|
||||
<LibItem {...libItem} notifications={notifications} removable={model === 'library'} key={index} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,6 @@
|
|||
multiselect-menu-container: menu-container;
|
||||
}
|
||||
|
||||
:import('~stremio/common/PaginationInput/styles.less') {
|
||||
pagination-prev-button-container: prev-button-container;
|
||||
pagination-next-button-container: next-button-container;
|
||||
pagination-button-icon: icon;
|
||||
pagination-label: label;
|
||||
}
|
||||
|
||||
:import('~stremio/common/ModalDialog/styles.less') {
|
||||
selectable-inputs-modal-container: modal-dialog-container;
|
||||
selectable-inputs-modal-content: modal-dialog-content;
|
||||
|
|
@ -75,26 +68,6 @@
|
|||
.spacing {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pagination-input {
|
||||
flex: none;
|
||||
height: 3rem;
|
||||
margin-left: 1.5rem;
|
||||
|
||||
.pagination-prev-button-container, .pagination-next-button-container {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
|
||||
.pagination-button-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-label {
|
||||
width: 3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-container {
|
||||
|
|
@ -277,10 +250,6 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.pagination-input {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
display: flex;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,19 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { useModelState } = require('stremio/common');
|
||||
|
||||
const useLibrary = (model, urlParams, queryParams) => {
|
||||
const { core } = useServices();
|
||||
const loadNextPage = React.useCallback(() => {
|
||||
core.transport.dispatch({
|
||||
action: 'LibraryWithFilters',
|
||||
args: {
|
||||
action: 'LoadNextPage',
|
||||
}
|
||||
}, 'library');
|
||||
}, []);
|
||||
const action = React.useMemo(() => ({
|
||||
action: 'Load',
|
||||
args: {
|
||||
|
|
@ -12,12 +22,12 @@ const useLibrary = (model, urlParams, queryParams) => {
|
|||
request: {
|
||||
type: typeof urlParams.type === 'string' ? urlParams.type : null,
|
||||
sort: queryParams.has('sort') ? queryParams.get('sort') : undefined,
|
||||
page: queryParams.has('page') ? parseInt(queryParams.get('page'), 10) : undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}), [urlParams, queryParams]);
|
||||
return useModelState({ model, action });
|
||||
const library = useModelState({ model, action });
|
||||
return [library, loadNextPage];
|
||||
};
|
||||
|
||||
module.exports = useLibrary;
|
||||
|
|
|
|||
|
|
@ -32,21 +32,7 @@ const mapSelectableInputs = (library, t) => {
|
|||
window.location = event.value;
|
||||
}
|
||||
};
|
||||
const paginationInput = library.selectable.prevPage || library.selectable.nextPage ?
|
||||
{
|
||||
label: library.selected.request.page.toString(),
|
||||
onSelect: (event) => {
|
||||
if (event.value === 'prev' && library.selectable.prevPage) {
|
||||
window.location = library.selectable.prevPage.deepLinks.library;
|
||||
}
|
||||
if (event.value === 'next' && library.selectable.nextPage) {
|
||||
window.location = library.selectable.nextPage.deepLinks.library;
|
||||
}
|
||||
}
|
||||
}
|
||||
:
|
||||
null;
|
||||
return [typeSelect, sortSelect, paginationInput];
|
||||
return [typeSelect, sortSelect, library.selectable.nextPage];
|
||||
};
|
||||
|
||||
const useSelectableInputs = (library) => {
|
||||
|
|
|
|||
|
|
@ -269,7 +269,8 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
setError(null);
|
||||
if (player.selected === null) {
|
||||
video.unload();
|
||||
} else if (player.selected.metaRequest === null || (player.metaItem !== null && player.metaItem.type !== 'Loading')) {
|
||||
} else if (streamingServer.settings !== null && streamingServer.settings.type !== 'Loading' &&
|
||||
(player.selected.metaRequest === null || (player.metaItem !== null && player.metaItem.type !== 'Loading'))) {
|
||||
video.load({
|
||||
stream: {
|
||||
...player.selected.stream,
|
||||
|
|
|
|||
Loading…
Reference in a new issue