From 1cf2339a35aa99c6b557e1f14357623ae7276cc2 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 15 Mar 2024 19:15:31 +0100 Subject: [PATCH] refactor: use chips instead of multiselect for library filters --- package-lock.json | 129 ++++++++++++++++++++++ package.json | 2 + src/common/Chips/Chip/Chip.less | 34 ++++++ src/common/Chips/Chip/Chip.tsx | 45 ++++++++ src/common/Chips/Chip/index.ts | 4 + src/common/Chips/Chips.less | 25 +++++ src/common/Chips/Chips.tsx | 51 +++++++++ src/common/Chips/index.ts | 4 + src/common/index.js | 2 + src/modules.d.ts | 5 +- src/routes/Library/Library.js | 21 +--- src/routes/Library/styles.less | 34 ------ src/routes/Library/useSelectableInputs.js | 9 +- tests/copyright.spec.js | 2 +- tsconfig.json | 12 +- webpack.config.js | 11 +- 16 files changed, 321 insertions(+), 69 deletions(-) create mode 100644 src/common/Chips/Chip/Chip.less create mode 100644 src/common/Chips/Chip/Chip.tsx create mode 100644 src/common/Chips/Chip/index.ts create mode 100644 src/common/Chips/Chips.less create mode 100644 src/common/Chips/Chips.tsx create mode 100644 src/common/Chips/index.ts diff --git a/package-lock.json b/package-lock.json index 3b2684f62..bc9f54f18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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, diff --git a/package.json b/package.json index dca1280da..9a333fed9 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/common/Chips/Chip/Chip.less b/src/common/Chips/Chip/Chip.less new file mode 100644 index 000000000..a0eb12d40 --- /dev/null +++ b/src/common/Chips/Chip/Chip.less @@ -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; + } +} \ No newline at end of file diff --git a/src/common/Chips/Chip/Chip.tsx b/src/common/Chips/Chip/Chip.tsx new file mode 100644 index 000000000..403d198af --- /dev/null +++ b/src/common/Chips/Chip/Chip.tsx @@ -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(null); + + const onClick = useCallback(({ currentTarget }: MouseEvent) => { + const value = currentTarget.dataset['value']; + value && onSelect(value); + }, [onselect]); + + useEffect(() => { + active && ref.current?.scrollIntoView({ + block: 'nearest', + inline: 'center', + behavior: 'smooth', + }); + }, [active]); + + return ( + + ); +}); + +export default Chip; \ No newline at end of file diff --git a/src/common/Chips/Chip/index.ts b/src/common/Chips/Chip/index.ts new file mode 100644 index 000000000..9713aecaa --- /dev/null +++ b/src/common/Chips/Chip/index.ts @@ -0,0 +1,4 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Chip from './Chip'; +export default Chip; \ No newline at end of file diff --git a/src/common/Chips/Chips.less b/src/common/Chips/Chips.less new file mode 100644 index 000000000..cf0e85917 --- /dev/null +++ b/src/common/Chips/Chips.less @@ -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%); + } +} \ No newline at end of file diff --git a/src/common/Chips/Chips.tsx b/src/common/Chips/Chips.tsx new file mode 100644 index 000000000..765b8fcf6 --- /dev/null +++ b/src/common/Chips/Chips.tsx @@ -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(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 ( +
+ { + options.map(({ label, value }) => ( + + )) + } +
+ ); +}); + +export default Chips; \ No newline at end of file diff --git a/src/common/Chips/index.ts b/src/common/Chips/index.ts new file mode 100644 index 000000000..883af30d1 --- /dev/null +++ b/src/common/Chips/index.ts @@ -0,0 +1,4 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Chips from './Chips'; +export default Chips; \ No newline at end of file diff --git a/src/common/index.js b/src/common/index.js index a6aafb76c..8046dab15 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -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, diff --git a/src/modules.d.ts b/src/modules.d.ts index fc79871c7..70ebbd3fc 100644 --- a/src/modules.d.ts +++ b/src/modules.d.ts @@ -1,2 +1,3 @@ -declare module '*'; -declare module 'classnames'; \ No newline at end of file +declare module '*.less'; +declare module 'stremio/common'; +declare module 'stremio/common/*'; \ No newline at end of file diff --git a/src/routes/Library/Library.js b/src/routes/Library/Library.js index e7af3a07d..82fbfe437 100644 --- a/src/routes/Library/Library.js +++ b/src/routes/Library/Library.js @@ -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 ?
- -
- +
: null @@ -122,15 +116,6 @@ const Library = ({ model, urlParams, queryParams }) => {
} - { - inputsModalOpen ? - - - - - : - null - } ); }; diff --git a/src/routes/Library/styles.less b/src/routes/Library/styles.less index ee89347ae..811d97197 100644 --- a/src/routes/Library/styles.less +++ b/src/routes/Library/styles.less @@ -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 { diff --git a/src/routes/Library/useSelectableInputs.js b/src/routes/Library/useSelectableInputs.js index 2121d1392..6f24dc2c9 100644 --- a/src/routes/Library/useSelectableInputs.js +++ b/src/routes/Library/useSelectableInputs.js @@ -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) => { diff --git a/tests/copyright.spec.js b/tests/copyright.spec.js index 61315ccdb..df6b7198f 100644 --- a/tests/copyright.spec.js +++ b/tests/copyright.spec.js @@ -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 () => { diff --git a/tsconfig.json b/tsconfig.json index 99988b3ad..896a7d197 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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", ], } diff --git a/webpack.config.js b/webpack.config.js index 3c98f683c..8f62fa021 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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: {