Merge branch 'development' of https://github.com/Stremio/stremio-web into feat/settings-transcoding-profile

This commit is contained in:
Tim 2024-03-28 04:14:53 +01:00
commit 0ad4d1dd0f
20 changed files with 340 additions and 74 deletions

129
package-lock.json generated
View file

@ -63,6 +63,8 @@
"postcss-loader": "6.2.0",
"readdirp": "3.6.0",
"terser-webpack-plugin": "5.2.4",
"ts-loader": "^9.5.1",
"typescript": "^5.4.2",
"webpack": "5.61.0",
"webpack-cli": "4.9.1",
"webpack-dev-server": "^4.7.4",
@ -13039,6 +13041,120 @@
"node": ">=6"
}
},
"node_modules/ts-loader": {
"version": "9.5.1",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz",
"integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==",
"dev": true,
"dependencies": {
"chalk": "^4.1.0",
"enhanced-resolve": "^5.0.0",
"micromatch": "^4.0.0",
"semver": "^7.3.4",
"source-map": "^0.7.4"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"typescript": "*",
"webpack": "^5.0.0"
}
},
"node_modules/ts-loader/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/ts-loader/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/ts-loader/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/ts-loader/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/ts-loader/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/ts-loader/node_modules/semver": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ts-loader/node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
"dev": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/ts-loader/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/tslib": {
"version": "1.14.1",
"license": "0BSD"
@ -13093,6 +13209,19 @@
"is-typedarray": "^1.0.0"
}
},
"node_modules/typescript": {
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
"integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/unbox-primitive": {
"version": "1.0.1",
"dev": true,

View file

@ -66,6 +66,8 @@
"postcss-loader": "6.2.0",
"readdirp": "3.6.0",
"terser-webpack-plugin": "5.2.4",
"ts-loader": "^9.5.1",
"typescript": "^5.4.2",
"webpack": "5.61.0",
"webpack-cli": "4.9.1",
"webpack-dev-server": "^4.7.4",

View file

@ -35,6 +35,7 @@
--primary-accent-color: rgb(123, 91, 245);
--secondary-accent-color: rgba(34, 179, 101, 1);
--tertiary-accent-color: rgba(246, 199, 0, 1);
--quaternary-accent-color: rgba(18, 69, 166, 1);
--overlay-color: rgba(255, 255, 255, 0.05);
--modal-background-color: rgba(15, 13, 32, 1);
--outer-glow: 0px 0px 30px rgba(123, 91, 245, 0.37);

View file

@ -0,0 +1,34 @@
// Copyright (C) 2017-2024 Smart code 203358507
@height: 2.75rem;
.chip {
flex: none;
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: @height;
font-size: 1rem;
font-weight: 500;
color: var(--primary-foreground-color);
white-space: nowrap;
text-transform: capitalize;
padding: 0 1.75rem;
border-radius: @height;
-webkit-tap-highlight-color: transparent;
background-color: transparent;
user-select: none;
overflow: hidden;
&:hover {
background-color: var(--overlay-color);
transition: background-color 0.1s ease-out;
}
&.active {
font-weight: 700;
background-color: var(--quaternary-accent-color);
transition: background-color 0.1s ease-in;
}
}

View file

@ -0,0 +1,45 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { MouseEvent, memo, useCallback, useEffect, useRef } from 'react';
import classNames from 'classnames';
import Button from 'stremio/common/Button';
import styles from './Chip.less';
type Props = {
label: string,
value: string,
active: boolean,
onSelect: (value: string) => void,
};
const Chip = memo(({ label, value, active, onSelect }: Props) => {
const ref = useRef<HTMLElement>(null);
const onClick = useCallback(({ currentTarget }: MouseEvent<HTMLElement>) => {
const value = currentTarget.dataset['value'];
value && onSelect(value);
}, [onselect]);
useEffect(() => {
active && ref.current?.scrollIntoView({
block: 'nearest',
inline: 'center',
behavior: 'smooth',
});
}, [active]);
return (
<Button
ref={ref}
key={value}
className={classNames(styles['chip'], { [styles['active']]: active })}
tabIndex={-1}
data-value={value}
onClick={onClick}
>
{label}
</Button>
);
});
export default Chip;

View file

@ -0,0 +1,4 @@
// Copyright (C) 2017-2024 Smart code 203358507
import Chip from './Chip';
export default Chip;

View file

@ -0,0 +1,25 @@
// Copyright (C) 2017-2024 Smart code 203358507
@mask-width: 10%;
.chips {
position: relative;
width: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 1rem;
overflow-x: auto;
&.left {
mask-image: linear-gradient(90deg, rgba(0, 0, 0, 1) calc(100% - @mask-width), rgba(0, 0, 0, 0) 100%);
}
&.right {
mask-image: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) @mask-width);
}
&.center {
mask-image: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) @mask-width, rgba(0, 0, 0, 1) calc(100% - @mask-width), rgba(0, 0, 0, 0) 100%);
}
}

