diff --git a/package-lock.json b/package-lock.json index 1c93bad01..c0876ac29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,20 @@ { "name": "stremio", - "version": "5.0.0-beta.14", + "version": "5.0.0-beta.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stremio", - "version": "5.0.0-beta.14", + "version": "5.0.0-beta.15", "license": "gpl-2.0", "dependencies": { "@babel/runtime": "7.26.0", "@sentry/browser": "8.42.0", "@stremio/stremio-colors": "5.2.0", "@stremio/stremio-core-web": "0.48.4", - "@stremio/stremio-icons": "5.4.0", - "@stremio/stremio-video": "0.0.46", + "@stremio/stremio-icons": "5.4.1", + "@stremio/stremio-video": "0.0.48", "a-color-picker": "1.2.1", "bowser": "2.11.0", "buffer": "6.0.3", @@ -23,7 +23,7 @@ "filter-invalid-dom-props": "3.0.1", "hat": "^0.0.3", "i18next": "^24.0.5", - "langs": "^2.0.0", + "langs": "github:Stremio/nodejs-langs", "lodash.debounce": "4.0.8", "lodash.intersection": "4.4.0", "lodash.isequal": "4.5.0", @@ -36,7 +36,7 @@ "react-i18next": "^15.1.3", "react-is": "18.3.1", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", - "stremio-translations": "github:Stremio/stremio-translations#f666d9a97cafa5aa150878b5c51a2896b5f4f1b2", + "stremio-translations": "github:Stremio/stremio-translations#a0f50634202f748a57907b645d2cd92fbaa479dd", "url": "0.11.4", "use-long-press": "^3.2.0" }, @@ -67,6 +67,7 @@ "postcss-loader": "8.1.1", "readdirp": "4.0.2", "terser-webpack-plugin": "5.3.10", + "thread-loader": "^4.0.4", "ts-loader": "^9.5.1", "typescript": "^5.7.2", "typescript-eslint": "^8.17.0", @@ -1869,7 +1870,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", - "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1880,8 +1880,7 @@ "node_modules/@babel/runtime/node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/@babel/template": { "version": "7.25.9", @@ -3261,7 +3260,6 @@ "version": "8.42.0", "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.42.0.tgz", "integrity": "sha512-xzgRI0wglKYsPrna574w1t38aftuvo44gjOKFvPNGPnYfiW9y4m+64kUz3JFbtanvOrKPcaITpdYiB4DeJXEbA==", - "license": "MIT", "dependencies": { "@sentry/core": "8.42.0" }, @@ -3273,7 +3271,6 @@ "version": "8.42.0", "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.42.0.tgz", "integrity": "sha512-dkIw5Wdukwzngg5gNJ0QcK48LyJaMAnBspqTqZ3ItR01STi6Z+6+/Bt5XgmrvDgRD+FNBinflc5zMmfdFXXhvw==", - "license": "MIT", "dependencies": { "@sentry/core": "8.42.0" }, @@ -3285,7 +3282,6 @@ "version": "8.42.0", "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.42.0.tgz", "integrity": "sha512-oNcJEBlDfXnRFYC5Mxj5fairyZHNqlnU4g8kPuztB9G5zlsyLgWfPxzcn1ixVQunth2/WZRklDi4o1ZfyHww7w==", - "license": "MIT", "dependencies": { "@sentry-internal/browser-utils": "8.42.0", "@sentry/core": "8.42.0" @@ -3298,7 +3294,6 @@ "version": "8.42.0", "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.42.0.tgz", "integrity": "sha512-XrPErqVhPsPh/oFLVKvz7Wb+Fi2J1zCPLeZCxWqFuPWI2agRyLVu0KvqJyzSpSrRAEJC/XFzuSVILlYlXXSfgA==", - "license": "MIT", "dependencies": { "@sentry-internal/replay": "8.42.0", "@sentry/core": "8.42.0" @@ -3311,7 +3306,6 @@ "version": "8.42.0", "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.42.0.tgz", "integrity": "sha512-lStrEk609KJHwXfDrOgoYVVoFFExixHywxSExk7ZDtwj2YPv6r6Y1gogvgr7dAZj7jWzadHkxZ33l9EOSJBfug==", - "license": "MIT", "dependencies": { "@sentry-internal/browser-utils": "8.42.0", "@sentry-internal/feedback": "8.42.0", @@ -3327,7 +3321,6 @@ "version": "8.42.0", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.42.0.tgz", "integrity": "sha512-ac6O3pgoIbU6rpwz6LlwW0wp3/GAHuSI0C5IsTgIY6baN8rOBnlAtG6KrHDDkGmUQ2srxkDJu9n1O6Td3cBCqw==", - "license": "MIT", "engines": { "node": ">=14.18" } @@ -3375,8 +3368,7 @@ "node_modules/@stremio/stremio-colors": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@stremio/stremio-colors/-/stremio-colors-5.2.0.tgz", - "integrity": "sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg==", - "license": "MIT" + "integrity": "sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg==" }, "node_modules/@stremio/stremio-core-web": { "version": "0.48.4", @@ -3406,9 +3398,9 @@ "license": "MIT" }, "node_modules/@stremio/stremio-icons": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@stremio/stremio-icons/-/stremio-icons-5.4.0.tgz", - "integrity": "sha512-rRWNER+wLgMjxd6sKT0MMq4lzXDOobY3GNdT3NDeeymBtB/CD0YmYqQuUOyYDjEZ1btIbNaniUOBoPW9d3ZQ8A==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@stremio/stremio-icons/-/stremio-icons-5.4.1.tgz", + "integrity": "sha512-7g4JP7tPRT1UDZxbuH/Urq7fc6te3joy8qyx/NGWIW7wO169TTISO7ZWdejzESvUVgZ/7i6rzkRmXZ3wefWcBg==", "workspaces": [ "react", "react-native", @@ -3417,9 +3409,10 @@ ] }, "node_modules/@stremio/stremio-video": { - "version": "0.0.46", - "resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.46.tgz", - "integrity": "sha512-U15CGB6CrUZKq3IKcEouAEH2RQoLy2+BI/hDStEYEACxlRlFaavKPI2opl37muh9TY089RnZVBYAM3yDidBZdg==", + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.48.tgz", + "integrity": "sha512-6ALGXCZC4NPsfhPcrwFWQzvH6UMMRsgSkHetnOhv9WmZ5ubiyUdbBzj9atGiGuuQz8pRcze66ztrub+dsaQbpw==", + "license": "MIT", "dependencies": { "buffer": "6.0.3", "color": "4.2.3", @@ -10162,6 +10155,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -10264,6 +10264,7 @@ }, "node_modules/langs": { "version": "2.0.0", + "resolved": "git+ssh://git@github.com/Stremio/nodejs-langs.git#584beab4032469f6b76be1d119a0ab25f31f4ab6", "license": "MIT" }, "node_modules/launch-editor": { @@ -13373,8 +13374,9 @@ }, "node_modules/stremio-translations": { "version": "1.44.9", - "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#f666d9a97cafa5aa150878b5c51a2896b5f4f1b2", - "integrity": "sha512-SzaIGUMqQuMAq58sI9L/RKSs5O4eF8VKPMqnWFddBSg/tZOU9xuNYqjRPKT07cp8MRfzzGQmCKMByozTYfjdIA==" + "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#a0f50634202f748a57907b645d2cd92fbaa479dd", + "integrity": "sha512-JJpd1JJet3T6/VTNdZ2NZ7uvHJ4zkuyqo5BnTcDGqLVNO/OpicGqKhZjE4WGSgmuhsfPBU8T0ICCfzKu2xpvKg==", + "license": "MIT" }, "node_modules/string_decoder": { "version": "1.1.1", @@ -13939,6 +13941,29 @@ "node": ">=0.2.6" } }, + "node_modules/thread-loader": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/thread-loader/-/thread-loader-4.0.4.tgz", + "integrity": "sha512-tXagu6Hivd03wB2tiS1bqvw345sc7mKei32EgpYpq31ZLes9FN0mEK2nKzXLRFgwt3PsBB0E/MZDp159rDoqwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^4.1.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.2.0" + }, + "engines": { + "node": ">= 16.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", diff --git a/package.json b/package.json old mode 100755 new mode 100644 index 5ee65d7c7..0b784890c --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "stremio", "displayName": "Stremio", - "version": "5.0.0-beta.14", + "version": "5.0.0-beta.15", "author": "Smart Code OOD", "private": true, "license": "gpl-2.0", @@ -17,8 +17,8 @@ "@sentry/browser": "8.42.0", "@stremio/stremio-colors": "5.2.0", "@stremio/stremio-core-web": "0.48.4", - "@stremio/stremio-icons": "5.4.0", - "@stremio/stremio-video": "0.0.46", + "@stremio/stremio-icons": "5.4.1", + "@stremio/stremio-video": "0.0.48", "a-color-picker": "1.2.1", "bowser": "2.11.0", "buffer": "6.0.3", @@ -27,7 +27,7 @@ "filter-invalid-dom-props": "3.0.1", "hat": "^0.0.3", "i18next": "^24.0.5", - "langs": "^2.0.0", + "langs": "github:Stremio/nodejs-langs", "lodash.debounce": "4.0.8", "lodash.intersection": "4.4.0", "lodash.isequal": "4.5.0", @@ -40,7 +40,7 @@ "react-i18next": "^15.1.3", "react-is": "18.3.1", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", - "stremio-translations": "github:Stremio/stremio-translations#f666d9a97cafa5aa150878b5c51a2896b5f4f1b2", + "stremio-translations": "github:Stremio/stremio-translations#a0f50634202f748a57907b645d2cd92fbaa479dd", "url": "0.11.4", "use-long-press": "^3.2.0" }, @@ -71,6 +71,7 @@ "postcss-loader": "8.1.1", "readdirp": "4.0.2", "terser-webpack-plugin": "5.3.10", + "thread-loader": "^4.0.4", "ts-loader": "^9.5.1", "typescript": "^5.7.2", "typescript-eslint": "^8.17.0", diff --git a/src/common/EventModal/styles.less b/src/common/EventModal/styles.less index 24c1d053d..b251175e5 100644 --- a/src/common/EventModal/styles.less +++ b/src/common/EventModal/styles.less @@ -102,7 +102,7 @@ } } -@media (orientation: landscape) and (max-height: @xsmall) { +@media (orientation: landscape) and (max-height: @minimum) { .event-modal { .modal-dialog-container { .modal-dialog-content { diff --git a/src/common/MetaItem/styles.less b/src/common/MetaItem/styles.less index c2d094ac9..17c7c4884 100644 --- a/src/common/MetaItem/styles.less +++ b/src/common/MetaItem/styles.less @@ -166,6 +166,7 @@ object-position: center; object-fit: cover; opacity: 0.9; + overflow-clip-margin: unset; } .placeholder-icon { diff --git a/src/common/Transition/Transition.tsx b/src/common/Transition/Transition.tsx new file mode 100644 index 000000000..e12b2a42b --- /dev/null +++ b/src/common/Transition/Transition.tsx @@ -0,0 +1,60 @@ +import { cloneElement, useCallback, useEffect, useMemo, useState } from 'react'; +import classNames from 'classnames'; + +type Props = { + children: JSX.Element, + when: boolean, + name: string, +}; + +const Transition = ({ children, when, name }: Props) => { + const [element, setElement] = useState(null); + const [mounted, setMounted] = useState(false); + + const [state, setState] = useState('enter'); + const [active, setActive] = useState(false); + + const callbackRef = useCallback((element: HTMLElement | null) => { + setElement(element); + }, []); + + const className = useMemo(() => { + const animationClass = `${name}-${state}`; + const activeClass = active ? `${name}-active` : null; + + return children && classNames( + children.props.className, + animationClass, + activeClass, + ); + }, [name, state, active, children]); + + const onTransitionEnd = useCallback(() => { + state === 'exit' && setMounted(false); + }, [state]); + + useEffect(() => { + setState(when ? 'enter' : 'exit'); + when && setMounted(true); + }, [when]); + + useEffect(() => { + requestAnimationFrame(() => { + setActive(!!element); + }); + }, [element]); + + useEffect(() => { + element?.addEventListener('transitionend', onTransitionEnd); + return () => element?.removeEventListener('transitionend', onTransitionEnd); + }, [element, onTransitionEnd]); + + return ( + mounted && cloneElement(children, { + ref: callbackRef, + className, + }) + ); +}; + +export default Transition; diff --git a/src/common/Transition/index.ts b/src/common/Transition/index.ts new file mode 100644 index 000000000..dd0f32cda --- /dev/null +++ b/src/common/Transition/index.ts @@ -0,0 +1,2 @@ +import Transition from './Transition'; +export default Transition; diff --git a/src/common/animations.less b/src/common/animations.less index 4b0776d16..6ca879343 100644 --- a/src/common/animations.less +++ b/src/common/animations.less @@ -38,4 +38,17 @@ 100% { transform: translateY(0%); } +} + +.slide-left-enter { + transform: translateX(100%); +} + +.slide-left-active { + transform: translateX(0%); + transition: transform 0.3s cubic-bezier(0.32, 0, 0.67, 0); +} + +.slide-left-exit { + transform: translateX(100%); } \ No newline at end of file diff --git a/src/common/index.js b/src/common/index.js index 45eb0ef41..da5462f0e 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -29,6 +29,7 @@ const Slider = require('./Slider'); const { default: TextInput } = require('./TextInput'); const { ToastProvider, useToast } = require('./Toast'); const { TooltipProvider, Tooltip } = require('./Tooltips'); +const { default: Transition } = require('./Transition'); const Video = require('./Video'); const comparatorWithPriorities = require('./comparatorWithPriorities'); const CONSTANTS = require('./CONSTANTS'); @@ -36,6 +37,7 @@ const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender'); const getVisibleChildrenRange = require('./getVisibleChildrenRange'); const interfaceLanguages = require('./interfaceLanguages.json'); const languageNames = require('./languageNames.json'); +const languages = require('./languages'); const routesRegexp = require('./routesRegexp'); const useAnimationFrame = require('./useAnimationFrame'); const useBinaryState = require('./useBinaryState'); @@ -84,6 +86,7 @@ module.exports = { useToast, TooltipProvider, Tooltip, + Transition, Video, comparatorWithPriorities, CONSTANTS, @@ -92,6 +95,7 @@ module.exports = { getVisibleChildrenRange, interfaceLanguages, languageNames, + languages, routesRegexp, useAnimationFrame, useBinaryState, diff --git a/src/common/languages.ts b/src/common/languages.ts new file mode 100644 index 000000000..eea073bc7 --- /dev/null +++ b/src/common/languages.ts @@ -0,0 +1,25 @@ +import langs from 'langs'; + +const all = langs.all().map((lang) => ({ + ...lang, + code: lang['2'], + label: lang.local, + alpha2: lang['1'], + alpha3: [lang['2'], lang['2B'], lang['2T'], lang['3']], + locale: lang['locale'], +})); + +const find = (code: string) => { + return all.find(({ alpha2, alpha3, locale }) => [alpha2, ...alpha3, locale].includes(code)); +}; + +const label = (code: string) => { + const language = find(code); + return language?.label ?? code; +}; + +export { + all, + find, + label, +}; diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index 840664c8b..56f9c0d08 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -9,16 +9,20 @@ const { Button, Image, Multiselect } = require('stremio/common'); const { useServices } = require('stremio/services'); const Stream = require('./Stream'); const styles = require('./styles'); +const { usePlatform } = require('stremio/common'); const ALL_ADDONS_KEY = 'ALL'; const StreamsList = ({ className, video, ...props }) => { const { t } = useTranslation(); const { core } = useServices(); + const platform = usePlatform(); + const streamsContainerRef = React.useRef(null); const [selectedAddon, setSelectedAddon] = React.useState(ALL_ADDONS_KEY); const onAddonSelected = React.useCallback((event) => { + streamsContainerRef.current.scrollTo({ top: 0, left: 0, behavior: platform.name === 'ios' ? 'smooth' : 'instant' }); setSelectedAddon(event.value); - }, []); + }, [platform]); const backButtonOnClick = React.useCallback(() => { if (video.deepLinks && typeof video.deepLinks.metaDetailsVideos === 'string') { window.location.replace(video.deepLinks.metaDetailsVideos + ( @@ -142,7 +146,7 @@ const StreamsList = ({ className, video, ...props }) => { : null } -
+
{filteredStreams.map((stream, index) => ( :first-child { margin-right: 0.5rem; } diff --git a/src/routes/Player/AudioMenu/AudioMenu.less b/src/routes/Player/AudioMenu/AudioMenu.less new file mode 100644 index 000000000..344ec13e7 --- /dev/null +++ b/src/routes/Player/AudioMenu/AudioMenu.less @@ -0,0 +1,78 @@ +.audio-menu { + display: flex; + flex-direction: row; + + .container { + flex: none; + align-self: stretch; + display: flex; + flex-direction: column; + width: 16rem; + + .header { + flex: none; + align-self: stretch; + padding: 1.5rem 2rem; + font-weight: 700; + color: var(--primary-foreground-color); + } + + .list { + flex: 1; + align-self: stretch; + display: flex; + flex-direction: column; + gap: 0.5rem; + overflow-y: auto; + padding: 0 1rem; + padding-bottom: 1rem; + + .option { + flex: none; + display: flex; + flex-direction: row; + align-items: center; + gap: 1rem; + height: 4rem; + padding: 0 1.5rem; + border-radius: var(--border-radius); + + &:global(.selected), &:hover { + background-color: var(--overlay-color); + } + + .info { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.25rem; + + .lang, .label { + flex: auto; + text-wrap: nowrap; + text-overflow: ellipsis; + } + + .lang { + font-size: 1.1rem; + line-height: 1.5rem; + color: var(--primary-foreground-color); + } + + .label { + font-size: 0.9rem; + color: var(--color-placeholder-text); + } + } + + .icon { + flex: none; + width: 0.5rem; + height: 0.5rem; + border-radius: 100%; + background-color: var(--secondary-accent-color); + } + } + } + } +} \ No newline at end of file diff --git a/src/routes/Player/AudioMenu/AudioMenu.tsx b/src/routes/Player/AudioMenu/AudioMenu.tsx new file mode 100644 index 000000000..92f9c3c01 --- /dev/null +++ b/src/routes/Player/AudioMenu/AudioMenu.tsx @@ -0,0 +1,66 @@ +import React, { MouseEvent, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import classNames from 'classnames'; +import { Button, languages } from 'stremio/common'; +import styles from './AudioMenu.less'; + +type Props = { + className: string, + selectedAudioTrackId: string | null, + audioTracks: AudioTrack[], + onAudioTrackSelected: (id: string) => void, +}; + +const AudioMenu = ({ className, selectedAudioTrackId, audioTracks, onAudioTrackSelected }: Props) => { + const { t } = useTranslation(); + + const onAudioTrackClick = useCallback(({ currentTarget }: MouseEvent) => { + const id = currentTarget.getAttribute('data-id')!; + onAudioTrackSelected && onAudioTrackSelected(id); + }, [onAudioTrackSelected]); + + const onMouseDown = (event: MouseEvent) => { + // @ts-expect-error: Property 'audioMenuClosePrevented' does not exist on type 'MouseEvent'. + event.nativeEvent.audioMenuClosePrevented = true; + }; + + return ( +
+
+
+ { t('AUDIO_TRACKS') } +
+
+ { + audioTracks.map(({ id, label, lang }, index) => ( + + )) + } +
+
+
+ ); +}; + +export default AudioMenu; diff --git a/src/routes/Player/AudioMenu/index.ts b/src/routes/Player/AudioMenu/index.ts new file mode 100644 index 000000000..265490872 --- /dev/null +++ b/src/routes/Player/AudioMenu/index.ts @@ -0,0 +1,2 @@ +import AudioMenu from './AudioMenu'; +export default AudioMenu; diff --git a/src/routes/Player/ControlBar/ControlBar.js b/src/routes/Player/ControlBar/ControlBar.js index a68a542bf..a5fe1adf9 100644 --- a/src/routes/Player/ControlBar/ControlBar.js +++ b/src/routes/Player/ControlBar/ControlBar.js @@ -35,9 +35,9 @@ const ControlBar = ({ onVolumeChangeRequested, onSeekRequested, onToggleSubtitlesMenu, - onToggleInfoMenu, + onToggleAudioMenu, onToggleSpeedMenu, - onToggleVideosMenu, + onToggleSideDrawer, onToggleOptionsMenu, onToggleStatisticsMenu, ...props @@ -48,8 +48,8 @@ const ControlBar = ({ const onSubtitlesButtonMouseDown = React.useCallback((event) => { event.nativeEvent.subtitlesMenuClosePrevented = true; }, []); - const onInfoButtonMouseDown = React.useCallback((event) => { - event.nativeEvent.infoMenuClosePrevented = true; + const onAudioButtonMouseDown = React.useCallback((event) => { + event.nativeEvent.audioMenuClosePrevented = true; }, []); const onSpeedButtonMouseDown = React.useCallback((event) => { event.nativeEvent.speedMenuClosePrevented = true; @@ -151,18 +151,18 @@ const ControlBar = ({ - - + { metaItem?.content?.videos?.length > 0 ? - : @@ -200,9 +200,9 @@ ControlBar.propTypes = { onVolumeChangeRequested: PropTypes.func, onSeekRequested: PropTypes.func, onToggleSubtitlesMenu: PropTypes.func, - onToggleInfoMenu: PropTypes.func, + onToggleAudioMenu: PropTypes.func, onToggleSpeedMenu: PropTypes.func, - onToggleVideosMenu: PropTypes.func, + onToggleSideDrawer: PropTypes.func, onToggleOptionsMenu: PropTypes.func, onToggleStatisticsMenu: PropTypes.func, }; diff --git a/src/routes/Player/ControlBar/styles.less b/src/routes/Player/ControlBar/styles.less index 147a3f0a2..bcfbbdaef 100644 --- a/src/routes/Player/ControlBar/styles.less +++ b/src/routes/Player/ControlBar/styles.less @@ -100,14 +100,16 @@ .control-bar-buttons-menu-container { position: absolute; - right: 0.15rem; + right: 0rem; bottom: 4.5rem; - flex-direction: column; padding: 0.5rem; + margin: 0.5rem; + max-width: calc(100dvw - 1rem); border-radius: var(--border-radius); background-color: var(--modal-background-color); box-shadow: 0 1.35rem 2.7rem @color-background-dark5-40, 0 1.1rem 0.85rem @color-background-dark5-20; + overflow-x: auto; &:not(:global(.open)) { display: none; diff --git a/src/routes/Player/InfoMenu/InfoMenu.js b/src/routes/Player/InfoMenu/InfoMenu.js deleted file mode 100644 index eec5e7286..000000000 --- a/src/routes/Player/InfoMenu/InfoMenu.js +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const React = require('react'); -const PropTypes = require('prop-types'); -const classnames = require('classnames'); -// const Stream = require('stremio/routes/MetaDetails/StreamsList/Stream'); -// const AddonDetails = require('stremio/common/AddonDetailsModal/AddonDetails'); -const { MetaPreview, CONSTANTS } = require('stremio/common'); -const styles = require('./styles'); - -const InfoMenu = ({ className, ...props }) => { - const metaItem = React.useMemo(() => { - return props.metaItem !== null ? - { - ...props.metaItem, - links: props.metaItem.links.filter(({ category }) => category === CONSTANTS.SHARE_LINK_CATEGORY) - } - : - null; - }, [props.metaItem]); - const onMouseDown = React.useCallback((event) => { - event.nativeEvent.infoMenuClosePrevented = true; - }, []); - return ( -
- { - metaItem !== null ? - - : - null - } - {/* { - props.stream !== null ? - - : - null - } */} - {/* { - props.addon !== null ? - - : - null - } */} -
- ); -}; - -InfoMenu.propTypes = { - className: PropTypes.string, - metaItem: PropTypes.object, - addon: PropTypes.object, - stream: PropTypes.object -}; - -module.exports = InfoMenu; diff --git a/src/routes/Player/InfoMenu/index.js b/src/routes/Player/InfoMenu/index.js deleted file mode 100644 index 8dae65490..000000000 --- a/src/routes/Player/InfoMenu/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const InfoMenu = require('./InfoMenu'); - -module.exports = InfoMenu; diff --git a/src/routes/Player/InfoMenu/styles.less b/src/routes/Player/InfoMenu/styles.less deleted file mode 100644 index 32a72b77d..000000000 --- a/src/routes/Player/InfoMenu/styles.less +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -.info-menu-container { - width: 30rem; - padding: 2rem; - - .stream { - pointer-events: none; - } -} \ No newline at end of file diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index cdde60474..2d48f326d 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -8,18 +8,19 @@ const langs = require('langs'); const { useTranslation } = require('react-i18next'); const { useRouteFocused } = require('stremio-router'); const { useServices } = require('stremio/services'); -const { HorizontalNavBar, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common'); +const { HorizontalNavBar, Transition, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common'); const BufferingLoader = require('./BufferingLoader'); const VolumeChangeIndicator = require('./VolumeChangeIndicator'); const Error = require('./Error'); const ControlBar = require('./ControlBar'); const NextVideoPopup = require('./NextVideoPopup'); const StatisticsMenu = require('./StatisticsMenu'); -const InfoMenu = require('./InfoMenu'); const OptionsMenu = require('./OptionsMenu'); -const VideosMenu = require('./VideosMenu'); const SubtitlesMenu = require('./SubtitlesMenu'); +const { default: AudioMenu } = require('./AudioMenu'); const SpeedMenu = require('./SpeedMenu'); +const { default: SideDrawerButton } = require('./SideDrawerButton'); +const { default: SideDrawer } = require('./SideDrawer'); const usePlayer = require('./usePlayer'); const useSettings = require('./useSettings'); const useStatistics = require('./useStatistics'); @@ -54,23 +55,23 @@ const Player = ({ urlParams, queryParams }) => { const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false); const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false); - const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false); + const [audioMenuOpen, , closeAudioMenu, toggleAudioMenu] = useBinaryState(false); const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false); - const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false); const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] = useBinaryState(false); const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false); + const [sideDrawerOpen, , closeSideDrawer, toggleSideDrawer] = useBinaryState(false); const menusOpen = React.useMemo(() => { - return optionsMenuOpen || subtitlesMenuOpen || infoMenuOpen || speedMenuOpen || videosMenuOpen || statisticsMenuOpen; - }, [optionsMenuOpen, subtitlesMenuOpen, infoMenuOpen, speedMenuOpen, videosMenuOpen, statisticsMenuOpen]); + return optionsMenuOpen || subtitlesMenuOpen || audioMenuOpen || speedMenuOpen || statisticsMenuOpen || sideDrawerOpen; + }, [optionsMenuOpen, subtitlesMenuOpen, audioMenuOpen, speedMenuOpen, statisticsMenuOpen, sideDrawerOpen]); const closeMenus = React.useCallback(() => { closeOptionsMenu(); closeSubtitlesMenu(); - closeInfoMenu(); + closeAudioMenu(); closeSpeedMenu(); - closeVideosMenu(); closeStatisticsMenu(); + closeSideDrawer(); }, []); const overlayHidden = React.useMemo(() => { @@ -237,18 +238,17 @@ const Player = ({ urlParams, queryParams }) => { if (!event.nativeEvent.subtitlesMenuClosePrevented) { closeSubtitlesMenu(); } - if (!event.nativeEvent.infoMenuClosePrevented) { - closeInfoMenu(); + if (!event.nativeEvent.audioMenuClosePrevented) { + closeAudioMenu(); } if (!event.nativeEvent.speedMenuClosePrevented) { closeSpeedMenu(); } - if (!event.nativeEvent.videosMenuClosePrevented) { - closeVideosMenu(); - } if (!event.nativeEvent.statisticsMenuClosePrevented) { closeStatisticsMenu(); } + + closeSideDrawer(); }, []); const onContainerMouseMove = React.useCallback((event) => { @@ -405,18 +405,16 @@ const Player = ({ urlParams, queryParams }) => { React.useEffect(() => { if ((!Array.isArray(video.state.subtitlesTracks) || video.state.subtitlesTracks.length === 0) && - (!Array.isArray(video.state.extraSubtitlesTracks) || video.state.extraSubtitlesTracks.length === 0) && - (!Array.isArray(video.state.audioTracks) || video.state.audioTracks.length === 0)) { + (!Array.isArray(video.state.extraSubtitlesTracks) || video.state.extraSubtitlesTracks.length === 0)) { closeSubtitlesMenu(); } - }, [video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks]); + }, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks]); React.useEffect(() => { - if (player.metaItem === null || player.metaItem.type !== 'Ready') { - closeInfoMenu(); - closeVideosMenu(); + if (!Array.isArray(video.state.audioTracks) || video.state.audioTracks.length === 0) { + closeAudioMenu(); } - }, [player.metaItem]); + }, [video.state.audioTracks]); React.useEffect(() => { if (video.state.playbackSpeed === null) { @@ -510,17 +508,24 @@ const Player = ({ urlParams, queryParams }) => { case 'KeyS': { closeMenus(); if ((Array.isArray(video.state.subtitlesTracks) && video.state.subtitlesTracks.length > 0) || - (Array.isArray(video.state.extraSubtitlesTracks) && video.state.extraSubtitlesTracks.length > 0) || - (Array.isArray(video.state.audioTracks) && video.state.audioTracks.length > 0)) { + (Array.isArray(video.state.extraSubtitlesTracks) && video.state.extraSubtitlesTracks.length > 0)) { toggleSubtitlesMenu(); } break; } + case 'KeyA': { + closeMenus(); + if (Array.isArray(video.state.audioTracks) && video.state.audioTracks.length > 0) { + toggleAudioMenu(); + } + + break; + } case 'KeyI': { closeMenus(); if (player.metaItem !== null && player.metaItem.type === 'Ready') { - toggleInfoMenu(); + toggleSideDrawer(); } break; @@ -533,14 +538,6 @@ const Player = ({ urlParams, queryParams }) => { break; } - case 'KeyV': { - closeMenus(); - if (player.metaItem !== null && player.metaItem.type === 'Ready' && player.metaItem?.content?.videos?.length > 0) { - toggleVideosMenu(); - } - - break; - } case 'KeyD': { closeMenus(); if (streamingServer.statistics !== null && streamingServer.statistics.type !== 'Err' && player.selected && typeof player.selected.stream.infoHash === 'string' && typeof player.selected.stream.fileIdx === 'number') { @@ -581,7 +578,7 @@ const Player = ({ urlParams, queryParams }) => { window.removeEventListener('keyup', onKeyUp); window.removeEventListener('wheel', onWheel); }; - }, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, settings.seekShortTimeDuration, routeFocused, menusOpen, nextVideoPopupOpen, video.state.paused, video.state.time, video.state.volume, video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, video.state.playbackSpeed, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu, toggleStatisticsMenu]); + }, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, settings.seekShortTimeDuration, routeFocused, menusOpen, nextVideoPopupOpen, video.state.paused, video.state.time, video.state.volume, video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, video.state.playbackSpeed, toggleSubtitlesMenu, toggleStatisticsMenu, toggleSideDrawer]); React.useEffect(() => { video.events.on('error', onError); @@ -666,6 +663,15 @@ const Player = ({ urlParams, queryParams }) => { onMouseMove={onBarMouseMove} onMouseOver={onBarMouseMove} /> + { + player.metaItem?.type === 'Ready' ? + + : + null + } { onSeekRequested={onSeekRequested} onToggleOptionsMenu={toggleOptionsMenu} onToggleSubtitlesMenu={toggleSubtitlesMenu} - onToggleInfoMenu={toggleInfoMenu} + onToggleAudioMenu={toggleAudioMenu} onToggleSpeedMenu={toggleSpeedMenu} - onToggleVideosMenu={toggleVideosMenu} onToggleStatisticsMenu={toggleStatisticsMenu} + onToggleSideDrawer={toggleSideDrawer} onMouseMove={onBarMouseMove} onMouseOver={onBarMouseMove} /> @@ -718,12 +724,18 @@ const Player = ({ urlParams, queryParams }) => { : null } + + + { subtitlesMenuOpen ? { extraSubtitlesSize={video.state.extraSubtitlesSize} onSubtitlesTrackSelected={onSubtitlesTrackSelected} onExtraSubtitlesTrackSelected={onExtraSubtitlesTrackSelected} - onAudioTrackSelected={onAudioTrackSelected} onSubtitlesOffsetChanged={onSubtitlesOffsetChanged} onSubtitlesSizeChanged={onSubtitlesSizeChanged} onExtraSubtitlesOffsetChanged={onSubtitlesOffsetChanged} @@ -746,12 +757,12 @@ const Player = ({ urlParams, queryParams }) => { null } { - infoMenuOpen ? - : null @@ -766,16 +777,6 @@ const Player = ({ urlParams, queryParams }) => { : null } - { - videosMenuOpen ? - - : - null - } { optionsMenuOpen ? void; +}; + +const SideDrawer = memo(forwardRef(({ seriesInfo, className, closeSideDrawer, ...props }: Props, ref) => { + const { core } = useServices(); + const [season, setSeason] = useState(seriesInfo?.season); + const metaItem = useMemo(() => { + return seriesInfo ? + { + ...props.metaItem, + links: props.metaItem.links.filter(({ category }) => category === CONSTANTS.SHARE_LINK_CATEGORY) + } + : + props.metaItem; + }, [props.metaItem]); + const videos = useMemo(() => { + return Array.isArray(metaItem.videos) ? + metaItem.videos.filter((video) => video.season === season) + : + metaItem.videos; + }, [metaItem, season]); + const seasons = useMemo(() => { + return props.metaItem.videos + .map(({ season }) => season) + .filter((season, index, seasons) => { + return seasons.indexOf(season) === index; + }) + .sort((a, b) => (a || Number.MAX_SAFE_INTEGER) - (b || Number.MAX_SAFE_INTEGER)); + }, [props.metaItem.videos]); + + const seasonOnSelect = useCallback((event: { value: string }) => { + setSeason(parseInt(event.value)); + }, []); + + const onMarkVideoAsWatched = useCallback((video: Video, watched: boolean) => { + core.transport.dispatch({ + action: 'Player', + args: { + action: 'MarkVideoAsWatched', + args: [video, !watched] + } + }); + }, []); + + const onMouseDown = (event: React.MouseEvent) => { + event.stopPropagation(); + }; + + return ( +
+
+ +
+
+ +
+ { + seriesInfo ? +
+ +
+ {videos.map((video, index) => ( +
+
+ : null + } + +
+ ); +})); + +export default SideDrawer; diff --git a/src/routes/Player/SideDrawer/index.ts b/src/routes/Player/SideDrawer/index.ts new file mode 100644 index 000000000..e19e268d3 --- /dev/null +++ b/src/routes/Player/SideDrawer/index.ts @@ -0,0 +1,2 @@ +import SideDrawer from './SideDrawer'; +export default SideDrawer; diff --git a/src/routes/Player/SideDrawerButton/SideDrawerButton.less b/src/routes/Player/SideDrawerButton/SideDrawerButton.less new file mode 100644 index 000000000..e7e86027f --- /dev/null +++ b/src/routes/Player/SideDrawerButton/SideDrawerButton.less @@ -0,0 +1,47 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +@import (reference) '~stremio/common/screen-sizes.less'; + +.side-drawer-button { + height: 12.5rem; + width: 7.5rem; + display: flex; + align-items: center; + justify-content: start; + padding-left: 0.5rem; + border-radius: 50%; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + background-color: var(--modal-background-color); + -webkit-tap-highlight-color: transparent; + cursor: pointer; + opacity: 1; + will-change: opacity; + transition: opacity 0.3s ease-in-out, border-radius 0.3s ease-in-out; + + .icon { + position: relative; + width: 2.5rem; + height: 2.5rem; + color: var(--primary-foreground-color); + opacity: 0.6; + transition: 0.3s opacity ease-in-out; + } + + &:hover { + .icon { + opacity: 1; + } + } +} + +@media screen and (max-width: @xsmall) { + .side-drawer-button { + height: 8rem; + width: 4.5rem; + + .icon { + width: 2rem; + height: 2rem; + } + } +} \ No newline at end of file diff --git a/src/routes/Player/SideDrawerButton/SideDrawerButton.tsx b/src/routes/Player/SideDrawerButton/SideDrawerButton.tsx new file mode 100644 index 000000000..81409f434 --- /dev/null +++ b/src/routes/Player/SideDrawerButton/SideDrawerButton.tsx @@ -0,0 +1,21 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React from 'react'; +import classNames from 'classnames'; +import Icon from '@stremio/stremio-icons/react'; +import styles from './SideDrawerButton.less'; + +type Props = { + className: string, + onClick: () => void, +}; + +const SideDrawerButton = ({ className, onClick }: Props) => { + return ( +
+ +
+ ); +}; + +export default SideDrawerButton; diff --git a/src/routes/Player/SideDrawerButton/index.ts b/src/routes/Player/SideDrawerButton/index.ts new file mode 100644 index 000000000..73585e555 --- /dev/null +++ b/src/routes/Player/SideDrawerButton/index.ts @@ -0,0 +1,2 @@ +import SideDrawerButton from './SideDrawerButton'; +export default SideDrawerButton; diff --git a/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js b/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js index c65cb9c9e..1a51d1363 100644 --- a/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js +++ b/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js @@ -3,7 +3,7 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); -const { Button, CONSTANTS, comparatorWithPriorities, languageNames } = require('stremio/common'); +const { Button, CONSTANTS, comparatorWithPriorities, languages } = require('stremio/common'); const DiscreteSelectInput = require('./DiscreteSelectInput'); const styles = require('./styles'); const { t } = require('i18next'); @@ -129,49 +129,23 @@ const SubtitlesMenu = React.memo((props) => { const onSubtitlesOffsetChanged = React.useCallback((event) => { const delta = event.value === 'increment' ? 1 : -1; if (typeof props.selectedSubtitlesTrackId === 'string') { - if (props.extraSubtitlesOffset !== null && !isNaN(props.extraSubtitlesOffset)) { - const offset = Math.max(0, Math.min(100, Math.floor(props.extraSubtitlesOffset + delta))); - if (typeof props.onExtraSubtitlesOffsetChanged === 'function') { - props.onExtraSubtitlesOffsetChanged(offset); - } - } - } else if (typeof props.selectedExtraSubtitlesTrackId === 'string') { if (props.subtitlesOffset !== null && !isNaN(props.subtitlesOffset)) { const offset = Math.max(0, Math.min(100, Math.floor(props.subtitlesOffset + delta))); if (typeof props.onSubtitlesOffsetChanged === 'function') { props.onSubtitlesOffsetChanged(offset); } } + } else if (typeof props.selectedExtraSubtitlesTrackId === 'string') { + if (props.extraSubtitlesOffset !== null && !isNaN(props.extraSubtitlesOffset)) { + const offset = Math.max(0, Math.min(100, Math.floor(props.extraSubtitlesOffset + delta))); + if (typeof props.onExtraSubtitlesOffsetChanged === 'function') { + props.onExtraSubtitlesOffsetChanged(offset); + } + } } }, [props.selectedSubtitlesTrackId, props.selectedExtraSubtitlesTrackId, props.subtitlesOffset, props.extraSubtitlesOffset, props.onSubtitlesOffsetChanged, props.onExtraSubtitlesOffsetChanged]); - const audioTrackOnClick = React.useCallback((event) => { - if (typeof props.onAudioTrackSelected === 'function') { - props.onAudioTrackSelected(event.currentTarget.dataset.id); - } - }, [props.onAudioTrackSelected]); return (
- { - Array.isArray(props.audioTracks) && props.audioTracks.length > 1 ? -
-
Audio Languages
-
- {props.audioTracks.map(({ id, label, lang }, index) => ( - - ))} -
-
- : - null - }
{ t('PLAYER_SUBTITLES_LANGUAGES') }
@@ -185,8 +159,8 @@ const SubtitlesMenu = React.memo((props) => { } {subtitlesLanguages.map((lang, index) => ( -
+
+
+
{ t('SETTINGS_SHORTCUT_MENU_AUDIO') }
+
+
+ A +
+
{ t('SETTINGS_SHORTCUT_MENU_INFO') }
diff --git a/src/types/MetaItem.d.ts b/src/types/MetaItem.d.ts index 0d692a580..56e7db3c7 100644 --- a/src/types/MetaItem.d.ts +++ b/src/types/MetaItem.d.ts @@ -15,7 +15,7 @@ type MetaItemPreview = { posterShape: PosterShape, releaseInfo: string | null, runtime: string | null, - released: string | null, + released: Date | null | undefined, trailerStreams: TrailerStream[], links: Link[], behaviorHints: BehaviorHints, diff --git a/src/types/Video.d.ts b/src/types/Video.d.ts index 92bc704f5..7ed3d0fac 100644 --- a/src/types/Video.d.ts +++ b/src/types/Video.d.ts @@ -14,4 +14,9 @@ type Video = { episode?: number, streams: Stream[], trailerStreams: TrailerStream[], + watched: boolean, + progress: number, + upcoming: boolean, + deepLinks: VideoDeepLinks, + scheduled: boolean, }; diff --git a/src/types/models/Player.d.ts b/src/types/models/Player.d.ts index 6664e83f5..2add1456b 100644 --- a/src/types/models/Player.d.ts +++ b/src/types/models/Player.d.ts @@ -25,6 +25,11 @@ type Subtitle = { url: string, }; +type SeriesInfo = { + episode: number, + season: number, +}; + type Player = { addon: Addon | null, libraryItem: LibraryItemPlayer | null, @@ -36,10 +41,7 @@ type Player = { streamRequest: ResourceRequest, subtitlesPath: ResourceRequestPath, } | null, - seriesInfo: { - season: number, - episode: number, - } | null, + seriesInfo: SeriesInfo | null, subtitles: Subtitle[], title: string | null, }; diff --git a/src/types/types.d.ts b/src/types/types.d.ts index 8b7a627b6..8f6d55730 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -61,3 +61,10 @@ type Catalog = { installed?: boolean, deepLinks?: D, }; + +type AudioTrack = { + id: string, + label: string, + lang: string, + origin: string, +}; diff --git a/webpack.config.js b/webpack.config.js index 1b6c6601b..b23ee4c15 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,8 +1,10 @@ // Copyright (C) 2017-2023 Smart code 203358507 const path = require('path'); +const os = require('os'); const { execSync } = require('child_process'); const webpack = require('webpack'); +const threadLoader = require('thread-loader'); const HtmlWebPackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); @@ -14,6 +16,25 @@ const pachageJson = require('./package.json'); const COMMIT_HASH = execSync('git rev-parse HEAD').toString().trim(); +const THREAD_LOADER = { + loader: 'thread-loader', + options: { + name: 'shared-pool', + workers: os.cpus().length, + }, +}; + +threadLoader.warmup( + THREAD_LOADER.options, + [ + 'babel-loader', + 'ts-loader', + 'css-loader', + 'postcss-loader', + 'less-loader', + ], +); + module.exports = (env, argv) => ({ mode: argv.mode, devtool: argv.mode === 'production' ? 'source-map' : 'eval-source-map', @@ -30,20 +51,31 @@ module.exports = (env, argv) => ({ { test: /\.js$/, exclude: /node_modules/, - use: { - loader: 'babel-loader', - options: { - presets: [ - '@babel/preset-env', - '@babel/preset-react' - ], + use: [ + THREAD_LOADER, + { + loader: 'babel-loader', + options: { + presets: [ + '@babel/preset-env', + '@babel/preset-react' + ], + } } - } + ] }, { test: /\.(ts|tsx)$/, exclude: /node_modules/, - use: 'ts-loader', + use: [ + THREAD_LOADER, + { + loader: 'ts-loader', + options: { + happyPackMode: true, + } + } + ] }, { test: /\.less$/, @@ -55,6 +87,7 @@ module.exports = (env, argv) => ({ esModule: false } }, + THREAD_LOADER, { loader: 'css-loader', options: {