refactor: use chips instead of multiselect for library filters

This commit is contained in:
Tim 2024-03-15 19:15:31 +01:00
parent 1c57dd3a65
commit 1cf2339a35
16 changed files with 321 additions and 69 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",
@ -13023,6 +13025,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"
@ -13077,6 +13193,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

@ -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.5rem;
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: 600;
background-color: var(--primary-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,51 @@
// 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 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 === 0 ? 'left' : scrollLeft + offsetWidth >= 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

@ -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: {