View file

@ -0,0 +1,56 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { memo, useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import Chip from './Chip';
import styles from './Chips.less';
type Option = {
label: string,
value: string,
};
type Props = {
options: Option[],
selected: string[],
onSelect: (value: string) => {},
};
const SCROLL_THRESHOLD = 1;
const Chips = memo(({ options, selected, onSelect }: Props) => {
const ref = useRef<HTMLDivElement>(null);
const [scrollPosition, setScrollPosition] = useState('left');
useEffect(() => {
const onScroll = ({ target }: Event) => {
const { scrollLeft, scrollWidth, offsetWidth} = target as HTMLDivElement;
const position =
(scrollLeft - SCROLL_THRESHOLD) <= 0 ? 'left' :
(scrollLeft + offsetWidth + SCROLL_THRESHOLD) >= scrollWidth ? 'right' :
'center';
setScrollPosition(position);
};
ref.current?.addEventListener('scroll', onScroll);
return () => ref.current?.removeEventListener('scroll', onScroll);
}, []);
return (
<div ref={ref} className={classNames(styles['chips'], [styles[scrollPosition]])}>
{
options.map(({ label, value }) => (
<Chip
key={value}
label={label}
value={value}
active={selected.includes(value)}
onSelect={onSelect}
/>
))
}
</div>
);
});
export default Chips;

View file

@ -0,0 +1,4 @@
// Copyright (C) 2017-2024 Smart code 203358507
import Chips from './Chips';
export default Chips;

View file

@ -3,6 +3,7 @@
const AddonDetailsModal = require('./AddonDetailsModal');
const Button = require('./Button');
const Checkbox = require('./Checkbox');
const { default: Chips } = require('./Chips');
const ColorInput = require('./ColorInput');
const ContinueWatchingItem = require('./ContinueWatchingItem');
const DelayedRenderer = require('./DelayedRenderer');
@ -50,6 +51,7 @@ module.exports = {
AddonDetailsModal,
Button,
Checkbox,
Chips,
ColorInput,
ContinueWatchingItem,
DelayedRenderer,

5
src/modules.d.ts vendored
View file

@ -1,2 +1,3 @@
declare module '*';
declare module 'classnames';
declare module '*.less';
declare module 'stremio/common';
declare module 'stremio/common/*';

View file

@ -3,9 +3,8 @@
const React = require('react');
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, useProfile, useNotifications, routesRegexp, useOnScrollToBottom, useBinaryState, withCoreSuspender } = require('stremio/common');
const { Button, Chips, DelayedRenderer, Multiselect, MainNavBars, LibItem, Image, useProfile, useNotifications, routesRegexp, useOnScrollToBottom, withCoreSuspender } = require('stremio/common');
const useLibrary = require('./useLibrary');
const useSelectableInputs = require('./useSelectableInputs');
const styles = require('./styles');
@ -49,8 +48,7 @@ const Library = ({ model, urlParams, queryParams }) => {
const profile = useProfile();
const notifications = useNotifications();
const [library, loadNextPage] = useLibrary(model, urlParams, queryParams);
const [typeSelect, sortSelect, hasNextPage] = useSelectableInputs(library);
const [inputsModalOpen, openInputsModal, closeInputsModal] = useBinaryState(false);
const [typeSelect, sortChips, hasNextPage] = useSelectableInputs(library);
const scrollContainerRef = React.useRef(null);
const onScrollToBottom = React.useCallback(() => {
if (hasNextPage) {
@ -70,11 +68,7 @@ const Library = ({ model, urlParams, queryParams }) => {
model === 'continue_watching' || profile.auth !== null ?
<div className={styles['selectable-inputs-container']}>
<Multiselect {...typeSelect} className={styles['select-input-container']} />
<Multiselect {...sortSelect} className={styles['select-input-container']} />
<div className={styles['spacing']} />
<Button className={styles['filter-container']} title={'All filters'} onClick={openInputsModal}>
<Icon className={styles['filter-icon']} name={'filters'} />
</Button>
<Chips {...sortChips} className={styles['select-input-container']} />
</div>
:
null
@ -122,15 +116,6 @@ const Library = ({ model, urlParams, queryParams }) => {
</div>
}
</div>
{
inputsModalOpen ?
<ModalDialog title={'Library filters'} className={styles['selectable-inputs-modal']} onCloseRequest={closeInputsModal}>
<Multiselect {...typeSelect} className={styles['select-input-container']} />
<Multiselect {...sortSelect} className={styles['select-input-container']} />
</ModalDialog>
:
null
}
</MainNavBars>
);
};

View file

@ -46,28 +46,6 @@
overflow: auto;
}
}
.filter-container {
flex: none;
display: none;
align-items: center;
justify-content: center;
width: 3rem;
height: 3rem;
border-radius: var(--border-radius);
background-color: var(--overlay-color);
.filter-icon {
flex: none;
width: 1.4rem;
height: 1.4rem;
color: var(--primary-foreground-color);
}
}
.spacing {
flex: 1;
}
}
.message-container {
@ -241,18 +219,6 @@
.library-content {
.selectable-inputs-container {
justify-content: space-between;
.select-input-container {
display: none;
}
.spacing {
display: none;
}
.filter-container {
display: flex;
}
}
.meta-items-container {

View file

@ -18,8 +18,7 @@ const mapSelectableInputs = (library, t) => {
window.location = event.value;
}
};
const sortSelect = {
title: t.string('SELECT_SORT'),
const sortChips = {
options: library.selectable.sorts
.map(({ sort, deepLinks }) => ({
value: deepLinks.library,
@ -28,11 +27,11 @@ const mapSelectableInputs = (library, t) => {
selected: library.selectable.sorts
.filter(({ selected }) => selected)
.map(({ deepLinks }) => deepLinks.library),
onSelect: (event) => {
window.location = event.value;
onSelect: (value) => {
window.location = value;
}
};
return [typeSelect, sortSelect, library.selectable.nextPage];
return [typeSelect, sortChips, library.selectable.nextPage];
};
const useSelectableInputs = (library) => {

View file

@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const debounce = require('lodash.debounce');
const { useRouteFocused } = require('stremio-router');
const { Slider } = require('stremio/common');
const { Slider, Button, useBinaryState } = require('stremio/common');
const formatTime = require('./formatTime');
const styles = require('./styles');
@ -13,6 +13,8 @@ const SeekBar = ({ className, time, duration, buffered, onSeekRequested }) => {
const disabled = time === null || isNaN(time) || duration === null || isNaN(duration);
const routeFocused = useRouteFocused();
const [seekTime, setSeekTime] = React.useState(null);
const [remainingTimeMode,,, toggleRemainingTimeMode] = useBinaryState(false);
const resetTimeDebounced = React.useCallback(debounce(() => {
setSeekTime(null);
}, 1500), []);
@ -56,7 +58,13 @@ const SeekBar = ({ className, time, duration, buffered, onSeekRequested }) => {
onSlide={onSlide}
onComplete={onComplete}
/>
<div className={styles['label']}>{formatTime(duration)}</div>
<Button onClick={toggleRemainingTimeMode}>
<div className={styles['label']}>
{remainingTimeMode && duration !== null && !isNaN(duration)
? formatTime(duration - time, '-')
: formatTime(duration) }
</div>
</Button>
</div>
);
};

View file

@ -4,7 +4,7 @@ const formatUnit = (value) => {
return ('0' + value).slice(-1 * Math.max(value.toString().length, 2));
};
const formatTime = (time) => {
const formatTime = (time, prefix = '') => {
if (time === null || isNaN(time)) {
return '--:--:--';
}
@ -12,7 +12,7 @@ const formatTime = (time) => {
const hours = Math.floor(time / (1000 * 60 * 60));
const minutes = Math.floor((time / (1000 * 60)) % 60);
const seconds = Math.floor((time / 1000) % 60);
return `${formatUnit(hours)}:${formatUnit(minutes)}:${formatUnit(seconds)}`;
return `${formatUnit(hours)}:${formatUnit(minutes)}:${formatUnit(seconds)}${prefix}`;
};
module.exports = formatTime;

View file

@ -14,7 +14,7 @@
.label {
flex: none;
width: 5rem;
width: 6rem;
white-space: nowrap;
text-overflow: ellipsis;
direction: rtl;

View file

@ -3,7 +3,7 @@
const fs = require('fs');
const readdirp = require('readdirp');
const COPYRIGHT_HEADER = /^\/\/ Copyright \(C\) 2017-2023 Smart code 203358507.*/;
const COPYRIGHT_HEADER = /^\/\/ Copyright \(C\) 2017-\d{4} Smart code 203358507.*/;
describe('copyright', () => {
test('js', async () => {

View file

@ -1,20 +1,20 @@
{
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable"],
"jsx": "preserve",
"rootDir": "./src",
"jsx": "react",
"baseUrl": "src",
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"stremio/*": ["src/*"],
},
"resolveJsonModule": true,
"esModuleInterop": true,
"allowJs": true,
"checkJs": false,
"noEmit": true,
"strict": false
"noEmit": false,
"strict": true,
},
"include": [
"./src",
"src",
],
}

View file

@ -44,6 +44,11 @@ module.exports = (env, argv) => ({
}
}
},
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: 'ts-loader',
},
{
test: /\.less$/,
exclude: /node_modules/,
@ -142,10 +147,10 @@ module.exports = (env, argv) => ({
]
},
resolve: {
extensions: ['.js', '.json', '.less', '.wasm'],
extensions: ['.tsx', '.ts', '.js', '.json', '.less', '.wasm'],
alias: {
'stremio': path.join(__dirname, 'src'),
'stremio-router': path.join(__dirname, 'src', 'router')
'stremio': path.resolve(__dirname, 'src'),
'stremio-router': path.resolve(__dirname, 'src', 'router')
}
},
devServer: {