From 657f9cd29e5b9b0690aee8e899f796d9be6eb380 Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 23 May 2025 16:34:41 +0300 Subject: [PATCH 01/39] feat: added scanner for missed translations --- .gitignore | 3 +- package-lock.json | 38 ++++++++++++++++ package.json | 4 +- scan-i18n-report.js | 107 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 scan-i18n-report.js diff --git a/.gitignore b/.gitignore index 71947782e..803977723 100755 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /yarn.lock /npm-debug.log .DS_Store -.prettierignore \ No newline at end of file +.prettierignore +i18n-report.csv \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 79e66d642..defb06643 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "mini-css-extract-plugin": "2.9.2", "postcss-loader": "8.1.1", "readdirp": "4.0.2", + "recast": "0.23.11", "terser-webpack-plugin": "5.3.10", "thread-loader": "^4.0.4", "ts-loader": "^9.5.1", @@ -4786,6 +4787,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -12497,6 +12511,23 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -13976,6 +14007,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinycolor2": { "version": "1.6.0", "dev": true, diff --git a/package.json b/package.json index 5ed9368ab..53682f6e6 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "start-prod": "webpack serve --mode production", "build": "webpack --mode production", "test": "jest", - "lint": "eslint src" + "lint": "eslint src", + "scan-translations": "node scan-i18n-report.js" }, "dependencies": { "@babel/runtime": "7.26.0", @@ -70,6 +71,7 @@ "mini-css-extract-plugin": "2.9.2", "postcss-loader": "8.1.1", "readdirp": "4.0.2", + "recast": "0.23.11", "terser-webpack-plugin": "5.3.10", "thread-loader": "^4.0.4", "ts-loader": "^9.5.1", diff --git a/scan-i18n-report.js b/scan-i18n-report.js new file mode 100644 index 000000000..3294d34e0 --- /dev/null +++ b/scan-i18n-report.js @@ -0,0 +1,107 @@ +const fs = require('fs'); +const path = require('path'); +const recast = require('recast'); +const babelParser = require('@babel/parser'); + +const directoryToScan = './src'; +const report = []; + +function toKey(str) { + return str + .toLowerCase() + .replace(/[^a-z0-9\s]/g, '') + .replace(/\s+/g, '_') + .slice(0, 40); +} + +function scanFile(filePath) { + try { + const code = fs.readFileSync(filePath, 'utf8'); + const ast = babelParser.parse(code, { + sourceType: 'module', + plugins: [ + 'jsx', + 'typescript', + 'classProperties', + 'objectRestSpread', + 'optionalChaining', + 'nullishCoalescingOperator', + ], + errorRecovery: true, + }); + + recast.types.visit(ast, { + // Text directly inside JSX + visitJSXText(path) { + const text = path.node.value.trim(); + if (text.length > 1 && /\w/.test(text)) { + const loc = path.node.loc?.start || { line: 0 }; + report.push({ + file: filePath, + line: loc.line, + string: text, + key: toKey(text), + }); + } + this.traverse(path); + }, + + // { "hello" } style + visitJSXExpressionContainer(path) { + const expr = path.node.expression; + + // Skip expressions that call t() + if ( + expr.type === 'CallExpression' && + expr.callee.type === 'Identifier' && + expr.callee.name === 't' + ) { + return false; + } + + // Find only { "text" } expressions + if (expr.type === 'StringLiteral') { + const parent = path.parentPath.node; + if (parent.type === 'JSXElement') { + const loc = path.node.loc?.start || { line: 0 }; + report.push({ + file: filePath, + line: loc.line, + string: expr.value, + key: toKey(expr.value), + }); + } + } + + this.traverse(path); + } + }); + + } catch (err) { + console.warn(`❌ Skipping ${filePath}: ${err.message}`); + } +} + +function walk(dir) { + fs.readdirSync(dir).forEach((file) => { + const fullPath = path.join(dir, file); + if (fs.statSync(fullPath).isDirectory()) { + walk(fullPath); + } else if (/\.(js|jsx|ts|tsx)$/.test(file)) { + console.log('Scanning file:', fullPath); + scanFile(fullPath); + } + }); +} + +function writeReport() { + const header = `File,Line,Hardcoded String,Suggested Key\n`; + const rows = report.map((row) => + `"${row.file}",${row.line},"${row.string.replace(/"/g, '""')}","${row.key}"` + ); + fs.writeFileSync('i18n-report.csv', header + rows.join('\n')); + console.log('✅ Report written to i18n-report.csv'); +} + +walk(directoryToScan); +writeReport(); From aba31c8ceb45a07209fb22588abc5b02850f7135 Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 23 May 2025 17:05:01 +0300 Subject: [PATCH 02/39] fix(Addons): added missing translations --- .../AddonDetails/AddonDetails.js | 6 ++++-- .../AddonDetailsModal/AddonDetailsModal.js | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/components/AddonDetailsModal/AddonDetails/AddonDetails.js b/src/components/AddonDetailsModal/AddonDetails/AddonDetails.js index da5fde198..c6722f294 100644 --- a/src/components/AddonDetailsModal/AddonDetails/AddonDetails.js +++ b/src/components/AddonDetailsModal/AddonDetails/AddonDetails.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); @@ -8,6 +9,7 @@ const { default: Image } = require('stremio/components/Image'); const styles = require('./styles'); const AddonDetails = ({ className, id, name, version, logo, description, types, transportUrl, official }) => { + const t = useTranslation(); const renderLogoFallback = React.useCallback(() => ( ), []); @@ -50,7 +52,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types, { Array.isArray(types) && types.length > 0 ?
- Supported types: + {`${t('ADDON_SUPPORTED_TYPES')}:`} { types.length === 1 ? @@ -66,7 +68,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types, { !official ?
-
Using third-party add-ons will always be subject to your responsibility and the governing law of the jurisdiction you are located.
+
{t('ADDON_DISCLAIMER')}
: null diff --git a/src/components/AddonDetailsModal/AddonDetailsModal.js b/src/components/AddonDetailsModal/AddonDetailsModal.js index 332eab364..2dcea3e3b 100644 --- a/src/components/AddonDetailsModal/AddonDetailsModal.js +++ b/src/components/AddonDetailsModal/AddonDetailsModal.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const ModalDialog = require('stremio/components/ModalDialog'); const { withCoreSuspender } = require('stremio/common/CoreSuspender'); @@ -43,6 +44,7 @@ function withRemoteAndLocalAddon(AddonDetails) { } const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { + const { t } = useTranslation(); const { core } = useServices(); const platform = usePlatform(); const addonDetails = useAddonDetails(transportUrl); @@ -145,17 +147,17 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { { addonDetails.selected === null ?
- Loading addon manifest + {t('ADDON_LOADING_MANIFEST')}
: addonDetails.remoteAddon === null || addonDetails.remoteAddon.content.type === 'Loading' ?
- Loading addon manifest from {addonDetails.selected.transportUrl} + {t('ADDON_LOADING_MANIFEST_FROM', { origin: addonDetails.selected.transportUrl})}
: addonDetails.remoteAddon.content.type === 'Err' && addonDetails.localAddon === null ?
- Failed to get addon manifest from {addonDetails.selected.transportUrl} + {t('ADDON_LOADING_MANIFEST_FAILED', {origin: addonDetails.selected.transportUrl})}
{addonDetails.remoteAddon.content.content.message}
: @@ -174,17 +176,18 @@ AddonDetailsModal.propTypes = { onCloseRequest: PropTypes.func }; -const AddonDetailsModalFallback = ({ onCloseRequest }) => ( - { + const { t } = useTranslation(); + return
- Loading addon manifest + {t('ADDON_LOADING_MANIFEST')}
-
-); +
; +}; AddonDetailsModalFallback.propTypes = AddonDetailsModal.propTypes; From f24ad7d069a0f4d5e9f8d897975780f4c569e75a Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 23 May 2025 17:17:30 +0300 Subject: [PATCH 03/39] fix(Addons): added missing translation keys --- src/routes/Addons/Addons.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/Addons/Addons.js b/src/routes/Addons/Addons.js index 91bcbd27f..2ff1dad26 100644 --- a/src/routes/Addons/Addons.js +++ b/src/routes/Addons/Addons.js @@ -132,12 +132,12 @@ const Addons = ({ urlParams, queryParams }) => { installedAddons.selected !== null ? installedAddons.selectable.types.length === 0 ?
- No addons ware installed! + {t('NO_ADDONS')}
: installedAddons.catalog.length === 0 ?
- No addons ware installed for that type! + {t('NO_ADDONS_FOR_TYPE')}
:
From 2de0a517e0f354d37197ee39d35780e8ccb7ecbf Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 23 May 2025 17:43:06 +0300 Subject: [PATCH 04/39] fix(Multiselect): added missing translation keys --- src/components/Multiselect/Multiselect.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Multiselect/Multiselect.js b/src/components/Multiselect/Multiselect.js index 0e353eef4..c2bf855d1 100644 --- a/src/components/Multiselect/Multiselect.js +++ b/src/components/Multiselect/Multiselect.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); @@ -11,6 +12,7 @@ const useBinaryState = require('stremio/common/useBinaryState'); const styles = require('./styles'); const Multiselect = ({ className, mode, direction, title, disabled, dataset, options, renderLabelContent, renderLabelText, onOpen, onClose, onSelect, ...props }) => { + const { t } = useTranslation(); const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); const filteredOptions = React.useMemo(() => { return Array.isArray(options) ? @@ -122,7 +124,7 @@ const Multiselect = ({ className, mode, direction, title, disabled, dataset, opt )) :
-
No options available
+
{t('NO_OPTIONS')}
}
From 5f81804b006daab2ae4ba9d78563a5970c2fbf19 Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 23 May 2025 18:03:47 +0300 Subject: [PATCH 05/39] fix(MetaDetails): added missing translation keys --- src/components/Video/Video.js | 8 +++++--- .../MetaDetails/VideosList/SeasonsBar/SeasonsBar.js | 8 ++++---- .../SeasonsBarPlaceholder/SeasonsBarPlaceholder.js | 8 +++++--- src/routes/MetaDetails/VideosList/VideosList.js | 2 +- src/routes/Player/NextVideoPopup/NextVideoPopup.js | 8 +++++--- src/routes/Player/StatisticsMenu/StatisticsMenu.js | 12 +++++++----- 6 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/components/Video/Video.js b/src/components/Video/Video.js index 94fbefcc7..92b223e0c 100644 --- a/src/components/Video/Video.js +++ b/src/components/Video/Video.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { t } = require('i18next'); @@ -13,6 +14,7 @@ const VideoPlaceholder = require('./VideoPlaceholder'); const styles = require('./styles'); const Video = ({ className, id, title, thumbnail, season, episode, released, upcoming, watched, progress, scheduled, seasonWatched, deepLinks, onMarkVideoAsWatched, onMarkSeasonAsWatched, ...props }) => { + const { t } = useTranslation(); const routeFocused = useRouteFocused(); const profile = useProfile(); const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); @@ -112,7 +114,7 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc : scheduled ?
- TBA + {t('TBA')}
: null @@ -121,7 +123,7 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc { upcoming && !watched ?
-
Upcoming
+
{t('UPCOMING')}
: null @@ -130,7 +132,7 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc watched ?
-
Watched
+
{t('CTX_WATCHED')}
: null diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js index 29637a24c..430b15aa9 100644 --- a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js +++ b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js @@ -56,9 +56,9 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => { return (
- { selectedOption={selectedSeason} onSelect={seasonOnSelect} /> -
diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBarPlaceholder/SeasonsBarPlaceholder.js b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBarPlaceholder/SeasonsBarPlaceholder.js index d767769ec..a46d6bda6 100644 --- a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBarPlaceholder/SeasonsBarPlaceholder.js +++ b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBarPlaceholder/SeasonsBarPlaceholder.js @@ -1,24 +1,26 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); const styles = require('./styles'); const SeasonsBarPlaceholder = ({ className }) => { + const { t } = useTranslation(); return (
-
Prev
+
{t('SEASON_PREV')}
-
Season 1
+
{`${t('SEASON')} 1`}
-
Next
+
{t('SEASON_NEXT')}
diff --git a/src/routes/MetaDetails/VideosList/VideosList.js b/src/routes/MetaDetails/VideosList/VideosList.js index a47b2f517..35012ddf0 100644 --- a/src/routes/MetaDetails/VideosList/VideosList.js +++ b/src/routes/MetaDetails/VideosList/VideosList.js @@ -122,7 +122,7 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect,
{' -
No videos found for this meta!
+
{t('ERR_NO_VIDEOS_FOR_META')}
: diff --git a/src/routes/Player/NextVideoPopup/NextVideoPopup.js b/src/routes/Player/NextVideoPopup/NextVideoPopup.js index a02d2377e..1f6bcbbe6 100644 --- a/src/routes/Player/NextVideoPopup/NextVideoPopup.js +++ b/src/routes/Player/NextVideoPopup/NextVideoPopup.js @@ -7,8 +7,10 @@ const { default: Icon } = require('@stremio/stremio-icons/react'); const { CONSTANTS, useProfile } = require('stremio/common'); const { Button, Image } = require('stremio/components'); const styles = require('./styles'); +const { useTranslation } = require('react-i18next'); const NextVideoPopup = ({ className, metaItem, nextVideo, onDismiss, onNextVideoRequested }) => { + const { t } = useTranslation(); const profile = useProfile(); const blurPosterImage = profile.settings.hideSpoilers && metaItem.type === 'series'; const watchNowButtonRef = React.useRef(null); @@ -65,7 +67,7 @@ const NextVideoPopup = ({ className, metaItem, nextVideo, onDismiss, onNextVideo { typeof metaItem?.name === 'string' ?
- Next on { metaItem.name } + {t('PLAYER_NEXT_VIDEO_TITLE_SHORT')} { metaItem.name }
: null @@ -82,11 +84,11 @@ const NextVideoPopup = ({ className, metaItem, nextVideo, onDismiss, onNextVideo
diff --git a/src/routes/Player/StatisticsMenu/StatisticsMenu.js b/src/routes/Player/StatisticsMenu/StatisticsMenu.js index 69ee2bf8d..1678e0396 100644 --- a/src/routes/Player/StatisticsMenu/StatisticsMenu.js +++ b/src/routes/Player/StatisticsMenu/StatisticsMenu.js @@ -1,20 +1,22 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const classNames = require('classnames'); const PropTypes = require('prop-types'); const styles = require('./styles.less'); const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => { + const { t } = useTranslation(); return (
- Statistics + {t('PLAYER_STATISTICS')}
- Peers + {t('PLAYER_PEERS')}
{ peers } @@ -22,7 +24,7 @@ const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => {
- Speed + {t('PLAYER_SPEED')}
{ speed } MB/s @@ -30,7 +32,7 @@ const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => {
- Completed + {t('PLAYER_COMPLETED')}
{ Math.min(completed, 100) } % @@ -39,7 +41,7 @@ const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => {
- Info Hash + {t('PLAYER_INFO_HASH')}
{ infoHash } From 789173bb5be07cd38254d4c9bfd57e5676f61579 Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 23 May 2025 18:18:35 +0300 Subject: [PATCH 06/39] fix(Intro): added missing translation keys --- src/routes/Intro/Intro.js | 20 ++++++++++---------- src/routes/Intro/styles.less | 4 ++++ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js index 6af8f0167..3ba1c3610 100644 --- a/src/routes/Intro/Intro.js +++ b/src/routes/Intro/Intro.js @@ -299,10 +299,10 @@ const Intro = ({ queryParams }) => { {'
- Freedom to Stream + {t('WEBSITE_SLOGAN_NEW_NEW')}
- All the Video Content You Enjoy in One Place + {t('WEBSITE_SLOGAN_ALL')}
@@ -362,7 +362,7 @@ const Intro = ({ queryParams }) => { :
- +
} { @@ -372,22 +372,22 @@ const Intro = ({ queryParams }) => { null }
{ state.form === SIGNUP_FORM ? : null @@ -395,7 +395,7 @@ const Intro = ({ queryParams }) => { { state.form === LOGIN_FORM ? : null @@ -403,7 +403,7 @@ const Intro = ({ queryParams }) => { { state.form === SIGNUP_FORM ? : null @@ -421,7 +421,7 @@ const Intro = ({ queryParams }) => {
-
Authenticating...
+
{t('AUTHENTICATING')}
diff --git a/src/routes/Intro/styles.less b/src/routes/Intro/styles.less index 31a09d54c..32fc73d79 100644 --- a/src/routes/Intro/styles.less +++ b/src/routes/Intro/styles.less @@ -101,6 +101,10 @@ color: var(--primary-foreground-color); text-align: center; } + + .uppercase { + text-transform: uppercase; + } } .submit-button, .guest-login-button, .signup-form-button, .login-form-button { From 64c553d253a240cf34f4d4b303df441ca402bc34 Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 27 May 2025 12:06:58 +0300 Subject: [PATCH 07/39] fix(Calendar): added missing translation keys --- src/routes/Calendar/Details/Details.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/Calendar/Details/Details.tsx b/src/routes/Calendar/Details/Details.tsx index cce550ee9..a03708572 100644 --- a/src/routes/Calendar/Details/Details.tsx +++ b/src/routes/Calendar/Details/Details.tsx @@ -1,6 +1,7 @@ // Copyright (C) 2017-2024 Smart code 203358507 import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import Icon from '@stremio/stremio-icons/react'; import { Button } from 'stremio/components'; import styles from './Details.less'; @@ -11,6 +12,7 @@ type Props = { }; const Details = ({ selected, items }: Props) => { + const { t } = useTranslation(); const videos = useMemo(() => { return items.find(({ date }) => date.day === selected?.day)?.items ?? []; }, [selected, items]); @@ -33,7 +35,7 @@ const Details = ({ selected, items }: Props) => { { !videos.length ?
- No new episodes for this day + {t('CALENDAR_NO_NEW_EPISODES')}
: null From 61fdf3113e78bcd41ae8411c731d9e7e39b04a58 Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 27 May 2025 12:07:57 +0300 Subject: [PATCH 08/39] fix(Discover): added missing translation keys --- src/routes/Discover/Discover.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index a1472d38e..ebefdf6c0 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); @@ -14,6 +15,7 @@ const styles = require('./styles'); const SCROLL_TO_BOTTOM_THRESHOLD = 400; const Discover = ({ urlParams, queryParams }) => { + const { t } = useTranslation(); const { core } = useServices(); const [discover, loadNextPage] = useDiscover(urlParams, queryParams); const [selectInputs, hasNextPage] = useSelectableInputs(discover); @@ -120,9 +122,9 @@ const Discover = ({ urlParams, queryParams }) => { { discover.catalog !== null && !discover.catalog.installed ?
-
Addon is not installed. Install now?
+
{t('ERR_ADDON_NOT_INSTALLED')}
: @@ -133,7 +135,7 @@ const Discover = ({ urlParams, queryParams }) => {
{' -
No catalog selected!
+
{t('NO_CATALOG_SELECTED')}
: From db40abfad34d814bffcd50e6ff2e058f00338bc3 Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 27 May 2025 12:12:39 +0300 Subject: [PATCH 09/39] fix(Library): added missing translation keys --- src/routes/Library/Library.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/routes/Library/Library.js b/src/routes/Library/Library.js index f6310470b..1a36457db 100644 --- a/src/routes/Library/Library.js +++ b/src/routes/Library/Library.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const NotFound = require('stremio/routes/NotFound'); @@ -47,6 +48,7 @@ function withModel(Library) { } const Library = ({ model, urlParams, queryParams }) => { + const { t } = useTranslation(); const profile = useProfile(); const notifications = useNotifications(); const [library, loadNextPage] = useLibrary(model, urlParams, queryParams); @@ -86,7 +88,7 @@ const Library = ({ model, urlParams, queryParams }) => { src={require('/images/empty.png')} alt={' '} /> -
{model === 'library' ? 'Library' : 'Continue Watching'} not loaded!
+
{model === 'library' ? t('LIBRARY_NOT_LOADED') : t('BOARD_CONTINUE_WATCHING_NOT_LOADED')}
: @@ -97,7 +99,7 @@ const Library = ({ model, urlParams, queryParams }) => { src={require('/images/empty.png')} alt={' '} /> -
Empty {model === 'library' ? 'Library' : 'Continue Watching'}
+
{model === 'library' ? t('LIBRARY_EMPTY') : t('BOARD_CONTINUE_WATCHING_EMPTY')}
:
From 7b57c0f508a622593ebd1aac1ebeb6c3fb210143 Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 27 May 2025 12:24:35 +0300 Subject: [PATCH 10/39] fix(MetaDetails): added missing translation keys --- src/routes/MetaDetails/MetaDetails.js | 8 +++++--- src/routes/MetaDetails/StreamsList/StreamsList.js | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/routes/MetaDetails/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js index 8a50b59c1..da79df285 100644 --- a/src/routes/MetaDetails/MetaDetails.js +++ b/src/routes/MetaDetails/MetaDetails.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { useServices } = require('stremio/services'); @@ -14,6 +15,7 @@ const useMetaExtensionTabs = require('./useMetaExtensionTabs'); const styles = require('./styles'); const MetaDetails = ({ urlParams, queryParams }) => { + const { t } = useTranslation(); const { core } = useServices(); const metaDetails = useMetaDetails(urlParams); const [season, setSeason] = useSeason(urlParams, queryParams); @@ -129,20 +131,20 @@ const MetaDetails = ({ urlParams, queryParams }) => {
{' -
No meta was selected!
+
{t('ERR_NO_META_SELECTED')}
: metaDetails.metaItem === null ?
{' -
No addons were requested for this meta!
+
{t('ERR_NO_ADDONS_FOR_META')}
: metaDetails.metaItem.content.type === 'Err' ?
{' -
No metadata was found!
+
{t('ERR_NO_META_FOUND')}
: metaDetails.metaItem.content.type === 'Loading' ? diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index 82ba57a51..a9665688a 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -134,7 +134,7 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => { : null } {' -
No addons were requested for streams!
+
{t('ERR_NO_ADDONS_FOR_STREAMS')}
: props.streams.every((streams) => streams.content.type === 'Err') ? From a25d23559f7ee836f12b5b06b41882e4a3ce3110 Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 27 May 2025 14:23:39 +0300 Subject: [PATCH 11/39] fix(NotFound): added missing translation keys --- src/routes/NotFound/NotFound.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/routes/NotFound/NotFound.js b/src/routes/NotFound/NotFound.js index 323dfd866..d984496bc 100644 --- a/src/routes/NotFound/NotFound.js +++ b/src/routes/NotFound/NotFound.js @@ -1,15 +1,17 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const { HorizontalNavBar, Image } = require('stremio/components'); const styles = require('./styles'); const NotFound = () => { + const { t } = useTranslation(); return (
{ src={require('/images/empty.png')} alt={' '} /> -
Page not found!
+
{t('PAGE_NOT_FOUND')}
); From a7eb1801e3eed36b5240965efe412b094c90d13b Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 27 May 2025 14:24:59 +0300 Subject: [PATCH 12/39] fix(Settings): added missing translation keys --- src/routes/Settings/Settings.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index d6fe9b7a4..43f714feb 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -184,20 +184,20 @@ const Settings = () => {
- App Version: {process.env.VERSION} + {`${t('SETTINGS_APP_VERSION')} : ${process.env.VERSION}`}
- Build Version: {process.env.COMMIT_HASH} + {`${t('SETTINGS_BUILD_VERSION')} : ${process.env.COMMIT_HASH}`}
{ streamingServer.settings !== null && streamingServer.settings.type === 'Ready' ? -
Server Version: {streamingServer.settings.content.serverVersion}
+
{`${t('SETTINGS_SERVER_VERSION')} : ${streamingServer.settings.content.serverVersion}`}
: null } { typeof shell?.transport?.props?.shellVersion === 'string' ? -
Shell Version: {shell.transport.props.shellVersion}
+
{`${t('SETTINGS_APP_VERSION')} : ${shell.transport.props.shellVersion}`}
: null } @@ -274,7 +274,7 @@ const Settings = () => {
@@ -310,7 +310,7 @@ const Settings = () => {
-
Trakt Scrobbling
+
{t('SETTINGS_TRAKT')}
- {`${t('SETTINGS_APP_VERSION')} : ${process.env.VERSION}`} + {`${t('SETTINGS_APP_VERSION')}: ${process.env.VERSION}`}
- {`${t('SETTINGS_BUILD_VERSION')} : ${process.env.COMMIT_HASH}`} + {`${t('SETTINGS_BUILD_VERSION')}: ${process.env.COMMIT_HASH}`}
{ streamingServer.settings !== null && streamingServer.settings.type === 'Ready' ? -
{`${t('SETTINGS_SERVER_VERSION')} : ${streamingServer.settings.content.serverVersion}`}
+
{`${t('SETTINGS_SERVER_VERSION')}: ${streamingServer.settings.content.serverVersion}`}
: null } { typeof shell?.transport?.props?.shellVersion === 'string' ? -
{`${t('SETTINGS_APP_VERSION')} : ${shell.transport.props.shellVersion}`}
+
{`${t('SETTINGS_APP_VERSION')}: ${shell.transport.props.shellVersion}`}
: null } From fe871f03f1e823e75cd4c5680f9a46090c63992c Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 27 May 2025 17:55:17 +0300 Subject: [PATCH 14/39] fix: dropdown options and titles translations --- src/common/useTranslate.js | 2 +- src/routes/Addons/useSelectableInputs.js | 4 ++-- src/routes/Discover/useSelectableInputs.js | 4 ++-- src/routes/Settings/useStreamingServerSettingsInputs.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/common/useTranslate.js b/src/common/useTranslate.js index 7214a4a1e..fdfb2f3fe 100644 --- a/src/common/useTranslate.js +++ b/src/common/useTranslate.js @@ -9,7 +9,7 @@ const useTranslate = () => { const string = useCallback((key) => t(key), [t]); const stringWithPrefix = useCallback((value, prefix, fallback = null) => { - const key = `${prefix}${value}`; + const key = `${prefix || ''}${value}`; const defaultValue = fallback ?? value.charAt(0).toUpperCase() + value.slice(1); return t(key, { diff --git a/src/routes/Addons/useSelectableInputs.js b/src/routes/Addons/useSelectableInputs.js index af0bb35fc..e9d276d71 100644 --- a/src/routes/Addons/useSelectableInputs.js +++ b/src/routes/Addons/useSelectableInputs.js @@ -10,8 +10,8 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t) => { .concat(installedAddons.selectable.catalogs) .map(({ name, deepLinks }) => ({ value: deepLinks.addons, - label: t.stringWithPrefix(name, 'ADDON_'), - title: t.stringWithPrefix(name, 'ADDON_'), + label: t.stringWithPrefix(name.toUpperCase(), 'ADDON_'), + title: t.stringWithPrefix(name.toUpperCase(), 'ADDON_'), })), selected: remoteAddons.selectable.catalogs .concat(installedAddons.selectable.catalogs) diff --git a/src/routes/Discover/useSelectableInputs.js b/src/routes/Discover/useSelectableInputs.js index 18d317507..66178c7ee 100644 --- a/src/routes/Discover/useSelectableInputs.js +++ b/src/routes/Discover/useSelectableInputs.js @@ -46,7 +46,7 @@ const mapSelectableInputs = (discover, t) => { } }; const extraSelects = discover.selectable.extra.map(({ name, isRequired, options }) => ({ - title: t.stringWithPrefix(name, 'SELECT_'), + title: t.stringWithPrefix(name), isRequired: isRequired, options: options.map(({ value, deepLinks }) => ({ label: typeof value === 'string' ? t.stringWithPrefix(value) : t.string('NONE'), @@ -62,7 +62,7 @@ const mapSelectableInputs = (discover, t) => { value })), renderLabelText: options.some(({ selected, value }) => selected && value === null) ? - () => t.stringWithPrefix(name, 'SELECT_') + () => t.stringWithPrefix(name.toUpperCase(), 'SELECT_') : null, onSelect: (event) => { diff --git a/src/routes/Settings/useStreamingServerSettingsInputs.js b/src/routes/Settings/useStreamingServerSettingsInputs.js index 1d4eee066..4bebd4d0e 100644 --- a/src/routes/Settings/useStreamingServerSettingsInputs.js +++ b/src/routes/Settings/useStreamingServerSettingsInputs.js @@ -140,7 +140,7 @@ const useStreamingServerSettingsInputs = (streamingServer) => { return { options: Object.keys(TORRENT_PROFILES) .map((profileName) => ({ - label: profileName, + label: t('TORRENT_PROFILE_' + profileName.replace(' ', '_').toUpperCase()), value: JSON.stringify(TORRENT_PROFILES[profileName]) })) .concat( From 51a1da958b05f2ec1566d02d9af453d217deffb3 Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 27 May 2025 17:55:47 +0300 Subject: [PATCH 15/39] fix: season number translation key --- src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js | 4 ++-- .../SeasonsBar/SeasonsBarPlaceholder/SeasonsBarPlaceholder.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js index 430b15aa9..d202aafb0 100644 --- a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js +++ b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js @@ -13,7 +13,7 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => { const options = React.useMemo(() => { return seasons.map((season) => ({ value: String(season), - label: season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL') + label: season > 0 ? t('SEASON_NUMBER', { season }) : t('SPECIAL') })); }, [seasons]); const selectedSeason = React.useMemo(() => { @@ -63,7 +63,7 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => { 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')} + title={season > 0 ? t('SEASON_NUMBER', { season }) : t('SPECIAL')} selectedOption={selectedSeason} onSelect={seasonOnSelect} /> diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBarPlaceholder/SeasonsBarPlaceholder.js b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBarPlaceholder/SeasonsBarPlaceholder.js index a46d6bda6..4013e0b64 100644 --- a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBarPlaceholder/SeasonsBarPlaceholder.js +++ b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBarPlaceholder/SeasonsBarPlaceholder.js @@ -16,7 +16,7 @@ const SeasonsBarPlaceholder = ({ className }) => {
{t('SEASON_PREV')}
-
{`${t('SEASON')} 1`}
+
{t('SEASON_NUMBER', { season: 1 })}
From 3c249e5925e98e017a3d76bf0ce0569b1e2b2bb1 Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 27 May 2025 18:20:18 +0300 Subject: [PATCH 16/39] fix hardcoded titles with no translations --- src/common/Toast/ToastItem/ToastItem.js | 4 +++- src/components/AddonDetailsModal/AddonDetailsModal.js | 4 ++-- src/components/ColorInput/ColorInput.js | 2 +- src/components/ModalDialog/ModalDialog.js | 4 +++- src/components/SharePrompt/SharePrompt.js | 2 +- src/components/Video/Video.js | 6 +++--- src/routes/Addons/Addons.js | 4 ++-- src/routes/Discover/Discover.js | 6 +++--- src/routes/Intro/PasswordResetModal/PasswordResetModal.js | 4 +++- .../DiscreteSelectInput/DiscreteSelectInput.js | 4 +++- src/routes/Settings/Settings.js | 8 ++++---- src/routes/Settings/URLsManager/URLsManager.tsx | 4 ++-- 12 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/common/Toast/ToastItem/ToastItem.js b/src/common/Toast/ToastItem/ToastItem.js index 94b5a98b4..3a317ab1b 100644 --- a/src/common/Toast/ToastItem/ToastItem.js +++ b/src/common/Toast/ToastItem/ToastItem.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); @@ -8,6 +9,7 @@ const { Button } = require('stremio/components'); const styles = require('./styles'); const ToastItem = ({ title, message, dataset, onSelect, onClose, ...props }) => { + const { t } = useTranslation(); const type = React.useMemo(() => { return ['success', 'alert', 'info', 'error'].includes(props.type) ? props.type @@ -74,7 +76,7 @@ const ToastItem = ({ title, message, dataset, onSelect, onClose, ...props }) => null }
- diff --git a/src/components/AddonDetailsModal/AddonDetailsModal.js b/src/components/AddonDetailsModal/AddonDetailsModal.js index 2dcea3e3b..372d6edd2 100644 --- a/src/components/AddonDetailsModal/AddonDetailsModal.js +++ b/src/components/AddonDetailsModal/AddonDetailsModal.js @@ -143,7 +143,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { return addonDetails.remoteAddon?.content.type === 'Ready' ? addonDetails.remoteAddon.content.content.manifest.background : null; }, [addonDetails.remoteAddon]); return ( - + { addonDetails.selected === null ?
@@ -180,7 +180,7 @@ const AddonDetailsModalFallback = ({ onCloseRequest }) => { const { t } = useTranslation(); return
diff --git a/src/components/ColorInput/ColorInput.js b/src/components/ColorInput/ColorInput.js index afa411f63..2a32542d8 100644 --- a/src/components/ColorInput/ColorInput.js +++ b/src/components/ColorInput/ColorInput.js @@ -82,7 +82,7 @@ const ColorInput = ({ className, value, dataset, onChange, ...props }) => { } { modalOpen ? - + : diff --git a/src/components/ModalDialog/ModalDialog.js b/src/components/ModalDialog/ModalDialog.js index 5e5907c5a..b07481f69 100644 --- a/src/components/ModalDialog/ModalDialog.js +++ b/src/components/ModalDialog/ModalDialog.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { useRouteFocused, useModalsContainer } = require('stremio-router'); @@ -10,6 +11,7 @@ const { Modal } = require('stremio-router'); const styles = require('./styles'); const ModalDialog = ({ className, title, buttons, children, dataset, onCloseRequest, background, ...props }) => { + const { t } = useTranslation(); const routeFocused = useRouteFocused(); const modalsContainer = useModalsContainer(); const modalContainerRef = React.useRef(null); @@ -60,7 +62,7 @@ const ModalDialog = ({ className, title, buttons, children, dataset, onCloseRequ
-
diff --git a/src/components/SharePrompt/SharePrompt.js b/src/components/SharePrompt/SharePrompt.js index 0a9843a02..af4393b8a 100644 --- a/src/components/SharePrompt/SharePrompt.js +++ b/src/components/SharePrompt/SharePrompt.js @@ -70,7 +70,7 @@ const SharePrompt = ({ className, url }) => { onClick={selectInputContent} tabIndex={-1} /> - diff --git a/src/components/Video/Video.js b/src/components/Video/Video.js index 92b223e0c..e0313b277 100644 --- a/src/components/Video/Video.js +++ b/src/components/Video/Video.js @@ -113,7 +113,7 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc
: scheduled ? -
+
{t('TBA')}
: @@ -147,10 +147,10 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc const renderMenu = React.useMemo(() => function renderMenu() { return (
- -
@@ -216,7 +216,7 @@ const Addons = ({ urlParams, queryParams }) => {
{ filtersModalOpen ? - + {selectInputs.map((selectInput, index) => ( { /> ))}
-
@@ -123,7 +123,7 @@ const Discover = ({ urlParams, queryParams }) => { discover.catalog !== null && !discover.catalog.installed ?
{t('ERR_ADDON_NOT_INSTALLED')}
-
@@ -204,7 +204,7 @@ const Discover = ({ urlParams, queryParams }) => {
{ inputsModalOpen ? - + {selectInputs.map(({ title, options, selected, renderLabelText, onSelect }, index) => ( { + const { t } = useTranslation(); const routeFocused = useRouteFocused(); const platform = usePlatform(); const [error, setError] = React.useState(''); @@ -45,7 +47,7 @@ const PasswordResetModal = ({ email, onCloseRequest }) => { } }, [routeFocused]); return ( - + { + const { t } = useTranslation(); const buttonOnClick = React.useCallback((event) => { if (typeof onChange === 'function') { onChange({ @@ -22,7 +24,7 @@ const DiscreteSelectInput = ({ className, value, label, disabled, dataset, onCha return (
{label}
-
+
diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 032a4cf4a..7dcb24d4d 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -219,9 +219,9 @@ const Settings = () => { }} />
-
+
- {profile.auth === null ? 'Anonymous user' : profile.auth.user.email} + {profile.auth === null ? t('ANONYMOUS_USER') : profile.auth.user.email}
{ @@ -273,7 +273,7 @@ const Settings = () => {
-
@@ -312,7 +312,7 @@ const Settings = () => {
{t('SETTINGS_TRAKT')}
-
- - From ce54ac9aac58eda4a7bb14e14f5a37a5a74b043c Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 27 May 2025 18:27:19 +0300 Subject: [PATCH 17/39] feat: update translations dependency --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index defb06643..aa19dd188 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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#a6be0425573917c2e82b66d28968c1a4d444cb96", + "stremio-translations": "github:Stremio/stremio-translations#212e9e01226b1f2f24c9d0374556088b71d5f21a", "url": "0.11.4", "use-long-press": "^3.2.0" }, @@ -13404,9 +13404,9 @@ } }, "node_modules/stremio-translations": { - "version": "1.44.10", - "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#a6be0425573917c2e82b66d28968c1a4d444cb96", - "integrity": "sha512-77kVE/eos/SA16kzeK7TTWmqoLF0mLPCJXjITwVIVzMHr8XyBPZFOfmiVEg4M6W1W7qYqA+dHhzicyLs7hJhlw==", + "version": "1.44.12", + "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#212e9e01226b1f2f24c9d0374556088b71d5f21a", + "integrity": "sha512-z56Y3X/6Aery5GWuOmbLfeicQI9zSE5kodNxIvYU0ovAIHAcJeTAGoqeMhMq88IXm8n1XB/lqIcau8rTCvj0IQ==", "license": "MIT" }, "node_modules/string_decoder": { diff --git a/package.json b/package.json index 53682f6e6..564a01c05 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,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#a6be0425573917c2e82b66d28968c1a4d444cb96", + "stremio-translations": "github:Stremio/stremio-translations#212e9e01226b1f2f24c9d0374556088b71d5f21a", "url": "0.11.4", "use-long-press": "^3.2.0" }, From 18b70402a4aeef2d95614d1eea8dbefe72e53eeb Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 27 May 2025 18:32:20 +0300 Subject: [PATCH 18/39] fix lint --- src/components/Video/Video.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Video/Video.js b/src/components/Video/Video.js index e0313b277..5e5d30ed8 100644 --- a/src/components/Video/Video.js +++ b/src/components/Video/Video.js @@ -4,7 +4,6 @@ const React = require('react'); const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); -const { t } = require('i18next'); const { useRouteFocused } = require('stremio-router'); const { default: Icon } = require('@stremio/stremio-icons/react'); const { Button, Image, Popup } = require('stremio/components'); From bafa6a7ad2ffa658240d0020895f6fafa204a394 Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 28 May 2025 12:20:55 +0300 Subject: [PATCH 19/39] Feat: refactor the translations scanner into a test --- scan-i18n-report.js => i18nScan.test.js | 42 +++++++++++++------------ package.json | 2 +- 2 files changed, 23 insertions(+), 21 deletions(-) rename scan-i18n-report.js => i18nScan.test.js (74%) diff --git a/scan-i18n-report.js b/i18nScan.test.js similarity index 74% rename from scan-i18n-report.js rename to i18nScan.test.js index 3294d34e0..32754b1e4 100644 --- a/scan-i18n-report.js +++ b/i18nScan.test.js @@ -4,7 +4,6 @@ const recast = require('recast'); const babelParser = require('@babel/parser'); const directoryToScan = './src'; -const report = []; function toKey(str) { return str @@ -14,7 +13,7 @@ function toKey(str) { .slice(0, 40); } -function scanFile(filePath) { +function scanFile(filePath, report) { try { const code = fs.readFileSync(filePath, 'utf8'); const ast = babelParser.parse(code, { @@ -31,7 +30,6 @@ function scanFile(filePath) { }); recast.types.visit(ast, { - // Text directly inside JSX visitJSXText(path) { const text = path.node.value.trim(); if (text.length > 1 && /\w/.test(text)) { @@ -46,11 +44,9 @@ function scanFile(filePath) { this.traverse(path); }, - // { "hello" } style visitJSXExpressionContainer(path) { const expr = path.node.expression; - // Skip expressions that call t() if ( expr.type === 'CallExpression' && expr.callee.type === 'Identifier' && @@ -59,7 +55,6 @@ function scanFile(filePath) { return false; } - // Find only { "text" } expressions if (expr.type === 'StringLiteral') { const parent = path.parentPath.node; if (parent.type === 'JSXElement') { @@ -82,26 +77,33 @@ function scanFile(filePath) { } } -function walk(dir) { +function walk(dir, report) { fs.readdirSync(dir).forEach((file) => { const fullPath = path.join(dir, file); if (fs.statSync(fullPath).isDirectory()) { - walk(fullPath); + walk(fullPath, report); } else if (/\.(js|jsx|ts|tsx)$/.test(file)) { - console.log('Scanning file:', fullPath); - scanFile(fullPath); + // console.log('📄 Scanning file:', fullPath); + scanFile(fullPath, report); } }); } -function writeReport() { - const header = `File,Line,Hardcoded String,Suggested Key\n`; - const rows = report.map((row) => - `"${row.file}",${row.line},"${row.string.replace(/"/g, '""')}","${row.key}"` - ); - fs.writeFileSync('i18n-report.csv', header + rows.join('\n')); - console.log('✅ Report written to i18n-report.csv'); -} +describe('i18n hardcoded string scan', () => { + it('should print hardcoded strings with suggested keys', () => { + const report = []; + walk(directoryToScan, report); -walk(directoryToScan); -writeReport(); + if (report.length === 0) { + console.log('✅ No hardcoded strings found.'); + } else { + console.log('🚨 Hardcoded strings found:'); + report.forEach(row => { + console.log(`File: ${row.file}, Line: ${row.line}, String: "${row.string}", Suggested Key: ${row.key}`); + }); + } + + // Optional: expect no hardcoded strings if you want the test to fail in that case + // expect(report.length).toBe(0); + }); +}); diff --git a/package.json b/package.json index 564a01c05..40a319adc 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build": "webpack --mode production", "test": "jest", "lint": "eslint src", - "scan-translations": "node scan-i18n-report.js" + "scan-translations": "npx jest i18nScan.test.js" }, "dependencies": { "@babel/runtime": "7.26.0", From 66ad1ea59f856a9c0fd3e3a1742922e3ae5f0950 Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 28 May 2025 12:25:55 +0300 Subject: [PATCH 20/39] fix: move scan translations test to test folder --- .gitignore | 3 +-- package.json | 2 +- i18nScan.test.js => tests/i18nScan.test.js | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) rename i18nScan.test.js => tests/i18nScan.test.js (98%) diff --git a/.gitignore b/.gitignore index 803977723..71947782e 100755 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ /yarn.lock /npm-debug.log .DS_Store -.prettierignore -i18n-report.csv \ No newline at end of file +.prettierignore \ No newline at end of file diff --git a/package.json b/package.json index 40a319adc..35bbaa641 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build": "webpack --mode production", "test": "jest", "lint": "eslint src", - "scan-translations": "npx jest i18nScan.test.js" + "scan-translations": "npx jest ./tests/i18nScan.test.js" }, "dependencies": { "@babel/runtime": "7.26.0", diff --git a/i18nScan.test.js b/tests/i18nScan.test.js similarity index 98% rename from i18nScan.test.js rename to tests/i18nScan.test.js index 32754b1e4..03d3156c8 100644 --- a/i18nScan.test.js +++ b/tests/i18nScan.test.js @@ -98,7 +98,7 @@ describe('i18n hardcoded string scan', () => { console.log('✅ No hardcoded strings found.'); } else { console.log('🚨 Hardcoded strings found:'); - report.forEach(row => { + report.forEach((row) => { console.log(`File: ${row.file}, Line: ${row.line}, String: "${row.string}", Suggested Key: ${row.key}`); }); } From 4f9cd612860d99a2a1ebbbd76c96b59a3f7cc80d Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 28 May 2025 13:36:21 +0300 Subject: [PATCH 21/39] fix(Discover): dropdown options translations --- src/common/useTranslate.js | 2 +- src/routes/Discover/useSelectableInputs.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/useTranslate.js b/src/common/useTranslate.js index fdfb2f3fe..7214a4a1e 100644 --- a/src/common/useTranslate.js +++ b/src/common/useTranslate.js @@ -9,7 +9,7 @@ const useTranslate = () => { const string = useCallback((key) => t(key), [t]); const stringWithPrefix = useCallback((value, prefix, fallback = null) => { - const key = `${prefix || ''}${value}`; + const key = `${prefix}${value}`; const defaultValue = fallback ?? value.charAt(0).toUpperCase() + value.slice(1); return t(key, { diff --git a/src/routes/Discover/useSelectableInputs.js b/src/routes/Discover/useSelectableInputs.js index 66178c7ee..97d9e7192 100644 --- a/src/routes/Discover/useSelectableInputs.js +++ b/src/routes/Discover/useSelectableInputs.js @@ -46,10 +46,10 @@ const mapSelectableInputs = (discover, t) => { } }; const extraSelects = discover.selectable.extra.map(({ name, isRequired, options }) => ({ - title: t.stringWithPrefix(name), + title: t(name), isRequired: isRequired, options: options.map(({ value, deepLinks }) => ({ - label: typeof value === 'string' ? t.stringWithPrefix(value) : t.string('NONE'), + label: typeof value === 'string' ? t(value) : t.string('NONE'), value: JSON.stringify({ href: deepLinks.discover, value From d27f92fb01be8dbf97fa8e0e4e25115f5b8edc24 Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 28 May 2025 16:30:04 +0300 Subject: [PATCH 22/39] fix(Discover): use correct translation func --- src/routes/Discover/useSelectableInputs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/Discover/useSelectableInputs.js b/src/routes/Discover/useSelectableInputs.js index 97d9e7192..89963cff3 100644 --- a/src/routes/Discover/useSelectableInputs.js +++ b/src/routes/Discover/useSelectableInputs.js @@ -46,10 +46,10 @@ const mapSelectableInputs = (discover, t) => { } }; const extraSelects = discover.selectable.extra.map(({ name, isRequired, options }) => ({ - title: t(name), + title: t.string(name), isRequired: isRequired, options: options.map(({ value, deepLinks }) => ({ - label: typeof value === 'string' ? t(value) : t.string('NONE'), + label: typeof value === 'string' ? t.string(value) : t.string('NONE'), value: JSON.stringify({ href: deepLinks.discover, value From e704abcb03fa2f4505327d4324472e0394f8c4a1 Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 28 May 2025 17:16:08 +0300 Subject: [PATCH 23/39] feat: make translation test fail if hardcoded strings are found --- tests/i18nScan.test.js | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/tests/i18nScan.test.js b/tests/i18nScan.test.js index 03d3156c8..5f2964a9a 100644 --- a/tests/i18nScan.test.js +++ b/tests/i18nScan.test.js @@ -88,22 +88,20 @@ function walk(dir, report) { } }); } +const report = []; -describe('i18n hardcoded string scan', () => { - it('should print hardcoded strings with suggested keys', () => { - const report = []; - walk(directoryToScan, report); +walk(directoryToScan, report); - if (report.length === 0) { - console.log('✅ No hardcoded strings found.'); - } else { - console.log('🚨 Hardcoded strings found:'); - report.forEach((row) => { - console.log(`File: ${row.file}, Line: ${row.line}, String: "${row.string}", Suggested Key: ${row.key}`); - }); - } - - // Optional: expect no hardcoded strings if you want the test to fail in that case - // expect(report.length).toBe(0); +if (report.length !== 0) { + describe.each(report)('Missing translation key', (entry) => { + it(`should not have "${entry.string}" in ${entry.file} at line ${entry.line}`, () => { + expect(entry.string).toBeFalsy(); + }); }); -}); +} else { + describe('Missing translation key', () => { + it('No hardcoded strings found', () => { + expect(true).toBe(true); // or just skip + }); + }); +} From d692d090410c57e5406270c22e3ec94fce7ee806 Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 28 May 2025 17:17:34 +0300 Subject: [PATCH 24/39] fix: replace all remaining untranslated strings with translation keys --- .../AddonDetailsModal/AddonDetails/AddonDetails.js | 6 +++--- src/components/Checkbox/Checkbox.less | 1 + src/components/Checkbox/Checkbox.tsx | 1 - src/routes/Addons/Addon/Addon.js | 2 +- src/routes/Addons/Addons.js | 2 +- src/routes/Player/StatisticsMenu/StatisticsMenu.js | 2 +- src/routes/Settings/URLsManager/URLsManager.tsx | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/AddonDetailsModal/AddonDetails/AddonDetails.js b/src/components/AddonDetailsModal/AddonDetails/AddonDetails.js index c6722f294..78c744b96 100644 --- a/src/components/AddonDetailsModal/AddonDetails/AddonDetails.js +++ b/src/components/AddonDetailsModal/AddonDetails/AddonDetails.js @@ -9,7 +9,7 @@ const { default: Image } = require('stremio/components/Image'); const styles = require('./styles'); const AddonDetails = ({ className, id, name, version, logo, description, types, transportUrl, official }) => { - const t = useTranslation(); + const { t } = useTranslation(); const renderLogoFallback = React.useCallback(() => ( ), []); @@ -26,7 +26,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types, {typeof name === 'string' && name.length > 0 ? name : id} { typeof version === 'string' && version.length > 0 ? - v. {version} + {t('ADDON_VERSION_SHORT', {version})} : null } @@ -43,7 +43,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types, { typeof transportUrl === 'string' && transportUrl.length > 0 ?
- URL: + {`${t('URL')}:`} {transportUrl}
: diff --git a/src/components/Checkbox/Checkbox.less b/src/components/Checkbox/Checkbox.less index a84244ce9..9276990b3 100644 --- a/src/components/Checkbox/Checkbox.less +++ b/src/components/Checkbox/Checkbox.less @@ -23,6 +23,7 @@ .link { font-size: 0.9rem; color: var(--primary-accent-color); + margin-left: 0.5rem; &:hover { text-decoration: underline; diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx index da4ae33eb..b252006eb 100644 --- a/src/components/Checkbox/Checkbox.tsx +++ b/src/components/Checkbox/Checkbox.tsx @@ -80,7 +80,6 @@ const Checkbox = React.forwardRef(({ name, disabled, cl
{label} - {' '} { href && link ?
{ typeof version === 'string' && version.length > 0 ? -
v.{version}
+
{t('ADDON_VERSION_SHORT', {version})}
: null } diff --git a/src/routes/Addons/Addons.js b/src/routes/Addons/Addons.js index 17fc2a1b5..c33c22f9a 100644 --- a/src/routes/Addons/Addons.js +++ b/src/routes/Addons/Addons.js @@ -265,7 +265,7 @@ const Addons = ({ urlParams, queryParams }) => { {typeof sharedAddon.manifest.name === 'string' && sharedAddon.manifest.name.length > 0 ? sharedAddon.manifest.name : sharedAddon.manifest.id} { typeof sharedAddon.manifest.version === 'string' && sharedAddon.manifest.version.length > 0 ? - v. {sharedAddon.manifest.version} + {t('ADDON_VERSION_SHORT', { version: sharedAddon.manifest.version })} : null } diff --git a/src/routes/Player/StatisticsMenu/StatisticsMenu.js b/src/routes/Player/StatisticsMenu/StatisticsMenu.js index 1678e0396..b5f5232ea 100644 --- a/src/routes/Player/StatisticsMenu/StatisticsMenu.js +++ b/src/routes/Player/StatisticsMenu/StatisticsMenu.js @@ -27,7 +27,7 @@ const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => { {t('PLAYER_SPEED')}
- { speed } MB/s + {`${speed} ${t('MB_S')}`}
diff --git a/src/routes/Settings/URLsManager/URLsManager.tsx b/src/routes/Settings/URLsManager/URLsManager.tsx index b0907ed9e..b0d1245eb 100644 --- a/src/routes/Settings/URLsManager/URLsManager.tsx +++ b/src/routes/Settings/URLsManager/URLsManager.tsx @@ -30,7 +30,7 @@ const URLsManager = () => { return (
-
URL
+
{t('URL')}
{t('STATUS')}
From 8768b9ced30512c005dcc831efea5399ee2b9800 Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 28 May 2025 19:02:28 +0300 Subject: [PATCH 25/39] added missing modal button labels and \input placeholder translation keys --- package-lock.json | 6 +++--- package.json | 2 +- .../AddonDetailsModal/AddonDetailsModal.js | 8 ++++---- src/components/ColorInput/ColorInput.js | 2 +- src/routes/Intro/Intro.js | 16 ++++++++-------- .../PasswordResetModal/PasswordResetModal.js | 4 ++-- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index aa19dd188..854f2b0b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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#212e9e01226b1f2f24c9d0374556088b71d5f21a", + "stremio-translations": "github:Stremio/stremio-translations#1f0b73ff5605b1afde18636f82b8fd45d299a426", "url": "0.11.4", "use-long-press": "^3.2.0" }, @@ -13405,8 +13405,8 @@ }, "node_modules/stremio-translations": { "version": "1.44.12", - "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#212e9e01226b1f2f24c9d0374556088b71d5f21a", - "integrity": "sha512-z56Y3X/6Aery5GWuOmbLfeicQI9zSE5kodNxIvYU0ovAIHAcJeTAGoqeMhMq88IXm8n1XB/lqIcau8rTCvj0IQ==", + "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#1f0b73ff5605b1afde18636f82b8fd45d299a426", + "integrity": "sha512-1jvv4VRWNiFl+GivV6fXpq9QSIpElMUzGMG3jFxkJqvvZeYZpUsK/D4Bd9lR0jBSPYhwpLMZgFoGHWx3ABg+Ig==", "license": "MIT" }, "node_modules/string_decoder": { diff --git a/package.json b/package.json index 35bbaa641..d1428b7a3 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,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#212e9e01226b1f2f24c9d0374556088b71d5f21a", + "stremio-translations": "github:Stremio/stremio-translations#1f0b73ff5605b1afde18636f82b8fd45d299a426", "url": "0.11.4", "use-long-press": "^3.2.0" }, diff --git a/src/components/AddonDetailsModal/AddonDetailsModal.js b/src/components/AddonDetailsModal/AddonDetailsModal.js index 372d6edd2..a89a68b77 100644 --- a/src/components/AddonDetailsModal/AddonDetailsModal.js +++ b/src/components/AddonDetailsModal/AddonDetailsModal.js @@ -51,7 +51,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { const modalButtons = React.useMemo(() => { const cancelButton = { className: styles['cancel-button'], - label: 'Cancel', + label: t('BUTTON_CANCEL'), props: { onClick: (event) => { if (typeof onCloseRequest === 'function') { @@ -69,7 +69,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { addonDetails.remoteAddon.content.content.manifest.behaviorHints.configurable ? { className: styles['configure-button'], - label: 'Configure', + label: t('ADDON_CONFIGURE'), props: { onClick: (event) => { platform.openExternal(transportUrl.replace('manifest.json', 'configure')); @@ -88,7 +88,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { const toggleButton = addonDetails.localAddon !== null ? { className: styles['uninstall-button'], - label: 'Uninstall', + label: t('ADDON_UNINSTALL'), props: { onClick: (event) => { core.transport.dispatch({ @@ -115,7 +115,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { { className: styles['install-button'], - label: 'Install', + label: t('ADDON_INSTALL'), props: { onClick: (event) => { core.transport.dispatch({ diff --git a/src/components/ColorInput/ColorInput.js b/src/components/ColorInput/ColorInput.js index 2a32542d8..5bedf7161 100644 --- a/src/components/ColorInput/ColorInput.js +++ b/src/components/ColorInput/ColorInput.js @@ -56,7 +56,7 @@ const ColorInput = ({ className, value, dataset, onChange, ...props }) => { }; return [ { - label: 'Select', + label: t('SELECT'), props: { 'data-autofocus': true, onClick: selectButtonOnClick diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js index 3ba1c3610..48acadcd4 100644 --- a/src/routes/Intro/Intro.js +++ b/src/routes/Intro/Intro.js @@ -311,7 +311,7 @@ const Intro = ({ queryParams }) => { ref={emailRef} className={styles['credentials-text-input']} type={'email'} - placeholder={'Email'} + placeholder={t('EMAIL')} value={state.email} onChange={emailOnChange} onSubmit={emailOnSubmit} @@ -320,7 +320,7 @@ const Intro = ({ queryParams }) => { ref={passwordRef} className={styles['credentials-text-input']} type={'password'} - placeholder={'Password'} + placeholder={t('PASSWORD')} value={state.password} onChange={passwordOnChange} onSubmit={passwordOnSubmit} @@ -332,30 +332,30 @@ const Intro = ({ queryParams }) => { ref={confirmPasswordRef} className={styles['credentials-text-input']} type={'password'} - placeholder={'Confirm Password'} + placeholder={t('PASSWORD_CONFIRM')} value={state.confirmPassword} onChange={confirmPasswordOnChange} onSubmit={confirmPasswordOnSubmit} /> diff --git a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js b/src/routes/Intro/PasswordResetModal/PasswordResetModal.js index 5158f0136..8c69c1489 100644 --- a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js +++ b/src/routes/Intro/PasswordResetModal/PasswordResetModal.js @@ -25,13 +25,13 @@ const PasswordResetModal = ({ email, onCloseRequest }) => { return [ { className: styles['cancel-button'], - label: 'Cancel', + label: t('BUTTON_CANCEL'), props: { onClick: onCloseRequest } }, { - label: 'Send', + label: t('SEND'), props: { onClick: goToPasswordReset } From 015e770e42e818a77ba8189c2d3f70f8d2cc53aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 21:49:25 +0000 Subject: [PATCH 26/39] chore(deps): bump actions/github-script from 6 to 7 Bumps [actions/github-script](https://github.com/actions/github-script) from 6 to 7. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/github-script dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/auto_assign.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto_assign.yml b/.github/workflows/auto_assign.yml index 87e9c8f37..dfbddb6a4 100644 --- a/.github/workflows/auto_assign.yml +++ b/.github/workflows/auto_assign.yml @@ -14,7 +14,7 @@ jobs: # Auto assign PR to author - name: Auto Assign PR to Author if: github.event_name == 'pull_request' - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -33,7 +33,7 @@ jobs: - name: Label PRs and Issues env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | From 7c3dd67eb97e6862e452200c9090c8312dffe76e Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 10 Jun 2025 14:18:04 +0300 Subject: [PATCH 27/39] fix: video release date locale formatting --- src/components/Video/Video.js | 2 +- src/routes/MetaDetails/VideosList/VideosList.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Video/Video.js b/src/components/Video/Video.js index 5e5d30ed8..78a1e64f6 100644 --- a/src/components/Video/Video.js +++ b/src/components/Video/Video.js @@ -108,7 +108,7 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc { released instanceof Date && !isNaN(released.getTime()) ?
- {released.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })} + {released.toLocaleString(profile.settings.interfaceLanguage, { year: 'numeric', month: 'short', day: 'numeric' })}
: scheduled ? diff --git a/src/routes/MetaDetails/VideosList/VideosList.js b/src/routes/MetaDetails/VideosList/VideosList.js index 35012ddf0..88d53d427 100644 --- a/src/routes/MetaDetails/VideosList/VideosList.js +++ b/src/routes/MetaDetails/VideosList/VideosList.js @@ -5,6 +5,7 @@ const PropTypes = require('prop-types'); const classnames = require('classnames'); const { t } = require('i18next'); const { useServices } = require('stremio/services'); +const { useProfile } = require('stremio/common'); const { Image, SearchBar, Toggle, Video } = require('stremio/components'); const SeasonsBar = require('./SeasonsBar'); const { default: EpisodePicker } = require('../EpisodePicker'); @@ -12,6 +13,7 @@ const styles = require('./styles'); const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect, toggleNotifications }) => { const { core } = useServices(); + const profile = useProfile(); const showNotificationsToggle = React.useMemo(() => { return metaItem?.content?.content?.inLibrary && metaItem?.content?.content?.videos?.length; }, [metaItem]); @@ -158,7 +160,7 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect, return search.length === 0 || ( (typeof video.title === 'string' && video.title.toLowerCase().includes(search.toLowerCase())) || - (!isNaN(video.released.getTime()) && video.released.toLocaleString(undefined, { year: '2-digit', month: 'short', day: 'numeric' }).toLowerCase().includes(search.toLowerCase())) + (!isNaN(video.released.getTime()) && video.released.toLocaleString(profile.settings.interfaceLanguage, { year: '2-digit', month: 'short', day: 'numeric' }).toLowerCase().includes(search.toLowerCase())) ); }) .map((video, index) => ( From 48d95d9d6f0bac51cc7196736492f7d1818e042d Mon Sep 17 00:00:00 2001 From: val_makkas Date: Thu, 12 Jun 2025 22:07:50 +0300 Subject: [PATCH 28/39] fix(SideDrawer): add show more/less button for description --- src/routes/Player/SideDrawer/SideDrawer.less | 18 ++++++++++++++++++ src/routes/Player/SideDrawer/SideDrawer.tsx | 19 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/routes/Player/SideDrawer/SideDrawer.less b/src/routes/Player/SideDrawer/SideDrawer.less index 2d2178c3e..3c2c12401 100644 --- a/src/routes/Player/SideDrawer/SideDrawer.less +++ b/src/routes/Player/SideDrawer/SideDrawer.less @@ -67,6 +67,24 @@ } } + .show-more-btn { + background: none; + border: none; + color: #fff; + cursor: pointer; + font: inherit; + padding: 0.1rem 1.5rem 0.2rem 1.5rem; + margin: 0.1rem auto 0.2rem auto; + display: block; + text-align: center; + transition: text-decoration 0.2s; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + .series-content { flex: 2; display: flex; diff --git a/src/routes/Player/SideDrawer/SideDrawer.tsx b/src/routes/Player/SideDrawer/SideDrawer.tsx index cb94e24e5..5e404f402 100644 --- a/src/routes/Player/SideDrawer/SideDrawer.tsx +++ b/src/routes/Player/SideDrawer/SideDrawer.tsx @@ -19,6 +19,8 @@ type Props = { const SideDrawer = memo(forwardRef(({ seriesInfo, className, closeSideDrawer, ...props }: Props, ref) => { const { core } = useServices(); const [season, setSeason] = useState(seriesInfo?.season); + const [showFullDescription, setShowFullDescription] = useState(false); + const toggleDescription = () => setShowFullDescription((prev) => !prev); const metaItem = useMemo(() => { return seriesInfo ? { @@ -42,6 +44,13 @@ const SideDrawer = memo(forwardRef(({ seriesInfo, classNa }) .sort((a, b) => (a || Number.MAX_SAFE_INTEGER) - (b || Number.MAX_SAFE_INTEGER)); }, [props.metaItem.videos]); + const description = useMemo(() => { + if (!metaItem.description) return ''; + if (showFullDescription || metaItem.description.length <= 175) return metaItem.description; + const sliced = metaItem.description.slice(0, 175); + const lastSpace = sliced.lastIndexOf(' '); + return (lastSpace > 0 ? sliced.slice(0, lastSpace) : sliced) + '...'; + }, [metaItem.description, showFullDescription]) const seasonOnSelect = useCallback((event: { value: string }) => { setSeason(parseInt(event.value)); @@ -89,10 +98,18 @@ const SideDrawer = memo(forwardRef(({ seriesInfo, classNa runtime={metaItem.runtime} releaseInfo={metaItem.releaseInfo} released={metaItem.released} - description={metaItem.description} + description={description} links={metaItem.links} />
+ {metaItem.description && metaItem.description.length > 175 && ( + + )} { seriesInfo ?
From d5eb6a515faced8ad2a33d66f493d69300d2a7b7 Mon Sep 17 00:00:00 2001 From: val_makkas Date: Sat, 14 Jun 2025 08:49:48 +0300 Subject: [PATCH 29/39] fixed info box styling --- src/routes/Player/SideDrawer/SideDrawer.less | 20 +------------------- src/routes/Player/SideDrawer/SideDrawer.tsx | 19 +------------------ 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/src/routes/Player/SideDrawer/SideDrawer.less b/src/routes/Player/SideDrawer/SideDrawer.less index 3c2c12401..eda08d118 100644 --- a/src/routes/Player/SideDrawer/SideDrawer.less +++ b/src/routes/Player/SideDrawer/SideDrawer.less @@ -57,7 +57,7 @@ .info { padding: @padding; overflow-y: auto; - flex: none; + flex: 1; .side-drawer-meta-preview { .action-buttons-container { @@ -67,24 +67,6 @@ } } - .show-more-btn { - background: none; - border: none; - color: #fff; - cursor: pointer; - font: inherit; - padding: 0.1rem 1.5rem 0.2rem 1.5rem; - margin: 0.1rem auto 0.2rem auto; - display: block; - text-align: center; - transition: text-decoration 0.2s; - text-decoration: none; - - &:hover { - text-decoration: underline; - } - } - .series-content { flex: 2; display: flex; diff --git a/src/routes/Player/SideDrawer/SideDrawer.tsx b/src/routes/Player/SideDrawer/SideDrawer.tsx index 5e404f402..cb94e24e5 100644 --- a/src/routes/Player/SideDrawer/SideDrawer.tsx +++ b/src/routes/Player/SideDrawer/SideDrawer.tsx @@ -19,8 +19,6 @@ type Props = { const SideDrawer = memo(forwardRef(({ seriesInfo, className, closeSideDrawer, ...props }: Props, ref) => { const { core } = useServices(); const [season, setSeason] = useState(seriesInfo?.season); - const [showFullDescription, setShowFullDescription] = useState(false); - const toggleDescription = () => setShowFullDescription((prev) => !prev); const metaItem = useMemo(() => { return seriesInfo ? { @@ -44,13 +42,6 @@ const SideDrawer = memo(forwardRef(({ seriesInfo, classNa }) .sort((a, b) => (a || Number.MAX_SAFE_INTEGER) - (b || Number.MAX_SAFE_INTEGER)); }, [props.metaItem.videos]); - const description = useMemo(() => { - if (!metaItem.description) return ''; - if (showFullDescription || metaItem.description.length <= 175) return metaItem.description; - const sliced = metaItem.description.slice(0, 175); - const lastSpace = sliced.lastIndexOf(' '); - return (lastSpace > 0 ? sliced.slice(0, lastSpace) : sliced) + '...'; - }, [metaItem.description, showFullDescription]) const seasonOnSelect = useCallback((event: { value: string }) => { setSeason(parseInt(event.value)); @@ -98,18 +89,10 @@ const SideDrawer = memo(forwardRef(({ seriesInfo, classNa runtime={metaItem.runtime} releaseInfo={metaItem.releaseInfo} released={metaItem.released} - description={description} + description={metaItem.description} links={metaItem.links} />
- {metaItem.description && metaItem.description.length > 175 && ( - - )} { seriesInfo ?
From 86bd1b276a37756268b38e803fff0a953a83d94e Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Sat, 14 Jun 2025 18:24:57 +0300 Subject: [PATCH 30/39] fix(SideDrawer): mobile styles --- src/components/MultiselectMenu/Dropdown/Dropdown.less | 2 +- src/routes/Player/SideDrawer/SideDrawer.less | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/MultiselectMenu/Dropdown/Dropdown.less b/src/components/MultiselectMenu/Dropdown/Dropdown.less index 3bea17d22..2373241f2 100644 --- a/src/components/MultiselectMenu/Dropdown/Dropdown.less +++ b/src/components/MultiselectMenu/Dropdown/Dropdown.less @@ -2,7 +2,7 @@ @import (reference) '~stremio/common/screen-sizes.less'; -@parent-height: 10rem; +@parent-height: 12rem; .dropdown { background: var(--modal-background-color); diff --git a/src/routes/Player/SideDrawer/SideDrawer.less b/src/routes/Player/SideDrawer/SideDrawer.less index eda08d118..0fe22f58a 100644 --- a/src/routes/Player/SideDrawer/SideDrawer.less +++ b/src/routes/Player/SideDrawer/SideDrawer.less @@ -78,12 +78,6 @@ } } -@media screen and (max-width: @small) { - .side-drawer { - max-width: 40dvw; - } -} - @media @phone-portrait { .side-drawer { max-width: 100dvw; From f6d4e3f4a60c808d701d649cbc165fea631d8099 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 16 Jun 2025 15:22:10 +0200 Subject: [PATCH 31/39] feat: add player subtitles delay shortcuts --- src/common/animations.less | 13 ++++ src/routes/Player/Indicator/Indicator.less | 23 +++++++ src/routes/Player/Indicator/Indicator.tsx | 73 ++++++++++++++++++++++ src/routes/Player/Player.js | 47 +++++++++++++- src/routes/Player/styles.less | 7 +++ src/routes/Settings/Settings.js | 10 +++ 6 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 src/routes/Player/Indicator/Indicator.less create mode 100644 src/routes/Player/Indicator/Indicator.tsx diff --git a/src/common/animations.less b/src/common/animations.less index 91dbe386d..3b9815f14 100644 --- a/src/common/animations.less +++ b/src/common/animations.less @@ -82,6 +82,19 @@ transform: translateY(100%); } +.fade-enter { + opacity: 0; +} + +.fade-active { + opacity: 1; + transition: opacity 0.3s cubic-bezier(0.32, 0, 0.67, 0); +} + +.fade-exit { + opacity: 0; +} + @keyframes fade-in-no-motion { 0% { opacity: 0; diff --git a/src/routes/Player/Indicator/Indicator.less b/src/routes/Player/Indicator/Indicator.less new file mode 100644 index 000000000..699c53d23 --- /dev/null +++ b/src/routes/Player/Indicator/Indicator.less @@ -0,0 +1,23 @@ +.indicator-container { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + height: 4rem; + user-select: none; + + .indicator { + flex: none; + position: relative; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + padding: 0 2rem; + border-radius: 4rem; + text-align: center; + font-weight: bold; + color: var(--primary-foreground-color); + background-color: var(--modal-background-color); + } +} \ No newline at end of file diff --git a/src/routes/Player/Indicator/Indicator.tsx b/src/routes/Player/Indicator/Indicator.tsx new file mode 100644 index 000000000..cc0ef4ccf --- /dev/null +++ b/src/routes/Player/Indicator/Indicator.tsx @@ -0,0 +1,73 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import classNames from 'classnames'; +import { t } from 'i18next'; +import { Transition } from 'stremio/components'; +import { useBinaryState } from 'stremio/common'; +import styles from './Indicator.less'; + +type Property = { + label: string, + format: (value: number) => string, +}; + +const PROPERTIES: Record = { + 'extraSubtitlesDelay': { + label: 'SUBTITLES_DELAY', + format: (value) => `${(value / 1000).toFixed(2)}s`, + }, +}; + +type VideoState = Record; + +type Props = { + className: string, + videoState: VideoState, +}; + +const Indicator = ({ className, videoState }: Props) => { + const timeout = useRef(null); + const prevVideoState = useRef(videoState); + + const [shown, show, hide] = useBinaryState(false); + const [current, setCurrent] = useState(null); + + const label = useMemo(() => { + const property = current && PROPERTIES[current]; + return property && t(property.label); + }, [current]); + + const value = useMemo(() => { + const property = current && PROPERTIES[current]; + const value = current && videoState[current]; + return property && value && property.format(value); + }, [current, videoState]); + + useEffect(() => { + for (const property of Object.keys(PROPERTIES)) { + const prev = prevVideoState.current[property]; + const next = videoState[property]; + + if (next && next !== prev) { + setCurrent(property); + show(); + + timeout.current && clearTimeout(timeout.current); + timeout.current = setTimeout(hide, 1000); + } + } + + prevVideoState.current = videoState; + }, [videoState]); + + return ( + +
+
+
{label} {value}
+
+
+
+ ); +}; + +export default Indicator; diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 8ba813786..65c02534f 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -27,6 +27,7 @@ const useStatistics = require('./useStatistics'); const useVideo = require('./useVideo'); const styles = require('./styles'); const Video = require('./Video'); +const { default: Indicator } = require('./Indicator/Indicator'); const Player = ({ urlParams, queryParams }) => { const { t } = useTranslation(); @@ -216,6 +217,16 @@ const Player = ({ urlParams, queryParams }) => { video.setProp('extraSubtitlesDelay', delay); }, []); + const onIncreaseSubtitlesDelay = React.useCallback(() => { + const delay = video.state.extraSubtitlesDelay + 250; + onExtraSubtitlesDelayChanged(delay); + }, [video.state.extraSubtitlesDelay, onExtraSubtitlesDelayChanged]); + + const onDecreaseSubtitlesDelay = React.useCallback(() => { + const delay = video.state.extraSubtitlesDelay - 250; + onExtraSubtitlesDelayChanged(delay); + }, [video.state.extraSubtitlesDelay, onExtraSubtitlesDelayChanged]); + const onSubtitlesSizeChanged = React.useCallback((size) => { updateSettings({ subtitlesSize: size }); }, [updateSettings]); @@ -587,6 +598,14 @@ const Player = ({ urlParams, queryParams }) => { break; } + case 'KeyG': { + onDecreaseSubtitlesDelay(); + break; + } + case 'KeyH': { + onIncreaseSubtitlesDelay(); + break; + } case 'Escape': { closeMenus(); !settings.escExitFullscreen && window.history.back(); @@ -620,7 +639,29 @@ const Player = ({ urlParams, queryParams }) => { window.removeEventListener('keyup', onKeyUp); window.removeEventListener('wheel', onWheel); }; - }, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, settings.seekShortTimeDuration, settings.escExitFullscreen, 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]); + }, [ + player.metaItem, + player.selected, + streamingServer.statistics, + settings.seekTimeDuration, + settings.seekShortTimeDuration, + settings.escExitFullscreen, + 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, + onDecreaseSubtitlesDelay, + onIncreaseSubtitlesDelay, + ]); React.useEffect(() => { video.events.on('error', onError); @@ -760,6 +801,10 @@ const Player = ({ urlParams, queryParams }) => { onMouseOver={onBarMouseMove} onTouchEnd={onContainerMouseLeave} /> + { nextVideoPopupOpen ? { F
+
+
+
{ t('SETTINGS_SHORTCUT_SUBTITLES_DELAY') }
+
+
+ G +
and
+ H +
+
{ t('SETTINGS_SHORTCUT_NAVIGATE_MENUS') }
From 7ec7e8eb03f0766155e285fd6391821022e79ec2 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 18 Jun 2025 09:48:12 +0200 Subject: [PATCH 32/39] refactor: rewrite settings route --- package-lock.json | 29 + package.json | 2 + src/common/Toast/useToast.d.ts | 11 + src/components/Button/Button.tsx | 1 + .../{styles.less => ColorInput.less} | 0 .../{ColorInput.js => ColorInput.tsx} | 80 +- .../ColorInput/ColorPicker/ColorPicker.js | 5 +- src/components/ColorInput/index.js | 6 - src/components/ColorInput/index.ts | 6 + .../MultiselectMenu/Dropdown/Dropdown.tsx | 6 +- .../Dropdown/Option/Option.tsx | 4 +- .../MultiselectMenu/MultiselectMenu.tsx | 7 +- src/components/MultiselectMenu/types.d.ts | 2 +- src/components/Toggle/Toggle.js | 26 - .../Toggle/{styles.less => Toggle.less} | 0 src/components/Toggle/Toggle.tsx | 27 + src/components/Toggle/index.js | 5 - src/components/Toggle/index.ts | 5 + src/modules.d.ts | 2 + src/routes/Settings/General/General.less | 11 + src/routes/Settings/General/General.tsx | 182 ++++ src/routes/Settings/General/User/User.less | 87 ++ src/routes/Settings/General/User/User.tsx | 65 ++ src/routes/Settings/General/User/index.ts | 2 + src/routes/Settings/General/index.ts | 2 + .../Settings/General/useDataExport.d.ts | 6 + .../Settings/{ => General}/useDataExport.js | 0 .../Settings/General/useGeneralOptions.ts | 84 ++ src/routes/Settings/Info/Info.less | 31 + src/routes/Settings/Info/Info.tsx | 50 ++ src/routes/Settings/Info/index.ts | 2 + src/routes/Settings/Menu/Menu.less | 62 ++ src/routes/Settings/Menu/Menu.tsx | 61 ++ src/routes/Settings/Menu/index.ts | 2 + src/routes/Settings/Player/Player.tsx | 146 ++++ src/routes/Settings/Player/index.ts | 2 + .../usePlayerOptions.ts} | 157 ++-- src/routes/Settings/Settings.js | 809 ------------------ src/routes/Settings/Settings.less | 35 + src/routes/Settings/Settings.tsx | 109 +++ src/routes/Settings/Shortcuts/Shortcuts.less | 27 + src/routes/Settings/Shortcuts/Shortcuts.tsx | 88 ++ src/routes/Settings/Shortcuts/index.ts | 2 + src/routes/Settings/Streaming/Streaming.less | 44 + src/routes/Settings/Streaming/Streaming.tsx | 91 ++ .../URLsManager/AddItem/AddItem.less | 0 .../URLsManager/AddItem/AddItem.tsx | 0 .../URLsManager/AddItem/index.ts | 0 .../URLsManager/Item/Item.less | 0 .../{ => Streaming}/URLsManager/Item/Item.tsx | 0 .../{ => Streaming}/URLsManager/Item/index.ts | 0 .../URLsManager/URLsManager.less | 2 + .../URLsManager/URLsManager.tsx | 0 .../{ => Streaming}/URLsManager/index.ts | 0 .../URLsManager/useStreamingServerUrls.js | 0 src/routes/Settings/Streaming/index.ts | 2 + .../useStreamingOptions.ts} | 117 ++- .../components/Category/Category.less | 37 + .../Settings/components/Category/Category.tsx | 26 + .../Settings/components/Category/index.ts | 2 + src/routes/Settings/components/Link/Link.less | 16 + src/routes/Settings/components/Link/Link.tsx | 20 + src/routes/Settings/components/Link/index.ts | 2 + .../Settings/components/Option/Option.less | 78 ++ .../Settings/components/Option/Option.tsx | 36 + .../Settings/components/Option/index.ts | 2 + .../Settings/components/Section/Section.less | 22 + .../Settings/components/Section/Section.tsx | 26 + .../Settings/components/Section/index.ts | 2 + src/routes/Settings/components/index.ts | 11 + src/routes/Settings/constants.ts | 10 + src/routes/Settings/index.js | 5 - src/routes/Settings/index.ts | 4 + src/routes/Settings/styles.less | 466 ---------- src/routes/index.js | 2 +- src/services/Shell/Shell.d.ts | 11 + src/types/models/Ctx.d.ts | 2 + src/types/models/DataExport.d.ts | 3 + src/types/models/StremingServer.d.ts | 12 + 79 files changed, 1778 insertions(+), 1519 deletions(-) create mode 100644 src/common/Toast/useToast.d.ts rename src/components/ColorInput/{styles.less => ColorInput.less} (100%) rename src/components/ColorInput/{ColorInput.js => ColorInput.tsx} (56%) delete mode 100644 src/components/ColorInput/index.js create mode 100644 src/components/ColorInput/index.ts delete mode 100644 src/components/Toggle/Toggle.js rename src/components/Toggle/{styles.less => Toggle.less} (100%) create mode 100644 src/components/Toggle/Toggle.tsx delete mode 100644 src/components/Toggle/index.js create mode 100644 src/components/Toggle/index.ts create mode 100644 src/routes/Settings/General/General.less create mode 100644 src/routes/Settings/General/General.tsx create mode 100644 src/routes/Settings/General/User/User.less create mode 100644 src/routes/Settings/General/User/User.tsx create mode 100644 src/routes/Settings/General/User/index.ts create mode 100644 src/routes/Settings/General/index.ts create mode 100644 src/routes/Settings/General/useDataExport.d.ts rename src/routes/Settings/{ => General}/useDataExport.js (100%) create mode 100644 src/routes/Settings/General/useGeneralOptions.ts create mode 100644 src/routes/Settings/Info/Info.less create mode 100644 src/routes/Settings/Info/Info.tsx create mode 100644 src/routes/Settings/Info/index.ts create mode 100644 src/routes/Settings/Menu/Menu.less create mode 100644 src/routes/Settings/Menu/Menu.tsx create mode 100644 src/routes/Settings/Menu/index.ts create mode 100644 src/routes/Settings/Player/Player.tsx create mode 100644 src/routes/Settings/Player/index.ts rename src/routes/Settings/{useProfileSettingsInputs.js => Player/usePlayerOptions.ts} (68%) delete mode 100644 src/routes/Settings/Settings.js create mode 100644 src/routes/Settings/Settings.less create mode 100644 src/routes/Settings/Settings.tsx create mode 100644 src/routes/Settings/Shortcuts/Shortcuts.less create mode 100644 src/routes/Settings/Shortcuts/Shortcuts.tsx create mode 100644 src/routes/Settings/Shortcuts/index.ts create mode 100644 src/routes/Settings/Streaming/Streaming.less create mode 100644 src/routes/Settings/Streaming/Streaming.tsx rename src/routes/Settings/{ => Streaming}/URLsManager/AddItem/AddItem.less (100%) rename src/routes/Settings/{ => Streaming}/URLsManager/AddItem/AddItem.tsx (100%) rename src/routes/Settings/{ => Streaming}/URLsManager/AddItem/index.ts (100%) rename src/routes/Settings/{ => Streaming}/URLsManager/Item/Item.less (100%) rename src/routes/Settings/{ => Streaming}/URLsManager/Item/Item.tsx (100%) rename src/routes/Settings/{ => Streaming}/URLsManager/Item/index.ts (100%) rename src/routes/Settings/{ => Streaming}/URLsManager/URLsManager.less (98%) rename src/routes/Settings/{ => Streaming}/URLsManager/URLsManager.tsx (100%) rename src/routes/Settings/{ => Streaming}/URLsManager/index.ts (100%) rename src/routes/Settings/{ => Streaming}/URLsManager/useStreamingServerUrls.js (100%) create mode 100644 src/routes/Settings/Streaming/index.ts rename src/routes/Settings/{useStreamingServerSettingsInputs.js => Streaming/useStreamingOptions.ts} (60%) create mode 100644 src/routes/Settings/components/Category/Category.less create mode 100644 src/routes/Settings/components/Category/Category.tsx create mode 100644 src/routes/Settings/components/Category/index.ts create mode 100644 src/routes/Settings/components/Link/Link.less create mode 100644 src/routes/Settings/components/Link/Link.tsx create mode 100644 src/routes/Settings/components/Link/index.ts create mode 100644 src/routes/Settings/components/Option/Option.less create mode 100644 src/routes/Settings/components/Option/Option.tsx create mode 100644 src/routes/Settings/components/Option/index.ts create mode 100644 src/routes/Settings/components/Section/Section.less create mode 100644 src/routes/Settings/components/Section/Section.tsx create mode 100644 src/routes/Settings/components/Section/index.ts create mode 100644 src/routes/Settings/components/index.ts create mode 100644 src/routes/Settings/constants.ts delete mode 100644 src/routes/Settings/index.js create mode 100644 src/routes/Settings/index.ts delete mode 100644 src/routes/Settings/styles.less create mode 100644 src/services/Shell/Shell.d.ts create mode 100644 src/types/models/DataExport.d.ts diff --git a/package-lock.json b/package-lock.json index 79e66d642..91421acc5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,8 @@ "@stylistic/eslint-plugin": "^2.11.0", "@stylistic/eslint-plugin-jsx": "^2.11.0", "@types/hat": "^0.0.4", + "@types/lodash.isequal": "^4.5.8", + "@types/lodash.throttle": "^4.1.9", "@types/react": "^18.3.13", "@types/react-dom": "^18.3.1", "babel-loader": "9.2.1", @@ -3806,6 +3808,33 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.18", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.18.tgz", + "integrity": "sha512-KJ65INaxqxmU6EoCiJmRPZC9H9RVWCRd349tXM2M3O5NA7cY6YL7c0bHAHQ93NOfTObEQ004kd2QVHs/r0+m4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash.isequal": { + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.8.tgz", + "integrity": "sha512-uput6pg4E/tj2LGxCZo9+y27JNyB2OZuuI/T5F+ylVDYuqICLG2/ktjxx0v6GvVntAf8TvEzeQLcV0ffRirXuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/lodash.throttle": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz", + "integrity": "sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", diff --git a/package.json b/package.json index 5ed9368ab..bc215334b 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "@stylistic/eslint-plugin": "^2.11.0", "@stylistic/eslint-plugin-jsx": "^2.11.0", "@types/hat": "^0.0.4", + "@types/lodash.isequal": "^4.5.8", + "@types/lodash.throttle": "^4.1.9", "@types/react": "^18.3.13", "@types/react-dom": "^18.3.1", "babel-loader": "9.2.1", diff --git a/src/common/Toast/useToast.d.ts b/src/common/Toast/useToast.d.ts new file mode 100644 index 000000000..e74d7ade8 --- /dev/null +++ b/src/common/Toast/useToast.d.ts @@ -0,0 +1,11 @@ +type ToastOptions = { + type: string, + title: string, + timeout: number, +}; + +declare const useToast: () => { + show: (options: ToastOptions) => void, +}; + +export = useToast; diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index f97e1ba82..dcce809d2 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -7,6 +7,7 @@ import styles from './Button.less'; type Props = { className?: string, + style?: object, href?: string, target?: string title?: string, diff --git a/src/components/ColorInput/styles.less b/src/components/ColorInput/ColorInput.less similarity index 100% rename from src/components/ColorInput/styles.less rename to src/components/ColorInput/ColorInput.less diff --git a/src/components/ColorInput/ColorInput.js b/src/components/ColorInput/ColorInput.tsx similarity index 56% rename from src/components/ColorInput/ColorInput.js rename to src/components/ColorInput/ColorInput.tsx index afa411f63..93a197fba 100644 --- a/src/components/ColorInput/ColorInput.js +++ b/src/components/ColorInput/ColorInput.tsx @@ -1,55 +1,62 @@ // Copyright (C) 2017-2023 Smart code 203358507 -const React = require('react'); -const PropTypes = require('prop-types'); -const classnames = require('classnames'); -const AColorPicker = require('a-color-picker'); -const { useTranslation } = require('react-i18next'); -const { Button } = require('stremio/components'); -const ModalDialog = require('stremio/components/ModalDialog'); -const useBinaryState = require('stremio/common/useBinaryState'); -const ColorPicker = require('./ColorPicker'); -const styles = require('./styles'); +import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react'; +import classnames from 'classnames'; +import * as AColorPicker from 'a-color-picker'; +import { useTranslation } from 'react-i18next'; +import { Button } from 'stremio/components'; +import ModalDialog from 'stremio/components/ModalDialog'; +import useBinaryState from 'stremio/common/useBinaryState'; +import ColorPicker from './ColorPicker'; +import styles from './ColorInput.less'; -const parseColor = (value) => { +const parseColor = (value: string) => { const color = AColorPicker.parseColor(value, 'hexcss4'); return typeof color === 'string' ? color : '#ffffffff'; }; -const ColorInput = ({ className, value, dataset, onChange, ...props }) => { +type Props = { + className: string, + value: string, + onChange?: (value: string) => void, + onClick?: (event: React.MouseEvent) => void, +}; + +const ColorInput = ({ className, value, onChange, ...props }: Props) => { const { t } = useTranslation(); const [modalOpen, openModal, closeModal] = useBinaryState(false); - const [tempValue, setTempValue] = React.useState(() => { + const [tempValue, setTempValue] = useState(() => { return parseColor(value); }); - const labelButtonStyle = React.useMemo(() => ({ + + const labelButtonStyle = useMemo(() => ({ backgroundColor: value }), [value]); - const isTransparent = React.useMemo(() => { + + const isTransparent = useMemo(() => { return parseColor(value).endsWith('00'); }, [value]); - const labelButtonOnClick = React.useCallback((event) => { + + const labelButtonOnClick = useCallback((event: React.MouseEvent) => { if (typeof props.onClick === 'function') { props.onClick(event); } + // @ts-expect-error: Property 'openModalPrevented' does not exist on type 'MouseEvent'. if (!event.nativeEvent.openModalPrevented) { openModal(); } }, [props.onClick]); - const modalDialogOnClick = React.useCallback((event) => { + + const modalDialogOnClick = useCallback((event: React.MouseEvent) => { + // @ts-expect-error: Property 'openModalPrevented' does not exist on type 'MouseEvent'. event.nativeEvent.openModalPrevented = true; }, []); - const modalButtons = React.useMemo(() => { - const selectButtonOnClick = (event) => { + + const modalButtons = useMemo(() => { + const selectButtonOnClick = () => { if (typeof onChange === 'function') { - onChange({ - type: 'change', - value: tempValue, - dataset: dataset, - reactEvent: event, - nativeEvent: event.nativeEvent - }); + onChange(tempValue); } closeModal(); @@ -63,13 +70,16 @@ const ColorInput = ({ className, value, dataset, onChange, ...props }) => { } } ]; - }, [tempValue, dataset, onChange]); - const colorPickerOnInput = React.useCallback((event) => { - setTempValue(parseColor(event.value)); + }, [tempValue, onChange]); + + const colorPickerOnInput = useCallback((color: string) => { + setTempValue(parseColor(color)); }, []); - React.useLayoutEffect(() => { + + useLayoutEffect(() => { setTempValue(parseColor(value)); }, [value, modalOpen]); + return ( - ); -}); - -Toggle.displayName = 'Toggle'; - -Toggle.propTypes = { - className: PropTypes.string, - checked: PropTypes.bool, - children: PropTypes.node -}; - -module.exports = Toggle; diff --git a/src/components/Toggle/styles.less b/src/components/Toggle/Toggle.less similarity index 100% rename from src/components/Toggle/styles.less rename to src/components/Toggle/Toggle.less diff --git a/src/components/Toggle/Toggle.tsx b/src/components/Toggle/Toggle.tsx new file mode 100644 index 000000000..ea14b6258 --- /dev/null +++ b/src/components/Toggle/Toggle.tsx @@ -0,0 +1,27 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +import React, { forwardRef } from 'react'; +import classnames from 'classnames'; +import { Button } from 'stremio/components'; +import styles from './Toggle.less'; + +type Props = { + className?: string, + checked: boolean, + disabled?: boolean, + tabIndex?: number, + children?: React.ReactNode, +}; + +const Toggle = forwardRef(({ className, checked, children, ...props }: Props, ref) => { + return ( + + ); +}); + +Toggle.displayName = 'Toggle'; + +export default Toggle; diff --git a/src/components/Toggle/index.js b/src/components/Toggle/index.js deleted file mode 100644 index ae6c69d8a..000000000 --- a/src/components/Toggle/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const Toggle = require('./Toggle'); - -module.exports = Toggle; diff --git a/src/components/Toggle/index.ts b/src/components/Toggle/index.ts new file mode 100644 index 000000000..2884e5394 --- /dev/null +++ b/src/components/Toggle/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +import Toggle from './Toggle'; + +export default Toggle; diff --git a/src/modules.d.ts b/src/modules.d.ts index bf968c8d4..3d5516efd 100644 --- a/src/modules.d.ts +++ b/src/modules.d.ts @@ -3,4 +3,6 @@ declare module '*.less' { export = resource; } +declare module 'stremio-router'; declare module 'stremio/components/NavBar'; +declare module 'stremio/components/ModalDialog'; diff --git a/src/routes/Settings/General/General.less b/src/routes/Settings/General/General.less new file mode 100644 index 000000000..8c253dcff --- /dev/null +++ b/src/routes/Settings/General/General.less @@ -0,0 +1,11 @@ +:import('~stremio/routes/Settings/components/Option/Option.less') { + option-icon: icon; +} + +.trakt-container { + margin-top: 2rem; + + .option-icon { + color: var(--color-trakt) !important; + } +} diff --git a/src/routes/Settings/General/General.tsx b/src/routes/Settings/General/General.tsx new file mode 100644 index 000000000..73bf38a48 --- /dev/null +++ b/src/routes/Settings/General/General.tsx @@ -0,0 +1,182 @@ +import React, { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, MultiselectMenu, Toggle } from 'stremio/components'; +import { useServices } from 'stremio/services'; +import { usePlatform, useToast } from 'stremio/common'; +import { Section, Option, Link } from '../components'; +import User from './User'; +import useDataExport from './useDataExport'; +import styles from './General.less'; +import useGeneralOptions from './useGeneralOptions'; + +type Props = { + profile: Profile, +}; + +const General = forwardRef(({ profile }: Props, ref) => { + const { t } = useTranslation(); + const { core, shell } = useServices(); + const platform = usePlatform(); + const toast = useToast(); + const [dataExport, loadDataExport] = useDataExport(); + + const { + interfaceLanguageSelect, + quitOnCloseToggle, + escExitFullscreenToggle, + hideSpoilersToggle, + } = useGeneralOptions(profile); + + const [traktAuthStarted, setTraktAuthStarted] = useState(false); + + const isTraktAuthenticated = useMemo(() => { + const trakt = profile?.auth?.user?.trakt; + return trakt && (Date.now() / 1000) < (trakt.created_at + trakt.expires_in); + }, [profile.auth]); + + const onExportData = useCallback(() => { + loadDataExport(); + }, []); + + const onCalendarSubscribe = useCallback(() => { + if (!profile.auth) return; + + const protocol = platform.name === 'ios' ? 'webcal' : 'https'; + const url = `${protocol}://www.strem.io/calendar/${profile.auth.user._id}.ics`; + platform.openExternal(url); + + toast.show({ + type: 'success', + title: platform.name === 'ios' ? + t('SETTINGS_SUBSCRIBE_CALENDAR_IOS_TOAST') : + t('SETTINGS_SUBSCRIBE_CALENDAR_TOAST'), + timeout: 25000 + }); + // Stremio 4 emits not documented event subscribeCalendar + }, [profile.auth]); + + const onToggleTrakt = useCallback(() => { + if (!isTraktAuthenticated && profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user._id === 'string') { + platform.openExternal(`https://www.strem.io/trakt/auth/${profile.auth.user._id}`); + setTraktAuthStarted(true); + } else { + core.transport.dispatch({ + action: 'Ctx', + args: { + action: 'LogoutTrakt' + } + }); + } + }, [isTraktAuthenticated, profile.auth]); + + useEffect(() => { + if (dataExport.exportUrl) { + platform.openExternal(dataExport.exportUrl); + } + }, [dataExport.exportUrl]); + + useEffect(() => { + if (isTraktAuthenticated && traktAuthStarted) { + core.transport.dispatch({ + action: 'Ctx', + args: { + action: 'InstallTraktAddon' + } + }); + setTraktAuthStarted(false); + } + }, [isTraktAuthenticated, traktAuthStarted]); + + return <> +
+ +
+ +
+ { + profile?.auth?.user && + + } + { + profile?.auth?.user && + + } + + + + + { + profile?.auth?.user && + + } + { + profile?.auth?.user?.email && + + } + +
+ +
+ + { + shell.active && + + } + { + shell.active && + + } + +
+ ; +}); + +export default General; diff --git a/src/routes/Settings/General/User/User.less b/src/routes/Settings/General/User/User.less new file mode 100644 index 000000000..63c544f0c --- /dev/null +++ b/src/routes/Settings/General/User/User.less @@ -0,0 +1,87 @@ +@import (reference) '~stremio/common/screen-sizes.less'; + +.user { + gap: 1rem; + + .user-info-content { + flex: 1; + display: flex; + flex-direction: row; + align-items: center; + + .avatar-container { + flex: none; + align-self: stretch; + height: 5rem; + width: 5rem; + margin-right: 1rem; + border: 2px solid var(--primary-accent-color); + border-radius: 50%; + background-size: cover; + background-repeat: no-repeat; + background-position: center; + background-origin: content-box; + background-clip: content-box; + opacity: 0.9; + background-color: var(--primary-foreground-color); + } + + .email-logout-container { + flex: none; + display: flex; + flex-direction: column; + align-items: start; + + .email-label-container { + display: flex; + flex-direction: row; + align-items: center; + } + + .email-label-container { + .email-label { + flex: 1; + font-size: 1.1rem; + color: var(--primary-foreground-color); + opacity: 0.7; + } + } + } + } + + .user-panel-container { + flex: none; + display: flex; + flex-direction: row; + align-items: center; + width: 10rem; + height: 3.5rem; + border-radius: 3.5rem; + background-color: var(--overlay-color); + + &:hover { + outline: var(--focus-outline-size) solid var(--primary-foreground-color); + background-color: transparent; + } + + .user-panel-label { + flex: 1; + max-height: 2.4em; + padding: 0 0.5rem; + font-weight: 500; + text-align: center; + color: var(--primary-foreground-color); + } + } +} + +@media only screen and (max-width: @minimum) { + .user { + flex-direction: column; + align-items: flex-start; + + .user-panel-container { + width: 100% !important; + } + } +} \ No newline at end of file diff --git a/src/routes/Settings/General/User/User.tsx b/src/routes/Settings/General/User/User.tsx new file mode 100644 index 000000000..95fa66b0d --- /dev/null +++ b/src/routes/Settings/General/User/User.tsx @@ -0,0 +1,65 @@ +import React, { useCallback, useMemo } from 'react'; +import { t } from 'i18next'; +import { useServices } from 'stremio/services'; +import { Link } from '../../components'; +import styles from './User.less'; + +type Props = { + profile: Profile, +}; + +const User = ({ profile }: Props) => { + const { core } = useServices(); + + const avatar = useMemo(() => ( + !profile.auth ? + `url('${require('/images/anonymous.png')}')` + : + profile.auth.user.avatar ? + `url('${profile.auth.user.avatar}')` + : + `url('${require('/images/default_avatar.png')}')` + ), [profile.auth]); + + const onLogout = useCallback(() => { + core.transport.dispatch({ + action: 'Ctx', + args: { + action: 'Logout' + } + }); + }, []); + + return ( +
+
+
+
+
+
+ {profile.auth === null ? 'Anonymous user' : profile.auth.user.email} +
+
+ { + profile.auth !== null ? + + : + + } +
+
+
+ ); +}; + +export default User; diff --git a/src/routes/Settings/General/User/index.ts b/src/routes/Settings/General/User/index.ts new file mode 100644 index 000000000..8196fa7e1 --- /dev/null +++ b/src/routes/Settings/General/User/index.ts @@ -0,0 +1,2 @@ +import User from './User'; +export default User; diff --git a/src/routes/Settings/General/index.ts b/src/routes/Settings/General/index.ts new file mode 100644 index 000000000..7f9bcb00a --- /dev/null +++ b/src/routes/Settings/General/index.ts @@ -0,0 +1,2 @@ +import General from './General'; +export default General; diff --git a/src/routes/Settings/General/useDataExport.d.ts b/src/routes/Settings/General/useDataExport.d.ts new file mode 100644 index 000000000..5a24cf179 --- /dev/null +++ b/src/routes/Settings/General/useDataExport.d.ts @@ -0,0 +1,6 @@ +declare const useDataExport: () => [ + DataExport, + () => void, +]; + +export = useDataExport; diff --git a/src/routes/Settings/useDataExport.js b/src/routes/Settings/General/useDataExport.js similarity index 100% rename from src/routes/Settings/useDataExport.js rename to src/routes/Settings/General/useDataExport.js diff --git a/src/routes/Settings/General/useGeneralOptions.ts b/src/routes/Settings/General/useGeneralOptions.ts new file mode 100644 index 000000000..59e7b40c1 --- /dev/null +++ b/src/routes/Settings/General/useGeneralOptions.ts @@ -0,0 +1,84 @@ +import { useMemo } from 'react'; +import { interfaceLanguages } from 'stremio/common'; +import { useServices } from 'stremio/services'; + +const useGeneralOptions = (profile: Profile) => { + const { core } = useServices(); + + const interfaceLanguageSelect = useMemo(() => ({ + options: interfaceLanguages.map(({ name, codes }) => ({ + value: codes[0], + label: name, + })), + value: interfaceLanguages.find(({ codes }) => codes[1] === profile.settings.interfaceLanguage)?.codes?.[0] || profile.settings.interfaceLanguage, + onSelect: (value: string) => { + core.transport.dispatch({ + action: 'Ctx', + args: { + action: 'UpdateSettings', + args: { + ...profile.settings, + interfaceLanguage: value + } + } + }); + } + }), [profile.settings]); + + const escExitFullscreenToggle = useMemo(() => ({ + checked: profile.settings.escExitFullscreen, + onClick: () => { + core.transport.dispatch({ + action: 'Ctx', + args: { + action: 'UpdateSettings', + args: { + ...profile.settings, + escExitFullscreen: !profile.settings.escExitFullscreen + } + } + }); + } + }), [profile.settings]); + + const quitOnCloseToggle = useMemo(() => ({ + checked: profile.settings.quitOnClose, + onClick: () => { + core.transport.dispatch({ + action: 'Ctx', + args: { + action: 'UpdateSettings', + args: { + ...profile.settings, + quitOnClose: !profile.settings.quitOnClose + } + } + }); + } + }), [profile.settings]); + + const hideSpoilersToggle = useMemo(() => ({ + checked: profile.settings.hideSpoilers, + onClick: () => { + core.transport.dispatch({ + action: 'Ctx', + args: { + action: 'UpdateSettings', + args: { + ...profile.settings, + hideSpoilers: !profile.settings.hideSpoilers + } + } + }); + } + }), [profile.settings]); + + return { + interfaceLanguageSelect, + escExitFullscreenToggle, + quitOnCloseToggle, + hideSpoilersToggle, + }; +}; + +export default useGeneralOptions; diff --git a/src/routes/Settings/Info/Info.less b/src/routes/Settings/Info/Info.less new file mode 100644 index 000000000..bfb2641df --- /dev/null +++ b/src/routes/Settings/Info/Info.less @@ -0,0 +1,31 @@ +@import (reference) '~stremio/common/screen-sizes.less'; + +:import('~stremio/routes/Settings/components/Option/Option.less') { + option-content: content; +} + +.info { + display: none; + + .option-content { + color: var(--primary-foreground-color); + overflow: hidden; + + .label { + text-overflow: ellipsis; + white-space: nowrap; + } + } +} + +@media only screen and (max-width: @xsmall) { + .info { + display: flex; + } +} + +@media only screen and (max-width: @minimum) { + .info { + display: flex; + } +} \ No newline at end of file diff --git a/src/routes/Settings/Info/Info.tsx b/src/routes/Settings/Info/Info.tsx new file mode 100644 index 000000000..df81d075e --- /dev/null +++ b/src/routes/Settings/Info/Info.tsx @@ -0,0 +1,50 @@ +import React, { useMemo } from 'react'; +import { Option, Section } from '../components'; +import { useServices } from 'stremio/services'; +import styles from './Info.less'; + +type Props = { + streamingServer: StreamingServer, +}; + +const Info = ({ streamingServer }: Props) => { + const { shell } = useServices(); + + const settings = useMemo(() => ( + streamingServer?.settings?.type === 'Ready' ? + streamingServer.settings.content as StreamingServerSettings : null + ), [streamingServer?.settings]); + + return ( +
+ + + { + settings?.serverVersion && + + } + { + typeof shell?.transport?.props?.shellVersion === 'string' && + + } +
+ ); +}; + +export default Info; diff --git a/src/routes/Settings/Info/index.ts b/src/routes/Settings/Info/index.ts new file mode 100644 index 000000000..c7f185301 --- /dev/null +++ b/src/routes/Settings/Info/index.ts @@ -0,0 +1,2 @@ +import Info from './Info'; +export default Info; diff --git a/src/routes/Settings/Menu/Menu.less b/src/routes/Settings/Menu/Menu.less new file mode 100644 index 000000000..c9376ff33 --- /dev/null +++ b/src/routes/Settings/Menu/Menu.less @@ -0,0 +1,62 @@ +@import (reference) '~stremio/common/screen-sizes.less'; + +.menu { + flex: none; + align-self: stretch; + display: flex; + flex-direction: column; + width: 18rem; + padding: 3rem 1.5rem; + + .button { + flex: none; + align-self: stretch; + display: flex; + align-items: center; + height: 4rem; + border-radius: 4rem; + padding: 2rem; + margin-bottom: 0.5rem; + font-size: 1.1rem; + font-weight: 500; + color: var(--primary-foreground-color); + opacity: 0.4; + + &.selected { + font-weight: 600; + color: var(--primary-foreground-color); + background-color: var(--overlay-color); + opacity: 1; + } + + &:hover { + background-color: var(--overlay-color); + } + } + + .spacing { + flex: 1; + } + + .version-info-label { + flex: 0 1 auto; + margin: 0.5rem 0; + white-space: nowrap; + text-overflow: ellipsis; + color: var(--primary-foreground-color); + opacity: 0.3; + overflow: hidden; + } +} + +@media only screen and (max-width: @xsmall) { + .menu { + display: none; + } +} + +@media only screen and (max-width: @minimum) { + .menu { + display: none; + } +} \ No newline at end of file diff --git a/src/routes/Settings/Menu/Menu.tsx b/src/routes/Settings/Menu/Menu.tsx new file mode 100644 index 000000000..f2c2874de --- /dev/null +++ b/src/routes/Settings/Menu/Menu.tsx @@ -0,0 +1,61 @@ +import React, { useMemo } from 'react'; +import classNames from 'classnames'; +import { t } from 'i18next'; +import { useServices } from 'stremio/services'; +import { Button } from 'stremio/components'; +import { SECTIONS } from '../constants'; +import styles from './Menu.less'; + +type Props = { + selected: string, + streamingServer: StreamingServer, + onSelect: (event: React.MouseEvent) => void, +}; + +const Menu = ({ selected, streamingServer, onSelect }: Props) => { + const { shell } = useServices(); + + const settings = useMemo(() => ( + streamingServer?.settings?.type === 'Ready' ? + streamingServer.settings.content as StreamingServerSettings : null + ), [streamingServer?.settings]); + + return ( +
+ + + + + +
+
+ App Version: {process.env.VERSION} +
+
+ Build Version: {process.env.COMMIT_HASH} +
+ { + settings?.serverVersion && +
+ Server Version: {settings.serverVersion} +
+ } + { + typeof shell?.transport?.props?.shellVersion === 'string' && +
+ Shell Version: {shell.transport.props.shellVersion} +
+ } +
+ ); +}; + +export default Menu; diff --git a/src/routes/Settings/Menu/index.ts b/src/routes/Settings/Menu/index.ts new file mode 100644 index 000000000..b62044269 --- /dev/null +++ b/src/routes/Settings/Menu/index.ts @@ -0,0 +1,2 @@ +import Menu from './Menu'; +export default Menu; diff --git a/src/routes/Settings/Player/Player.tsx b/src/routes/Settings/Player/Player.tsx new file mode 100644 index 000000000..72a941e81 --- /dev/null +++ b/src/routes/Settings/Player/Player.tsx @@ -0,0 +1,146 @@ +import React, { forwardRef } from 'react'; +import { ColorInput, MultiselectMenu, Toggle } from 'stremio/components'; +import { useServices } from 'stremio/services'; +import { Category, Option, Section } from '../components'; +import usePlayerOptions from './usePlayerOptions'; + +type Props = { + profile: Profile, +}; + +const Player = forwardRef(({ profile }: Props, ref) => { + const { shell } = useServices(); + + const { + subtitlesLanguageSelect, + subtitlesSizeSelect, + subtitlesTextColorInput, + subtitlesBackgroundColorInput, + subtitlesOutlineColorInput, + audioLanguageSelect, + surroundSoundToggle, + seekTimeDurationSelect, + seekShortTimeDurationSelect, + playInExternalPlayerSelect, + nextVideoPopupDurationSelect, + bingeWatchingToggle, + playInBackgroundToggle, + hardwareDecodingToggle, + pauseOnMinimizeToggle, + } = usePlayerOptions(profile); + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + { + shell.active && + + } + { + shell.active && + + } + +
+ ); +}); + +export default Player; diff --git a/src/routes/Settings/Player/index.ts b/src/routes/Settings/Player/index.ts new file mode 100644 index 000000000..e513bdb21 --- /dev/null +++ b/src/routes/Settings/Player/index.ts @@ -0,0 +1,2 @@ +import Player from './Player'; +export default Player; diff --git a/src/routes/Settings/useProfileSettingsInputs.js b/src/routes/Settings/Player/usePlayerOptions.ts similarity index 68% rename from src/routes/Settings/useProfileSettingsInputs.js rename to src/routes/Settings/Player/usePlayerOptions.ts index 4bbe54059..04c263d54 100644 --- a/src/routes/Settings/useProfileSettingsInputs.js +++ b/src/routes/Settings/Player/usePlayerOptions.ts @@ -1,77 +1,25 @@ -// Copyright (C) 2017-2023 Smart code 203358507 +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useServices } from 'stremio/services'; +import { CONSTANTS, languageNames, usePlatform } from 'stremio/common'; -const React = require('react'); -const { useTranslation } = require('react-i18next'); -const { useServices } = require('stremio/services'); -const { CONSTANTS, usePlatform, interfaceLanguages, languageNames } = require('stremio/common'); +const LANGUAGES_NAMES: Record = languageNames; -const useProfileSettingsInputs = (profile) => { +const usePlayerOptions = (profile: Profile) => { const { t } = useTranslation(); const { core } = useServices(); const platform = usePlatform(); - // TODO combine those useMemo in one - const interfaceLanguageSelect = React.useMemo(() => ({ - options: interfaceLanguages.map(({ name, codes }) => ({ - value: codes[0], - label: name, - })), - value: interfaceLanguages.find(({ codes }) => codes[1] === profile.settings.interfaceLanguage)?.codes?.[0] || profile.settings.interfaceLanguage, - onSelect: (value) => { - core.transport.dispatch({ - action: 'Ctx', - args: { - action: 'UpdateSettings', - args: { - ...profile.settings, - interfaceLanguage: value - } - } - }); - } - }), [profile.settings]); - const hideSpoilersToggle = React.useMemo(() => ({ - checked: profile.settings.hideSpoilers, - onClick: () => { - core.transport.dispatch({ - action: 'Ctx', - args: { - action: 'UpdateSettings', - args: { - ...profile.settings, - hideSpoilers: !profile.settings.hideSpoilers - } - } - }); - } - }), [profile.settings]); - - const quitOnCloseToggle = React.useMemo(() => ({ - checked: profile.settings.quitOnClose, - onClick: () => { - core.transport.dispatch({ - action: 'Ctx', - args: { - action: 'UpdateSettings', - args: { - ...profile.settings, - quitOnClose: !profile.settings.quitOnClose - } - } - }); - } - }), [profile.settings]); - - const subtitlesLanguageSelect = React.useMemo(() => ({ + const subtitlesLanguageSelect = useMemo(() => ({ options: [ { value: null, label: t('NONE') }, - ...Object.keys(languageNames).map((code) => ({ + ...Object.keys(LANGUAGES_NAMES).map((code) => ({ value: code, - label: languageNames[code] + label: LANGUAGES_NAMES[code] })) ], value: profile.settings.subtitlesLanguage, - onSelect: (value) => { + onSelect: (value: string) => { core.transport.dispatch({ action: 'Ctx', args: { @@ -84,7 +32,8 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); - const subtitlesSizeSelect = React.useMemo(() => ({ + + const subtitlesSizeSelect = useMemo(() => ({ options: CONSTANTS.SUBTITLES_SIZES.map((size) => ({ value: `${size}`, label: `${size}%` @@ -93,7 +42,7 @@ const useProfileSettingsInputs = (profile) => { title: () => { return `${profile.settings.subtitlesSize}%`; }, - onSelect: (value) => { + onSelect: (value: string) => { core.transport.dispatch({ action: 'Ctx', args: { @@ -106,9 +55,10 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); - const subtitlesTextColorInput = React.useMemo(() => ({ + + const subtitlesTextColorInput = useMemo(() => ({ value: profile.settings.subtitlesTextColor, - onChange: (value) => { + onChange: (value: string) => { core.transport.dispatch({ action: 'Ctx', args: { @@ -121,9 +71,10 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); - const subtitlesBackgroundColorInput = React.useMemo(() => ({ + + const subtitlesBackgroundColorInput = useMemo(() => ({ value: profile.settings.subtitlesBackgroundColor, - onChange: (value) => { + onChange: (value: string) => { core.transport.dispatch({ action: 'Ctx', args: { @@ -136,9 +87,10 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); - const subtitlesOutlineColorInput = React.useMemo(() => ({ + + const subtitlesOutlineColorInput = useMemo(() => ({ value: profile.settings.subtitlesOutlineColor, - onChange: (value) => { + onChange: (value: string) => { core.transport.dispatch({ action: 'Ctx', args: { @@ -151,13 +103,14 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); - const audioLanguageSelect = React.useMemo(() => ({ - options: Object.keys(languageNames).map((code) => ({ + + const audioLanguageSelect = useMemo(() => ({ + options: Object.keys(LANGUAGES_NAMES).map((code) => ({ value: code, - label: languageNames[code] + label: LANGUAGES_NAMES [code] })), value: profile.settings.audioLanguage, - onSelect: (value) => { + onSelect: (value: string) => { core.transport.dispatch({ action: 'Ctx', args: { @@ -170,7 +123,8 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); - const surroundSoundToggle = React.useMemo(() => ({ + + const surroundSoundToggle = useMemo(() => ({ checked: profile.settings.surroundSound, onClick: () => { core.transport.dispatch({ @@ -185,23 +139,8 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); - const escExitFullscreenToggle = React.useMemo(() => ({ - checked: profile.settings.escExitFullscreen, - onClick: () => { - core.transport.dispatch({ - action: 'Ctx', - args: { - action: 'UpdateSettings', - args: { - ...profile.settings, - escExitFullscreen: !profile.settings.escExitFullscreen - } - } - }); - } - }), [profile.settings]); - const seekTimeDurationSelect = React.useMemo(() => ({ + const seekTimeDurationSelect = useMemo(() => ({ options: CONSTANTS.SEEK_TIME_DURATIONS.map((size) => ({ value: `${size}`, label: `${size / 1000} ${t('SECONDS')}` @@ -210,7 +149,7 @@ const useProfileSettingsInputs = (profile) => { title: () => { return `${profile.settings.seekTimeDuration / 1000} ${t('SECONDS')}`; }, - onSelect: (value) => { + onSelect: (value: string) => { core.transport.dispatch({ action: 'Ctx', args: { @@ -223,7 +162,8 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); - const seekShortTimeDurationSelect = React.useMemo(() => ({ + + const seekShortTimeDurationSelect = useMemo(() => ({ options: CONSTANTS.SEEK_TIME_DURATIONS.map((size) => ({ value: `${size}`, label: `${size / 1000} ${t('SECONDS')}` @@ -232,7 +172,7 @@ const useProfileSettingsInputs = (profile) => { title: () => { return `${profile.settings.seekShortTimeDuration / 1000} ${t('SECONDS')}`; }, - onSelect: (value) => { + onSelect: (value: string) => { core.transport.dispatch({ action: 'Ctx', args: { @@ -245,7 +185,8 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); - const playInExternalPlayerSelect = React.useMemo(() => ({ + + const playInExternalPlayerSelect = useMemo(() => ({ options: CONSTANTS.EXTERNAL_PLAYERS .filter(({ platforms }) => platforms.includes(platform.name)) .map(({ label, value }) => ({ @@ -257,7 +198,7 @@ const useProfileSettingsInputs = (profile) => { const selectedOption = CONSTANTS.EXTERNAL_PLAYERS.find(({ value }) => value === profile.settings.playerType); return selectedOption ? t(selectedOption.label, { defaultValue: selectedOption.label }) : profile.settings.playerType; }, - onSelect: (value) => { + onSelect: (value: string) => { core.transport.dispatch({ action: 'Ctx', args: { @@ -270,7 +211,8 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); - const nextVideoPopupDurationSelect = React.useMemo(() => ({ + + const nextVideoPopupDurationSelect = useMemo(() => ({ options: CONSTANTS.NEXT_VIDEO_POPUP_DURATIONS.map((duration) => ({ value: `${duration}`, label: duration === 0 ? 'Disabled' : `${duration / 1000} ${t('SECONDS')}` @@ -282,7 +224,7 @@ const useProfileSettingsInputs = (profile) => { : `${profile.settings.nextVideoNotificationDuration / 1000} ${t('SECONDS')}`; }, - onSelect: (value) => { + onSelect: (value: string) => { core.transport.dispatch({ action: 'Ctx', args: { @@ -295,7 +237,8 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); - const bingeWatchingToggle = React.useMemo(() => ({ + + const bingeWatchingToggle = useMemo(() => ({ checked: profile.settings.bingeWatching, onClick: () => { core.transport.dispatch({ @@ -310,7 +253,8 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); - const playInBackgroundToggle = React.useMemo(() => ({ + + const playInBackgroundToggle = useMemo(() => ({ checked: profile.settings.playInBackground, onClick: () => { core.transport.dispatch({ @@ -325,7 +269,8 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); - const hardwareDecodingToggle = React.useMemo(() => ({ + + const hardwareDecodingToggle = useMemo(() => ({ checked: profile.settings.hardwareDecoding, onClick: () => { core.transport.dispatch({ @@ -340,7 +285,8 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); - const pauseOnMinimizeToggle = React.useMemo(() => ({ + + const pauseOnMinimizeToggle = useMemo(() => ({ checked: profile.settings.pauseOnMinimize, onClick: () => { core.transport.dispatch({ @@ -355,9 +301,8 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); + return { - interfaceLanguageSelect, - hideSpoilersToggle, subtitlesLanguageSelect, subtitlesSizeSelect, subtitlesTextColorInput, @@ -365,8 +310,6 @@ const useProfileSettingsInputs = (profile) => { subtitlesOutlineColorInput, audioLanguageSelect, surroundSoundToggle, - escExitFullscreenToggle, - quitOnCloseToggle, seekTimeDurationSelect, seekShortTimeDurationSelect, playInExternalPlayerSelect, @@ -378,4 +321,4 @@ const useProfileSettingsInputs = (profile) => { }; }; -module.exports = useProfileSettingsInputs; +export default usePlayerOptions; diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js deleted file mode 100644 index 1f61374b9..000000000 --- a/src/routes/Settings/Settings.js +++ /dev/null @@ -1,809 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const React = require('react'); -const classnames = require('classnames'); -const throttle = require('lodash.throttle'); -const { useTranslation } = require('react-i18next'); -const { default: Icon } = require('@stremio/stremio-icons/react'); -const { useRouteFocused } = require('stremio-router'); -const { useServices } = require('stremio/services'); -const { useProfile, usePlatform, useStreamingServer, withCoreSuspender, useToast } = require('stremio/common'); -const { Button, ColorInput, MainNavBars, MultiselectMenu, Toggle } = require('stremio/components'); -const useProfileSettingsInputs = require('./useProfileSettingsInputs'); -const useStreamingServerSettingsInputs = require('./useStreamingServerSettingsInputs'); -const useDataExport = require('./useDataExport'); -const styles = require('./styles'); -const { default: URLsManager } = require('./URLsManager/URLsManager'); - -const GENERAL_SECTION = 'general'; -const PLAYER_SECTION = 'player'; -const STREAMING_SECTION = 'streaming'; -const SHORTCUTS_SECTION = 'shortcuts'; - -const Settings = () => { - const { t } = useTranslation(); - const { core, shell } = useServices(); - const { routeFocused } = useRouteFocused(); - const profile = useProfile(); - const [dataExport, loadDataExport] = useDataExport(); - const streamingServer = useStreamingServer(); - const platform = usePlatform(); - const toast = useToast(); - const { - interfaceLanguageSelect, - hideSpoilersToggle, - subtitlesLanguageSelect, - subtitlesSizeSelect, - subtitlesTextColorInput, - subtitlesBackgroundColorInput, - subtitlesOutlineColorInput, - audioLanguageSelect, - surroundSoundToggle, - seekTimeDurationSelect, - seekShortTimeDurationSelect, - escExitFullscreenToggle, - quitOnCloseToggle, - playInExternalPlayerSelect, - nextVideoPopupDurationSelect, - bingeWatchingToggle, - playInBackgroundToggle, - hardwareDecodingToggle, - pauseOnMinimizeToggle, - } = useProfileSettingsInputs(profile); - const { - streamingServerRemoteUrlInput, - remoteEndpointSelect, - cacheSizeSelect, - torrentProfileSelect, - transcodingProfileSelect, - } = useStreamingServerSettingsInputs(streamingServer); - const [traktAuthStarted, setTraktAuthStarted] = React.useState(false); - const isTraktAuthenticated = React.useMemo(() => { - return profile.auth !== null && profile.auth.user !== null && profile.auth.user.trakt !== null && - (Date.now() / 1000) < (profile.auth.user.trakt.created_at + profile.auth.user.trakt.expires_in); - }, [profile.auth]); - const logoutButtonOnClick = React.useCallback(() => { - core.transport.dispatch({ - action: 'Ctx', - args: { - action: 'Logout' - } - }); - }, []); - const toggleTraktOnClick = React.useCallback(() => { - if (!isTraktAuthenticated && profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user._id === 'string') { - platform.openExternal(`https://www.strem.io/trakt/auth/${profile.auth.user._id}`); - setTraktAuthStarted(true); - } else { - core.transport.dispatch({ - action: 'Ctx', - args: { - action: 'LogoutTrakt' - } - }); - } - }, [isTraktAuthenticated, profile.auth]); - const subscribeCalendarOnClick = React.useCallback(() => { - if (!profile.auth) return; - - const protocol = platform.name === 'ios' ? 'webcal' : 'https'; - const url = `${protocol}://www.strem.io/calendar/${profile.auth.user._id}.ics`; - platform.openExternal(url); - toast.show({ - type: 'success', - title: platform.name === 'ios' ? t('SETTINGS_SUBSCRIBE_CALENDAR_IOS_TOAST') : t('SETTINGS_SUBSCRIBE_CALENDAR_TOAST'), - timeout: 25000 - }); - // Stremio 4 emits not documented event subscribeCalendar - }, [profile.auth]); - const exportDataOnClick = React.useCallback(() => { - loadDataExport(); - }, []); - const onCopyRemoteUrlClick = React.useCallback(() => { - if (streamingServer.remoteUrl) { - navigator.clipboard.writeText(streamingServer.remoteUrl); - toast.show({ - type: 'success', - title: t('SETTINGS_REMOTE_URL_COPIED'), - timeout: 2500, - }); - } - }, [streamingServer.remoteUrl]); - const sectionsContainerRef = React.useRef(null); - const generalSectionRef = React.useRef(null); - const playerSectionRef = React.useRef(null); - const streamingServerSectionRef = React.useRef(null); - const shortcutsSectionRef = React.useRef(null); - const sections = React.useMemo(() => ([ - { ref: generalSectionRef, id: GENERAL_SECTION }, - { ref: playerSectionRef, id: PLAYER_SECTION }, - { ref: streamingServerSectionRef, id: STREAMING_SECTION }, - { ref: shortcutsSectionRef, id: SHORTCUTS_SECTION }, - ]), []); - const [selectedSectionId, setSelectedSectionId] = React.useState(GENERAL_SECTION); - const updateSelectedSectionId = React.useCallback(() => { - if (sectionsContainerRef.current.scrollTop + sectionsContainerRef.current.clientHeight >= sectionsContainerRef.current.scrollHeight - 50) { - setSelectedSectionId(sections[sections.length - 1].id); - } else { - for (let i = sections.length - 1; i >= 0; i--) { - if (sections[i].ref.current.offsetTop - sectionsContainerRef.current.offsetTop <= sectionsContainerRef.current.scrollTop) { - setSelectedSectionId(sections[i].id); - break; - } - } - } - }, []); - const sideMenuButtonOnClick = React.useCallback((event) => { - const section = sections.find((section) => { - return section.id === event.currentTarget.dataset.section; - }); - sectionsContainerRef.current.scrollTo({ - top: section.ref.current.offsetTop - sectionsContainerRef.current.offsetTop, - behavior: 'smooth' - }); - }, []); - const sectionsContainerOnScroll = React.useCallback(throttle(() => { - updateSelectedSectionId(); - }, 50), []); - React.useEffect(() => { - if (isTraktAuthenticated && traktAuthStarted) { - core.transport.dispatch({ - action: 'Ctx', - args: { - action: 'InstallTraktAddon' - } - }); - setTraktAuthStarted(false); - } - }, [isTraktAuthenticated, traktAuthStarted]); - React.useEffect(() => { - if (dataExport.exportUrl !== null && typeof dataExport.exportUrl === 'string') { - platform.openExternal(dataExport.exportUrl); - } - }, [dataExport.exportUrl]); - React.useLayoutEffect(() => { - if (routeFocused) { - updateSelectedSectionId(); - } - }, [routeFocused]); - return ( - -
-
- - - - -
-
- App Version: {process.env.VERSION} -
-
- Build Version: {process.env.COMMIT_HASH} -
- { - streamingServer.settings !== null && streamingServer.settings.type === 'Ready' ? -
Server Version: {streamingServer.settings.content.serverVersion}
- : - null - } - { - typeof shell?.transport?.props?.shellVersion === 'string' ? -
Shell Version: {shell.transport.props.shellVersion}
- : - null - } -
-
-
-
-
-
-
-
-
- {profile.auth === null ? 'Anonymous user' : profile.auth.user.email} -
-
- { - profile.auth !== null ? - - : - null - } -
-
-
- { - profile.auth === null ? -
- -
- : - null - } -
-
-
- { - profile.auth ? - - : - null - } -
- { - profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user._id === 'string' ? -
- -
- : - null - } -
- -
-
- -
-
- -
-
- -
- { - profile.auth !== null && profile.auth.user !== null ? -
- -
- : - null - } - { - profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user.email === 'string' ? -
- -
- : - null - } -
-
- -
Trakt Scrobbling
-
- -
-
-
-
-
-
{ t('SETTINGS_UI_LANGUAGE') }
-
- -
- { - shell.active && -
-
-
{ t('SETTINGS_QUIT_ON_CLOSE') }
-
- -
- } - { - shell.active && -
-
-
{ t('SETTINGS_FULLSCREEN_EXIT') }
-
- -
- } -
-
-
{ t('SETTINGS_BLUR_UNWATCHED_IMAGE') }
-
- -
-
-
-
{ t('SETTINGS_NAV_PLAYER') }
-
- -
{t('SETTINGS_SECTION_SUBTITLES')}
-
-
-
-
{ t('SETTINGS_SUBTITLES_LANGUAGE') }
-
- -
-
-
-
{ t('SETTINGS_SUBTITLES_SIZE') }
-
- -
-
-
-
{ t('SETTINGS_SUBTITLES_COLOR') }
-
- -
-
-
-
{ t('SETTINGS_SUBTITLES_COLOR_BACKGROUND') }
-
- -
-
-
-
{ t('SETTINGS_SUBTITLES_COLOR_OUTLINE') }
-
- -
-
-
-
- -
{t('SETTINGS_SECTION_AUDIO')}
-
-
-
-
{ t('SETTINGS_DEFAULT_AUDIO_TRACK') }
-
- -
-
-
-
{ t('SETTINGS_SURROUND_SOUND') }
-
- -
-
-
-
- -
{t('SETTINGS_SECTION_CONTROLS')}
-
-
-
-
{ t('SETTINGS_SEEK_KEY') }
-
- -
-
-
-
{ t('SETTINGS_SEEK_KEY_SHIFT') }
-
- -
-
-
-
{ t('SETTINGS_PLAY_IN_BACKGROUND') }
-
- -
-
-
-
- -
{t('SETTINGS_SECTION_AUTO_PLAY')}
-
-
-
-
{ t('AUTO_PLAY') }
-
- -
-
-
-
{ t('SETTINGS_NEXT_VIDEO_POPUP_DURATION') }
-
- -
-
-
-
- -
{t('SETTINGS_SECTION_ADVANCED')}
-
-
-
-
{ t('SETTINGS_PLAY_IN_EXTERNAL_PLAYER') }
-
- -
- { - shell.active && -
-
-
{ t('SETTINGS_HWDEC') }
-
- -
- } - { - shell.active && -
-
-
{ t('SETTINGS_PAUSE_MINIMIZED') }
-
- -
- } -
-
-
{ t('SETTINGS_NAV_STREAMING') }
- - { - streamingServerRemoteUrlInput.value !== null ? -
-
-
{t('SETTINGS_REMOTE_URL')}
-
-
-
{streamingServerRemoteUrlInput.value}
- -
-
- : - null - } - { - profile.auth !== null && profile.auth.user !== null && remoteEndpointSelect !== null ? -
-
-
{ t('SETTINGS_HTTPS_ENDPOINT') }
-
- -
- : - null - } - { - cacheSizeSelect !== null ? -
-
-
{ t('SETTINGS_SERVER_CACHE_SIZE') }
-
- -
- : - null - } - { - torrentProfileSelect !== null ? -
-
-
{ t('SETTINGS_SERVER_TORRENT_PROFILE') }
-
- -
- : - null - } - { - transcodingProfileSelect !== null ? -
-
-
{ t('SETTINGS_TRANSCODE_PROFILE') }
-
- -
- : - null - } -
-
-
{ t('SETTINGS_NAV_SHORTCUTS') }
-
-
-
{ t('SETTINGS_SHORTCUT_PLAY_PAUSE') }
-
-
- { t('SETTINGS_SHORTCUT_SPACE') } -
-
-
-
-
{ t('SETTINGS_SHORTCUT_SEEK_FORWARD') }
-
-
- -
{ t('SETTINGS_SHORTCUT_OR') }
- ⇧ { t('SETTINGS_SHORTCUT_SHIFT') } -
+
- -
-
-
-
-
{ t('SETTINGS_SHORTCUT_SEEK_BACKWARD') }
-
-
- -
{ t('SETTINGS_SHORTCUT_OR') }
- ⇧ { t('SETTINGS_SHORTCUT_SHIFT') } -
+
- -
-
-
-
-
{ t('SETTINGS_SHORTCUT_VOLUME_UP') }
-
-
- -
-
-
-
-
{ t('SETTINGS_SHORTCUT_VOLUME_DOWN') }
-
-
- -
-
-
-
-
{ t('SETTINGS_SHORTCUT_MENU_SUBTITLES') }
-
-
- S -
-
-
-
-
{ t('SETTINGS_SHORTCUT_MENU_AUDIO') }
-
-
- A -
-
-
-
-
{ t('SETTINGS_SHORTCUT_MENU_INFO') }
-
-
- I -
-
-
-
-
{ t('SETTINGS_SHORTCUT_MENU_VIDEOS') }
-
-
- V -
-
-
-
-
{ t('SETTINGS_SHORTCUT_FULLSCREEN') }
-
-
- F -
-
-
-
-
{ t('SETTINGS_SHORTCUT_NAVIGATE_MENUS') }
-
-
- 1 -
{ t('SETTINGS_SHORTCUT_TO') }
- 6 -
-
-
-
-
{ t('SETTINGS_SHORTCUT_GO_TO_SEARCH') }
-
-
- 0 -
-
-
-
-
{ t('SETTINGS_SHORTCUT_EXIT_BACK') }
-
-
- { t('SETTINGS_SHORTCUT_ESC') } -
-
-
-
-
-
-
- App Version -
-
-
-
- {process.env.VERSION} -
-
-
-
-
-
- Build Version -
-
-
-
- {process.env.COMMIT_HASH} -
-
-
- { - streamingServer.settings !== null && streamingServer.settings.type === 'Ready' ? -
-
-
- Server Version -
-
-
-
- {streamingServer.settings.content.serverVersion} -
-
-
- : - null - } - { - typeof shell?.transport?.props?.shellVersion === 'string' ? -
-
-
- Shell Version -
-
-
-
- { shell.transport.props.shellVersion } -
-
-
- : - null - } -
-
-
- - ); -}; - -const SettingsFallback = () => ( - -); - -module.exports = withCoreSuspender(Settings, SettingsFallback); diff --git a/src/routes/Settings/Settings.less b/src/routes/Settings/Settings.less new file mode 100644 index 000000000..3e9d96758 --- /dev/null +++ b/src/routes/Settings/Settings.less @@ -0,0 +1,35 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +@import (reference) '~stremio/common/screen-sizes.less'; + +.settings-container { + height: calc(100% - var(--safe-area-inset-bottom)); + width: 100%; + background-color: transparent; + + .settings-content { + height: 100%; + width: 100%; + display: flex; + flex-direction: row; + + .sections-container { + flex: 1; + align-self: stretch; + padding: 0 3rem; + overflow-y: auto; + } + } +} + +@media only screen and (max-width: @minimum) { + .settings-container { + .settings-content { + flex-direction: column-reverse; + + .sections-container { + padding: 0 1.5rem; + } + } + } +} \ No newline at end of file diff --git a/src/routes/Settings/Settings.tsx b/src/routes/Settings/Settings.tsx new file mode 100644 index 000000000..b37d1f0c6 --- /dev/null +++ b/src/routes/Settings/Settings.tsx @@ -0,0 +1,109 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import classnames from 'classnames'; +import throttle from 'lodash.throttle'; +import { useRouteFocused } from 'stremio-router'; +import { useProfile, useStreamingServer, withCoreSuspender } from 'stremio/common'; +import { MainNavBars } from 'stremio/components'; +import { SECTIONS } from './constants'; +import Menu from './Menu'; +import General from './General'; +import Player from './Player'; +import Streaming from './Streaming'; +import Shortcuts from './Shortcuts'; +import Info from './Info'; +import styles from './Settings.less'; + +const Settings = () => { + const { routeFocused } = useRouteFocused(); + const profile = useProfile(); + const streamingServer = useStreamingServer(); + + const sectionsContainerRef = useRef(null); + const generalSectionRef = useRef(null); + const playerSectionRef = useRef(null); + const streamingServerSectionRef = useRef(null); + const shortcutsSectionRef = useRef(null); + + const sections = useMemo(() => ([ + { ref: generalSectionRef, id: SECTIONS.GENERAL }, + { ref: playerSectionRef, id: SECTIONS.PLAYER }, + { ref: streamingServerSectionRef, id: SECTIONS.STREAMING }, + { ref: shortcutsSectionRef, id: SECTIONS.SHORTCUTS }, + ]), []); + + const [selectedSectionId, setSelectedSectionId] = useState(SECTIONS.GENERAL); + + const updateSelectedSectionId = useCallback(() => { + const container = sectionsContainerRef.current; + if (container!.scrollTop + container!.clientHeight >= container!.scrollHeight - 50) { + setSelectedSectionId(sections[sections.length - 1].id); + } else { + for (let i = sections.length - 1; i >= 0; i--) { + if (sections[i].ref.current!.offsetTop - container!.offsetTop <= container!.scrollTop) { + setSelectedSectionId(sections[i].id); + break; + } + } + } + }, []); + + const onMenuSelect = useCallback((event: React.MouseEvent) => { + const section = sections.find((section) => { + return section.id === event.currentTarget.dataset.section; + }); + + const container = sectionsContainerRef.current; + section && container!.scrollTo({ + top: section.ref.current!.offsetTop - container!.offsetTop, + behavior: 'smooth' + }); + }, []); + + const onContainerScroll = useCallback(throttle(() => { + updateSelectedSectionId(); + }, 50), []); + + useLayoutEffect(() => { + if (routeFocused) { + updateSelectedSectionId(); + } + }, [routeFocused]); + + return ( + +
+ + +
+ + + + + +
+
+
+ ); +}; + +const SettingsFallback = () => ( + +); + +export default withCoreSuspender(Settings, SettingsFallback); diff --git a/src/routes/Settings/Shortcuts/Shortcuts.less b/src/routes/Settings/Shortcuts/Shortcuts.less new file mode 100644 index 000000000..40d97987d --- /dev/null +++ b/src/routes/Settings/Shortcuts/Shortcuts.less @@ -0,0 +1,27 @@ +.shortcut-container { + display: flex; + align-items: center; + justify-content: center; + padding: 0; + overflow: visible; + + kbd { + flex: 0 1 auto; + height: 2.5rem; + min-width: 2.5rem; + line-height: 2.5rem; + padding: 0 1rem; + font-weight: 500; + color: var(--primary-foreground-color); + border-radius: 0.25em; + box-shadow: 0 4px 0 1px var(--modal-background-color); + background-color: var(--overlay-color); + } + + .label { + flex: none; + margin: 0 1rem; + white-space: nowrap; + color: var(--primary-foreground-color); + } +} \ No newline at end of file diff --git a/src/routes/Settings/Shortcuts/Shortcuts.tsx b/src/routes/Settings/Shortcuts/Shortcuts.tsx new file mode 100644 index 000000000..be44a4a32 --- /dev/null +++ b/src/routes/Settings/Shortcuts/Shortcuts.tsx @@ -0,0 +1,88 @@ +import React, { forwardRef } from 'react'; +import { t } from 'i18next'; +import { Section, Option } from '../components'; +import styles from './Shortcuts.less'; + +const Shortcuts = forwardRef((_, ref) => { + return ( +
+ + + + + + + + + + + + + +
+ ); +}); + +export default Shortcuts; diff --git a/src/routes/Settings/Shortcuts/index.ts b/src/routes/Settings/Shortcuts/index.ts new file mode 100644 index 000000000..d9540bf83 --- /dev/null +++ b/src/routes/Settings/Shortcuts/index.ts @@ -0,0 +1,2 @@ +import Shortcuts from './Shortcuts'; +export default Shortcuts; diff --git a/src/routes/Settings/Streaming/Streaming.less b/src/routes/Settings/Streaming/Streaming.less new file mode 100644 index 000000000..5fc34df11 --- /dev/null +++ b/src/routes/Settings/Streaming/Streaming.less @@ -0,0 +1,44 @@ +:import('~stremio/routes/Settings/components/Option/Option.less') { + option-content: content; +} + +.configure-input-container { + .option-content { + display: flex; + align-items: center; + gap: 1rem; + overflow: hidden; + + .label { + flex: auto; + white-space: pre; + text-overflow: ellipsis; + color: var(--primary-foreground-color); + padding: 0 1rem; + } + + .configure-button-container { + flex: none; + width: 3rem; + height: 3rem; + border-radius: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--overlay-color); + + &:hover { + outline: var(--focus-outline-size) solid var(--primary-foreground-color); + background-color: transparent; + } + + .icon { + flex: none; + width: 1rem; + height: 1rem; + margin: 0; + color: var(--primary-foreground-color); + } + } + } +} \ No newline at end of file diff --git a/src/routes/Settings/Streaming/Streaming.tsx b/src/routes/Settings/Streaming/Streaming.tsx new file mode 100644 index 000000000..85bf4ddc9 --- /dev/null +++ b/src/routes/Settings/Streaming/Streaming.tsx @@ -0,0 +1,91 @@ +import React, { forwardRef, useCallback } from 'react'; +import { t } from 'i18next'; +import Icon from '@stremio/stremio-icons/react'; +import { Button, MultiselectMenu } from 'stremio/components'; +import { useToast } from 'stremio/common'; +import { Section, Option } from '../components'; +import URLsManager from './URLsManager'; +import useStreamingOptions from './useStreamingOptions'; +import styles from './Streaming.less'; + +type Props = { + profile: Profile, + streamingServer: StreamingServer, +}; + +const Streaming = forwardRef(({ profile, streamingServer }: Props, ref) => { + const toast = useToast(); + + const { + streamingServerRemoteUrlInput, + remoteEndpointSelect, + cacheSizeSelect, + torrentProfileSelect, + transcodingProfileSelect, + } = useStreamingOptions(streamingServer); + + const onCopyRemoteUrl = useCallback(() => { + if (streamingServer.remoteUrl) { + navigator.clipboard.writeText(streamingServer.remoteUrl); + + toast.show({ + type: 'success', + title: t('SETTINGS_REMOTE_URL_COPIED'), + timeout: 2500, + }); + } + }, [streamingServer.remoteUrl]); + + return ( +
+ + { + streamingServerRemoteUrlInput.value !== null && + + } + { + profile.auth !== null && profile.auth.user !== null && remoteEndpointSelect !== null && + + } + { + cacheSizeSelect !== null && + + } + { + torrentProfileSelect !== null && + + } + { + transcodingProfileSelect !== null && + + } +
+ ); +}); + +export default Streaming; diff --git a/src/routes/Settings/URLsManager/AddItem/AddItem.less b/src/routes/Settings/Streaming/URLsManager/AddItem/AddItem.less similarity index 100% rename from src/routes/Settings/URLsManager/AddItem/AddItem.less rename to src/routes/Settings/Streaming/URLsManager/AddItem/AddItem.less diff --git a/src/routes/Settings/URLsManager/AddItem/AddItem.tsx b/src/routes/Settings/Streaming/URLsManager/AddItem/AddItem.tsx similarity index 100% rename from src/routes/Settings/URLsManager/AddItem/AddItem.tsx rename to src/routes/Settings/Streaming/URLsManager/AddItem/AddItem.tsx diff --git a/src/routes/Settings/URLsManager/AddItem/index.ts b/src/routes/Settings/Streaming/URLsManager/AddItem/index.ts similarity index 100% rename from src/routes/Settings/URLsManager/AddItem/index.ts rename to src/routes/Settings/Streaming/URLsManager/AddItem/index.ts diff --git a/src/routes/Settings/URLsManager/Item/Item.less b/src/routes/Settings/Streaming/URLsManager/Item/Item.less similarity index 100% rename from src/routes/Settings/URLsManager/Item/Item.less rename to src/routes/Settings/Streaming/URLsManager/Item/Item.less diff --git a/src/routes/Settings/URLsManager/Item/Item.tsx b/src/routes/Settings/Streaming/URLsManager/Item/Item.tsx similarity index 100% rename from src/routes/Settings/URLsManager/Item/Item.tsx rename to src/routes/Settings/Streaming/URLsManager/Item/Item.tsx diff --git a/src/routes/Settings/URLsManager/Item/index.ts b/src/routes/Settings/Streaming/URLsManager/Item/index.ts similarity index 100% rename from src/routes/Settings/URLsManager/Item/index.ts rename to src/routes/Settings/Streaming/URLsManager/Item/index.ts diff --git a/src/routes/Settings/URLsManager/URLsManager.less b/src/routes/Settings/Streaming/URLsManager/URLsManager.less similarity index 98% rename from src/routes/Settings/URLsManager/URLsManager.less rename to src/routes/Settings/Streaming/URLsManager/URLsManager.less index fd0055d1c..6c9f03065 100644 --- a/src/routes/Settings/URLsManager/URLsManager.less +++ b/src/routes/Settings/Streaming/URLsManager/URLsManager.less @@ -1,6 +1,8 @@ // Copyright (C) 2017-2024 Smart code 203358507 .wrapper { + position: relative; + width: 100%; display: flex; flex-direction: column; max-width: 35rem; diff --git a/src/routes/Settings/URLsManager/URLsManager.tsx b/src/routes/Settings/Streaming/URLsManager/URLsManager.tsx similarity index 100% rename from src/routes/Settings/URLsManager/URLsManager.tsx rename to src/routes/Settings/Streaming/URLsManager/URLsManager.tsx diff --git a/src/routes/Settings/URLsManager/index.ts b/src/routes/Settings/Streaming/URLsManager/index.ts similarity index 100% rename from src/routes/Settings/URLsManager/index.ts rename to src/routes/Settings/Streaming/URLsManager/index.ts diff --git a/src/routes/Settings/URLsManager/useStreamingServerUrls.js b/src/routes/Settings/Streaming/URLsManager/useStreamingServerUrls.js similarity index 100% rename from src/routes/Settings/URLsManager/useStreamingServerUrls.js rename to src/routes/Settings/Streaming/URLsManager/useStreamingServerUrls.js diff --git a/src/routes/Settings/Streaming/index.ts b/src/routes/Settings/Streaming/index.ts new file mode 100644 index 000000000..00294377e --- /dev/null +++ b/src/routes/Settings/Streaming/index.ts @@ -0,0 +1,2 @@ +import Streaming from './Streaming'; +export default Streaming; diff --git a/src/routes/Settings/useStreamingServerSettingsInputs.js b/src/routes/Settings/Streaming/useStreamingOptions.ts similarity index 60% rename from src/routes/Settings/useStreamingServerSettingsInputs.js rename to src/routes/Settings/Streaming/useStreamingOptions.ts index e4bd7e79c..4d0975621 100644 --- a/src/routes/Settings/useStreamingServerSettingsInputs.js +++ b/src/routes/Settings/Streaming/useStreamingOptions.ts @@ -1,13 +1,13 @@ // Copyright (C) 2017-2023 Smart code 203358507 -const React = require('react'); -const { useTranslation } = require('react-i18next'); -const isEqual = require('lodash.isequal'); -const { useServices } = require('stremio/services'); +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import isEqual from 'lodash.isequal'; +import { useServices } from 'stremio/services'; const CACHE_SIZES = [0, 2147483648, 5368709120, 10737418240, null]; -const cacheSizeToString = (size) => { +const cacheSizeToString = (size: number | null) => { return size === null ? 'Infinite' : @@ -17,7 +17,16 @@ const cacheSizeToString = (size) => { `${Math.ceil(((size / 1024 / 1024 / 1024) + Number.EPSILON) * 100) / 100}GiB`; }; -const TORRENT_PROFILES = { +type TorrentProfile = { + btDownloadSpeedHardLimit: number, + btDownloadSpeedSoftLimit: number, + btHandshakeTimeout: number, + btMaxConnections: number, + btMinPeersForStable: number, + btRequestTimeout: number +}; + +const TORRENT_PROFILES: Record = { default: { btDownloadSpeedHardLimit: 3670016, btDownloadSpeedSoftLimit: 2621440, @@ -52,17 +61,32 @@ const TORRENT_PROFILES = { } }; -const useStreamingServerSettingsInputs = (streamingServer) => { +const useStreamingOptions = (streamingServer: StreamingServer) => { const { core } = useServices(); const { t } = useTranslation(); // TODO combine those useMemo in one - const streamingServerRemoteUrlInput = React.useMemo(() => ({ + const settings = useMemo(() => ( + streamingServer?.settings?.type === 'Ready' ? + streamingServer.settings.content as StreamingServerSettings : null + ), [streamingServer.settings]); + + const networkInfo = useMemo(() => ( + streamingServer?.networkInfo?.type === 'Ready' ? + streamingServer.networkInfo.content as NetworkInfo : null + ), [streamingServer.networkInfo]); + + const deviceInfo = useMemo(() => ( + streamingServer?.deviceInfo?.type === 'Ready' ? + streamingServer.deviceInfo.content as DeviceInfo : null + ), [streamingServer.deviceInfo]); + + const streamingServerRemoteUrlInput = useMemo(() => ({ value: streamingServer.remoteUrl, }), [streamingServer.remoteUrl]); - const remoteEndpointSelect = React.useMemo(() => { - if (streamingServer.settings?.type !== 'Ready' || streamingServer.networkInfo?.type !== 'Ready') { + const remoteEndpointSelect = useMemo(() => { + if (!settings || !networkInfo) { return null; } @@ -72,29 +96,29 @@ const useStreamingServerSettingsInputs = (streamingServer) => { label: t('SETTINGS_DISABLED'), value: '', }, - ...streamingServer.networkInfo.content.availableInterfaces.map((address) => ({ + ...networkInfo.availableInterfaces.map((address) => ({ label: address, value: address, })) ], - value: streamingServer.settings.content.remoteHttps, - onSelect: (value) => { + value: settings.remoteHttps, + onSelect: (value: string | null) => { core.transport.dispatch({ action: 'StreamingServer', args: { action: 'UpdateSettings', args: { - ...streamingServer.settings.content, + ...settings, remoteHttps: value, } } }); } }; - }, [streamingServer.settings, streamingServer.networkInfo]); + }, [settings, networkInfo]); - const cacheSizeSelect = React.useMemo(() => { - if (streamingServer.settings === null || streamingServer.settings.type !== 'Ready') { + const cacheSizeSelect = useMemo(() => { + if (!settings) { return null; } @@ -103,36 +127,37 @@ const useStreamingServerSettingsInputs = (streamingServer) => { label: cacheSizeToString(size), value: JSON.stringify(size) })), - value: JSON.stringify(streamingServer.settings.content.cacheSize), + value: JSON.stringify(settings.cacheSize), title: () => { - return cacheSizeToString(streamingServer.settings.content.cacheSize); + return cacheSizeToString(settings.cacheSize); }, - onSelect: (value) => { + onSelect: (value: any) => { core.transport.dispatch({ action: 'StreamingServer', args: { action: 'UpdateSettings', args: { - ...streamingServer.settings.content, + ...settings, cacheSize: JSON.parse(value), } } }); } }; - }, [streamingServer.settings]); - const torrentProfileSelect = React.useMemo(() => { - if (streamingServer.settings === null || streamingServer.settings.type !== 'Ready') { + }, [settings]); + + const torrentProfileSelect = useMemo(() => { + if (!settings) { return null; } const selectedTorrentProfile = { - btDownloadSpeedHardLimit: streamingServer.settings.content.btDownloadSpeedHardLimit, - btDownloadSpeedSoftLimit: streamingServer.settings.content.btDownloadSpeedSoftLimit, - btHandshakeTimeout: streamingServer.settings.content.btHandshakeTimeout, - btMaxConnections: streamingServer.settings.content.btMaxConnections, - btMinPeersForStable: streamingServer.settings.content.btMinPeersForStable, - btRequestTimeout: streamingServer.settings.content.btRequestTimeout + btDownloadSpeedHardLimit: settings.btDownloadSpeedHardLimit, + btDownloadSpeedSoftLimit: settings.btDownloadSpeedSoftLimit, + btHandshakeTimeout: settings.btHandshakeTimeout, + btMaxConnections: settings.btMaxConnections, + btMinPeersForStable: settings.btMinPeersForStable, + btRequestTimeout: settings.btRequestTimeout }; const isCustomTorrentProfileSelected = Object.values(TORRENT_PROFILES).every((torrentProfile) => { return !isEqual(torrentProfile, selectedTorrentProfile); @@ -153,22 +178,23 @@ const useStreamingServerSettingsInputs = (streamingServer) => { [] ), value: JSON.stringify(selectedTorrentProfile), - onSelect: (value) => { + onSelect: (value: any) => { core.transport.dispatch({ action: 'StreamingServer', args: { action: 'UpdateSettings', args: { - ...streamingServer.settings.content, + ...settings, ...JSON.parse(value), } } }); } }; - }, [streamingServer.settings]); - const transcodingProfileSelect = React.useMemo(() => { - if (streamingServer.settings?.type !== 'Ready' || streamingServer.deviceInfo?.type !== 'Ready') { + }, [settings]); + + const transcodingProfileSelect = useMemo(() => { + if (!settings || !deviceInfo) { return null; } @@ -178,27 +204,34 @@ const useStreamingServerSettingsInputs = (streamingServer) => { label: t('SETTINGS_DISABLED'), value: null, }, - ...streamingServer.deviceInfo.content.availableHardwareAccelerations.map((name) => ({ + ...deviceInfo.availableHardwareAccelerations.map((name) => ({ label: name, value: name, })) ], - value: streamingServer.settings.content.transcodeProfile, - onSelect: (value) => { + value: settings.transcodeProfile, + onSelect: (value: string | null) => { core.transport.dispatch({ action: 'StreamingServer', args: { action: 'UpdateSettings', args: { - ...streamingServer.settings.content, + ...settings, transcodeProfile: value, } } }); } }; - }, [streamingServer.settings, streamingServer.deviceInfo]); - return { streamingServerRemoteUrlInput, remoteEndpointSelect, cacheSizeSelect, torrentProfileSelect, transcodingProfileSelect }; + }, [settings, deviceInfo]); + + return { + streamingServerRemoteUrlInput, + remoteEndpointSelect, + cacheSizeSelect, + torrentProfileSelect, + transcodingProfileSelect, + }; }; -module.exports = useStreamingServerSettingsInputs; +export default useStreamingOptions; diff --git a/src/routes/Settings/components/Category/Category.less b/src/routes/Settings/components/Category/Category.less new file mode 100644 index 000000000..23e0ce670 --- /dev/null +++ b/src/routes/Settings/components/Category/Category.less @@ -0,0 +1,37 @@ +.category { + position: relative; + width: 100%; + display: flex; + flex-direction: column; + align-items: start; + margin-bottom: 1rem; + padding-bottom: 1rem; + overflow: visible; + + &:not(:last-child) { + border-bottom: thin solid var(--overlay-color); + } + + .heading { + position: relative; + height: 4rem; + display: flex; + flex-direction: row; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; + + .label { + flex: none; + font-size: 1.1rem; + color: var(--primary-foreground-color); + } + + .icon { + flex: none; + width: 2rem; + height: 2rem; + color: var(--primary-foreground-color); + } + } +} \ No newline at end of file diff --git a/src/routes/Settings/components/Category/Category.tsx b/src/routes/Settings/components/Category/Category.tsx new file mode 100644 index 000000000..75d39950d --- /dev/null +++ b/src/routes/Settings/components/Category/Category.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { t } from 'i18next'; +import Icon from '@stremio/stremio-icons/react'; +import styles from './Category.less'; + +type Props = { + icon: string, + label: string, + children: React.ReactNode, +}; + +const Category = ({ icon, label, children }: Props) => { + return ( +
+
+ +
+ {t(label)} +
+
+ {children} +
+ ); +}; + +export default Category; diff --git a/src/routes/Settings/components/Category/index.ts b/src/routes/Settings/components/Category/index.ts new file mode 100644 index 000000000..9e9778dc3 --- /dev/null +++ b/src/routes/Settings/components/Category/index.ts @@ -0,0 +1,2 @@ +import Category from './Category'; +export default Category; diff --git a/src/routes/Settings/components/Link/Link.less b/src/routes/Settings/components/Link/Link.less new file mode 100644 index 000000000..ba12d94e9 --- /dev/null +++ b/src/routes/Settings/components/Link/Link.less @@ -0,0 +1,16 @@ +.link { + position: relative; + display: flex; + align-items: center; + height: 2rem; + + .label { + color: var(--primary-accent-color); + } + + &:hover { + .label { + text-decoration: underline; + } + } +} \ No newline at end of file diff --git a/src/routes/Settings/components/Link/Link.tsx b/src/routes/Settings/components/Link/Link.tsx new file mode 100644 index 000000000..e0216c92b --- /dev/null +++ b/src/routes/Settings/components/Link/Link.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Button } from 'stremio/components'; +import styles from './Link.less'; + +type Props = { + label: string, + href?: string, + target?: string, + onClick?: () => void, +}; + +const Link = ({ label, href, target, onClick }: Props) => { + return ( + + ); +}; + +export default Link; diff --git a/src/routes/Settings/components/Link/index.ts b/src/routes/Settings/components/Link/index.ts new file mode 100644 index 000000000..a575fb00f --- /dev/null +++ b/src/routes/Settings/components/Link/index.ts @@ -0,0 +1,2 @@ +import Link from './Link'; +export default Link; diff --git a/src/routes/Settings/components/Option/Option.less b/src/routes/Settings/components/Option/Option.less new file mode 100644 index 000000000..181cfa33b --- /dev/null +++ b/src/routes/Settings/components/Option/Option.less @@ -0,0 +1,78 @@ +.option { + position: relative; + width: 100%; + flex: none; + display: flex; + flex-direction: row; + align-items: center; + gap: 2rem; + margin-bottom: 2rem; + overflow: visible; + + .heading, .content { + flex: 1 1 50%; + position: relative; + display: flex; + flex-direction: row; + align-items: center; + } + + .heading { + display: flex; + gap: 0.75rem; + + .icon { + width: 3rem; + height: 3rem; + color: var(--primary-foreground-color); + } + + .label { + line-height: 1.5rem; + white-space: nowrap; + text-overflow: ellipsis; + color: var(--primary-foreground-color); + } + } + + .content { + justify-content: center; + overflow: visible; + + :global(.multiselect) { + width: 100%; + padding: 0; + background: var(--overlay-color); + } + + :global(.button) { + display: flex; + align-items: center; + justify-content: center; + height: 3.5rem; + width: 100%; + padding: 0 2rem; + border-radius: 3.5rem; + font-weight: 500; + color: var(--primary-foreground-color); + background-color: var(--overlay-color); + + &:hover { + outline: var(--focus-outline-size) solid var(--primary-foreground-color); + background-color: transparent; + } + } + + :global(.color-input) { + width: 100%; + padding: 1.3rem 1rem; + border-radius: 3rem; + border: 2px solid transparent; + transition: 0.3s all ease-in-out; + + &:hover { + border-color: var(--overlay-color); + } + } + } +} \ No newline at end of file diff --git a/src/routes/Settings/components/Option/Option.tsx b/src/routes/Settings/components/Option/Option.tsx new file mode 100644 index 000000000..0ff25f31e --- /dev/null +++ b/src/routes/Settings/components/Option/Option.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import classNames from 'classnames'; +import { t } from 'i18next'; +import styles from './Option.less'; +import Icon from '@stremio/stremio-icons/react'; + +type Props = { + className?: string, + icon?: string, + label: string, + children: React.ReactNode, +}; + +const Option = ({ className, icon, label, children }: Props) => { + return ( +
+
+ { + icon && + + } +
+ {t(label)} +
+
+
+ { children } +
+
+ ); +}; + +export default Option; diff --git a/src/routes/Settings/components/Option/index.ts b/src/routes/Settings/components/Option/index.ts new file mode 100644 index 000000000..2d1893d7b --- /dev/null +++ b/src/routes/Settings/components/Option/index.ts @@ -0,0 +1,2 @@ +import Option from './Option'; +export default Option; diff --git a/src/routes/Settings/components/Section/Section.less b/src/routes/Settings/components/Section/Section.less new file mode 100644 index 000000000..b4de116af --- /dev/null +++ b/src/routes/Settings/components/Section/Section.less @@ -0,0 +1,22 @@ +.section { + position: relative; + max-width: 35rem; + display: flex; + flex-direction: column; + align-items: start; + padding: 3rem 0; + overflow: visible; + + &:not(:last-child) { + border-bottom: thin solid var(--overlay-color); + } + + .label { + flex: none; + align-self: stretch; + font-size: 1.8rem; + line-height: 3.4rem; + margin-bottom: 2rem; + color: var(--primary-foreground-color); + } +} \ No newline at end of file diff --git a/src/routes/Settings/components/Section/Section.tsx b/src/routes/Settings/components/Section/Section.tsx new file mode 100644 index 000000000..47e10240a --- /dev/null +++ b/src/routes/Settings/components/Section/Section.tsx @@ -0,0 +1,26 @@ +import React, { forwardRef } from 'react'; +import classNames from 'classnames'; +import { t } from 'i18next'; +import styles from './Section.less'; + +type Props = { + className?: string, + label?: string, + children: React.ReactNode, +}; + +const Section = forwardRef(({ className, label, children }: Props, ref) => { + return ( +
+ { + label && +
+ {t(label)} +
+ } + { children } +
+ ); +}); + +export default Section; diff --git a/src/routes/Settings/components/Section/index.ts b/src/routes/Settings/components/Section/index.ts new file mode 100644 index 000000000..14170cb7b --- /dev/null +++ b/src/routes/Settings/components/Section/index.ts @@ -0,0 +1,2 @@ +import Section from './Section'; +export default Section; diff --git a/src/routes/Settings/components/index.ts b/src/routes/Settings/components/index.ts new file mode 100644 index 000000000..605ea4d24 --- /dev/null +++ b/src/routes/Settings/components/index.ts @@ -0,0 +1,11 @@ +import Category from './Category'; +import Link from './Link'; +import Option from './Option'; +import Section from './Section'; + +export { + Category, + Link, + Option, + Section, +}; diff --git a/src/routes/Settings/constants.ts b/src/routes/Settings/constants.ts new file mode 100644 index 000000000..001f3e3e3 --- /dev/null +++ b/src/routes/Settings/constants.ts @@ -0,0 +1,10 @@ +const SECTIONS = { + GENERAL: 'general', + PLAYER: 'player', + STREAMING: 'streaming', + SHORTCUTS: 'shortcuts', +}; + +export { + SECTIONS, +}; diff --git a/src/routes/Settings/index.js b/src/routes/Settings/index.js deleted file mode 100644 index b426b8b91..000000000 --- a/src/routes/Settings/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const Settings = require('./Settings'); - -module.exports = Settings; diff --git a/src/routes/Settings/index.ts b/src/routes/Settings/index.ts new file mode 100644 index 000000000..d8c25945a --- /dev/null +++ b/src/routes/Settings/index.ts @@ -0,0 +1,4 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +import Settings from './Settings'; +export default Settings; diff --git a/src/routes/Settings/styles.less b/src/routes/Settings/styles.less deleted file mode 100644 index 2fc2bd893..000000000 --- a/src/routes/Settings/styles.less +++ /dev/null @@ -1,466 +0,0 @@ -// Copyright (C) 2017-2024 Smart code 203358507 - -@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; -@import (reference) '~stremio/common/screen-sizes.less'; - -:import('~stremio/components/Toggle/styles.less') { - checkbox-icon: icon; -} - -:import('~stremio/components/Multiselect/styles.less') { - multiselect-menu-container: menu-container; - multiselect-label: label; -} - -.settings-container { - height: calc(100% - var(--safe-area-inset-bottom)); - width: 100%; - background-color: transparent; - - .settings-content { - height: 100%; - width: 100%; - display: flex; - flex-direction: row; - - .side-menu-container { - flex: none; - align-self: stretch; - display: flex; - flex-direction: column; - width: 18rem; - padding: 3rem 1.5rem; - - .side-menu-button { - flex: none; - align-self: stretch; - display: flex; - align-items: center; - height: 4rem; - border-radius: 4rem; - padding: 2rem; - margin-bottom: 0.5rem; - font-size: 1.1rem; - font-weight: 500; - color: var(--primary-foreground-color); - opacity: 0.4; - - &.selected { - font-weight: 600; - color: var(--primary-foreground-color); - background-color: var(--overlay-color); - opacity: 1; - } - - &:hover { - background-color: var(--overlay-color); - } - } - - .spacing { - flex: 1; - } - - .version-info-label { - flex: 0 1 auto; - margin: 0.5rem 0; - white-space: nowrap; - text-overflow: ellipsis; - color: var(--primary-foreground-color); - opacity: 0.3; - overflow: hidden; - } - } - - .sections-container { - flex: 1; - align-self: stretch; - padding: 0 3rem; - overflow-y: auto; - - .section-container { - display: flex; - flex-direction: column; - padding: 3rem 0; - overflow: visible; - - &:not(:last-child) { - border-bottom: thin solid var(--overlay-color); - } - - .section-title { - flex: none; - align-self: stretch; - font-size: 1.8rem; - line-height: 3.4rem; - margin-bottom: 3rem; - color: var(--primary-foreground-color); - } - - .section-category-container { - display: flex; - flex-direction: row; - align-items: center; - gap: 0 1em; - margin-bottom: 1.5rem; - line-height: 2.4rem; - - .label { - flex: none; - font-size: 1.1rem; - color: var(--primary-foreground-color); - } - - .icon { - flex: none; - width: 2rem; - height: 2rem; - color: var(--primary-foreground-color); - } - } - - .option-container { - flex: none; - align-self: stretch; - display: flex; - flex-direction: row; - align-items: center; - max-width: 35rem; - margin-bottom: 2rem; - overflow: visible; - - &.link-container { - margin-bottom: 0.5rem; - } - - &:last-child { - margin-bottom: 0; - } - - &.user-info-option-container { - gap: 1rem; - - .user-info-content { - flex: 1; - display: flex; - flex-direction: row; - align-items: center; - - .avatar-container { - flex: none; - align-self: stretch; - height: 5rem; - width: 5rem; - margin-right: 1rem; - border: 2px solid var(--primary-accent-color); - border-radius: 50%; - background-size: cover; - background-repeat: no-repeat; - background-position: center; - background-origin: content-box; - background-clip: content-box; - opacity: 0.9; - background-color: var(--primary-foreground-color); - } - - .email-logout-container { - flex: none; - display: flex; - flex-direction: column; - - .email-label-container, .logout-button-container { - display: flex; - flex-direction: row; - align-items: center; - } - - .email-label-container { - .email-label { - flex: 1; - font-size: 1.1rem; - color: var(--primary-foreground-color); - opacity: 0.7; - } - } - - .logout-button-container { - &:hover, &:focus { - outline: none; - - .logout-label { - text-decoration: underline; - } - } - - .logout-label { - flex: 1; - color: var(--primary-accent-color); - } - } - } - } - - .user-panel-container { - flex: none; - display: flex; - flex-direction: row; - align-items: center; - width: 10rem; - height: 3.5rem; - border-radius: 3.5rem; - background-color: var(--overlay-color); - - &:hover { - outline: var(--focus-outline-size) solid var(--primary-foreground-color); - background-color: transparent; - } - - .user-panel-label { - flex: 1; - max-height: 2.4em; - padding: 0 0.5rem; - font-weight: 500; - text-align: center; - color: var(--primary-foreground-color); - } - } - } - - .option-name-container, .option-input-container { - flex: 1 1 50%; - display: flex; - flex-direction: row; - align-items: center; - - .icon { - flex: none; - width: 1.5rem; - height: 1.5rem; - margin-right: 0.5rem; - color: var(--primary-foreground-color); - } - - .label { - flex-grow: 0; - flex-shrink: 1; - flex-basis: auto; - line-height: 1.5rem; - white-space: nowrap; - text-overflow: ellipsis; - color: var(--primary-foreground-color); - } - - &.trakt-icon { - .icon { - width: 3rem; - height: 3rem; - color: var(--color-trakt); - } - } - } - - .option-name-container { - justify-content: flex-start; - padding: 1rem 1rem 1rem 0; - margin-right: 2rem; - } - - .option-input-container { - padding: 1rem 1.5rem; - - &.multiselect-container { - padding: 0; - background: var(--overlay-color); - } - - &.button-container { - justify-content: center; - height: 3.5rem; - border-radius: 3.5rem; - background-color: var(--overlay-color); - - &:hover { - outline: var(--focus-outline-size) solid var(--primary-foreground-color); - background-color: transparent; - } - - .label { - font-weight: 500; - } - } - - &.multiselect-container { - >.multiselect-label { - line-height: 1.5rem; - max-height: 1.5rem; - } - - .multiselect-menu-container { - overflow: auto; - } - } - - &.link-input-container { - flex: 0 1 auto; - padding: 0; - - .label { - color: var(--primary-accent-color); - } - - &:hover { - .label { - text-decoration: underline; - } - } - } - - &.checkbox-container { - justify-content: center; - - .checkbox-icon { - width: 1.5rem; - height: 1.5rem; - } - } - - &.color-input-container { - padding: 1.3rem 1rem; - border-radius: 3rem; - border: 2px solid transparent; - transition: 0.3s all ease-in-out; - - &:hover { - border-color: var(--overlay-color); - } - } - - &.info-container { - justify-content: center; - - &.selectable { - user-select: text; - - .label { - user-select: text; - } - } - } - - &.configure-input-container { - padding: 0; - - .label { - flex-grow: 1; - white-space: pre; - text-overflow: ellipsis; - padding: 0 1rem; - } - - .configure-button-container { - flex: none; - width: 3rem; - height: 3rem; - border-radius: 100%; - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - background-color: var(--overlay-color); - - &:hover { - outline: var(--focus-outline-size) solid var(--primary-foreground-color); - background-color: transparent; - } - - .icon { - flex: none; - width: 1rem; - height: 1rem; - margin: 0; - color: var(--primary-foreground-color); - } - } - } - - &.shortcut-container { - justify-content: center; - padding: 0; - overflow: visible; - - kbd { - flex: 0 1 auto; - height: 2.5rem; - min-width: 2.5rem; - line-height: 2.5rem; - padding: 0 1rem; - font-weight: 500; - color: var(--primary-foreground-color); - border-radius: 0.25em; - box-shadow: 0 4px 0 1px var(--modal-background-color); - background-color: var(--overlay-color); - } - - .label { - margin: 0 1rem; - white-space: nowrap; - color: var(--primary-foreground-color); - } - } - } - } - } - - .versions-section-container { - display: none; - } - } - } -} - -@media only screen and (max-width: @xsmall) { - .settings-container { - .settings-content { - .side-menu-container { - display: none; - } - - .sections-container { - .versions-section-container { - display: flex; - } - } - } - } -} - -@media only screen and (max-width: @minimum) { - .settings-container { - .settings-content { - flex-direction: column-reverse; - - .side-menu-container { - display: none; - } - - .sections-container { - padding: 0 1.5rem; - - .section-container { - .user-info-option-container { - flex-direction: column; - align-items: flex-start; - - .user-panel-container { - width: 100% !important; - } - } - } - - .versions-section-container { - display: flex; - } - } - } - } -} \ No newline at end of file diff --git a/src/routes/index.js b/src/routes/index.js index 076a2213d..7d921a187 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -8,7 +8,7 @@ const Calendar = require('./Calendar').default; const MetaDetails = require('./MetaDetails'); const NotFound = require('./NotFound'); const Search = require('./Search'); -const Settings = require('./Settings'); +const { default: Settings } = require('./Settings'); const Player = require('./Player'); const Intro = require('./Intro'); diff --git a/src/services/Shell/Shell.d.ts b/src/services/Shell/Shell.d.ts new file mode 100644 index 000000000..b1bcca069 --- /dev/null +++ b/src/services/Shell/Shell.d.ts @@ -0,0 +1,11 @@ +type ShellTransportProps = { + shellVersion: string, +}; + +type ShellTransport = { + props: ShellTransportProps, +}; + +interface ShellService { + transport: ShellTransport, +} diff --git a/src/types/models/Ctx.d.ts b/src/types/models/Ctx.d.ts index e649b305b..11b6b7f3b 100644 --- a/src/types/models/Ctx.d.ts +++ b/src/types/models/Ctx.d.ts @@ -21,6 +21,7 @@ type Settings = { hardwareDecoding: boolean, escExitFullscreen: boolean, interfaceLanguage: string, + quitOnClose: boolean, hideSpoilers: boolean, nextVideoNotificationDuration: number, playInBackground: boolean, @@ -41,6 +42,7 @@ type Settings = { subtitlesSize: number, subtitlesTextColor: string, surroundSound: boolean, + pauseOnMinimize: boolean, }; type Profile = { diff --git a/src/types/models/DataExport.d.ts b/src/types/models/DataExport.d.ts new file mode 100644 index 000000000..bd7c7556a --- /dev/null +++ b/src/types/models/DataExport.d.ts @@ -0,0 +1,3 @@ +type DataExport = { + exportUrl: string | null, +}; diff --git a/src/types/models/StremingServer.d.ts b/src/types/models/StremingServer.d.ts index d344755d6..6e6f96f39 100644 --- a/src/types/models/StremingServer.d.ts +++ b/src/types/models/StremingServer.d.ts @@ -23,6 +23,8 @@ type StreamingServerSettings = { cacheRoot: string, cacheSize: number, serverVersion: string, + remoteHttps: string | null, + transcodeProfile: string | null, }; type SFile = { @@ -93,6 +95,14 @@ type Statistics = { swarmSize: number, }; +type NetworkInfo = { + availableInterfaces: string[], +}; + +type DeviceInfo = { + availableHardwareAccelerations: string[], +}; + type PlaybackDevice = { id: string, name: string, @@ -115,4 +125,6 @@ type StreamingServer = { torrent: [string, Loadable] | null, statistics: Loadable | null, playbackDevices: Loadable | null, + networkInfo: Loadable | null, + deviceInfo: Loadable | null, }; From cb405878a0ea0862f834520ddb0be31feb7a1028 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 18 Jun 2025 09:59:54 +0200 Subject: [PATCH 33/39] chore: update stremio-translations --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 79e66d642..0584eb3d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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#a6be0425573917c2e82b66d28968c1a4d444cb96", + "stremio-translations": "github:Stremio/stremio-translations#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f", "url": "0.11.4", "use-long-press": "^3.2.0" }, @@ -13373,9 +13373,9 @@ } }, "node_modules/stremio-translations": { - "version": "1.44.10", - "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#a6be0425573917c2e82b66d28968c1a4d444cb96", - "integrity": "sha512-77kVE/eos/SA16kzeK7TTWmqoLF0mLPCJXjITwVIVzMHr8XyBPZFOfmiVEg4M6W1W7qYqA+dHhzicyLs7hJhlw==", + "version": "1.44.12", + "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f", + "integrity": "sha512-doFloPfrWQ5EqufZMCTMPlvR5BBI/B7S//UD0G0Fr5NHRdIwKXB4Hf47y+adUTjn3oAGrepVfMpQVaIp5BIrKg==", "license": "MIT" }, "node_modules/string_decoder": { diff --git a/package.json b/package.json index 5ed9368ab..36e556149 100644 --- a/package.json +++ b/package.json @@ -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#a6be0425573917c2e82b66d28968c1a4d444cb96", + "stremio-translations": "github:Stremio/stremio-translations#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f", "url": "0.11.4", "use-long-press": "^3.2.0" }, From 1789ddd06ab41065ca8cb595ad1fd5194e98bab1 Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 18 Jun 2025 11:36:34 +0300 Subject: [PATCH 34/39] fix: translations commit hash --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 854f2b0b8..930645829 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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#1f0b73ff5605b1afde18636f82b8fd45d299a426", + "stremio-translations": "github:Stremio/stremio-translations#37123fdc01f1e0c8a29d6abf374292fa3bd7f4ad", "url": "0.11.4", "use-long-press": "^3.2.0" }, @@ -13405,8 +13405,8 @@ }, "node_modules/stremio-translations": { "version": "1.44.12", - "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#1f0b73ff5605b1afde18636f82b8fd45d299a426", - "integrity": "sha512-1jvv4VRWNiFl+GivV6fXpq9QSIpElMUzGMG3jFxkJqvvZeYZpUsK/D4Bd9lR0jBSPYhwpLMZgFoGHWx3ABg+Ig==", + "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#37123fdc01f1e0c8a29d6abf374292fa3bd7f4ad", + "integrity": "sha512-rzA0kf38VVcFW8fOesiGML+QFNsoyfD97M4rV8gKe8Kra/EtJsI2+4Yx90EDDIF++WeXqRzl2RyrZSuKJFBSYA==", "license": "MIT" }, "node_modules/string_decoder": { diff --git a/package.json b/package.json index d1428b7a3..48825b01a 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,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#1f0b73ff5605b1afde18636f82b8fd45d299a426", + "stremio-translations": "github:Stremio/stremio-translations#37123fdc01f1e0c8a29d6abf374292fa3bd7f4ad", "url": "0.11.4", "use-long-press": "^3.2.0" }, From 48a6da5ca73d6d5e95d9af30823c3c7bba6b6aa2 Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 18 Jun 2025 11:41:43 +0300 Subject: [PATCH 35/39] fix: translations commit latest hash --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 930645829..6d52e8dcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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#37123fdc01f1e0c8a29d6abf374292fa3bd7f4ad", + "stremio-translations": "github:Stremio/stremio-translations#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f", "url": "0.11.4", "use-long-press": "^3.2.0" }, @@ -13405,8 +13405,8 @@ }, "node_modules/stremio-translations": { "version": "1.44.12", - "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#37123fdc01f1e0c8a29d6abf374292fa3bd7f4ad", - "integrity": "sha512-rzA0kf38VVcFW8fOesiGML+QFNsoyfD97M4rV8gKe8Kra/EtJsI2+4Yx90EDDIF++WeXqRzl2RyrZSuKJFBSYA==", + "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f", + "integrity": "sha512-b38OjGwlsvFm/aNn/ia18mPxPjZvnI/GaToppn1XaQqCuZuSHxQlYDddwOYTztskWo4VO/IZmCi3UFewqpsqCQ==", "license": "MIT" }, "node_modules/string_decoder": { diff --git a/package.json b/package.json index 48825b01a..51bd9036c 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,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#37123fdc01f1e0c8a29d6abf374292fa3bd7f4ad", + "stremio-translations": "github:Stremio/stremio-translations#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f", "url": "0.11.4", "use-long-press": "^3.2.0" }, From cf654ae825d258c2c9817a501efc42acfcf1e526 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 18 Jun 2025 23:28:57 +0200 Subject: [PATCH 36/39] chore: update stremio-translations --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index a7a258109..d7d3c7d4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f", + "stremio-translations": "github:Stremio/stremio-translations#8212fa77c4febd22ddb611590e9fb574dc845416", "url": "0.11.4", "use-long-press": "^3.2.0" }, @@ -13405,8 +13405,8 @@ }, "node_modules/stremio-translations": { "version": "1.44.12", - "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f", - "integrity": "sha512-doFloPfrWQ5EqufZMCTMPlvR5BBI/B7S//UD0G0Fr5NHRdIwKXB4Hf47y+adUTjn3oAGrepVfMpQVaIp5BIrKg==", + "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#8212fa77c4febd22ddb611590e9fb574dc845416", + "integrity": "sha512-5DladLUsghLlVRsZh2bBnb7UMqU8NEYMHc+YbzBvb1llgMk9elXFSHtAjInepZlC5zWx2pJYOQ8lQzzqogQdFw==", "license": "MIT" }, "node_modules/string_decoder": { diff --git a/package.json b/package.json index 51bd9036c..77f7477b7 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,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#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f", + "stremio-translations": "github:Stremio/stremio-translations#8212fa77c4febd22ddb611590e9fb574dc845416", "url": "0.11.4", "use-long-press": "^3.2.0" }, From 00bac0aca21c072114a5abe768a50d7e3e78d348 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 18 Jun 2025 23:30:10 +0200 Subject: [PATCH 37/39] refactor(Settings): add translation for shortcuts and --- src/routes/Settings/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 552b6116c..d79bcebeb 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -715,7 +715,7 @@ const Settings = () => {
G -
and
+
{ t('SETTINGS_SHORTCUT_AND') }
H
From 3e91f55d227d46285f3accbce37bda1b3c7428b6 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 19 Jun 2025 03:05:32 +0200 Subject: [PATCH 38/39] fix(MultiselectMenu): support disabled prop --- src/components/MultiselectMenu/MultiselectMenu.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/MultiselectMenu/MultiselectMenu.tsx b/src/components/MultiselectMenu/MultiselectMenu.tsx index 3251f01fa..eb288a12b 100644 --- a/src/components/MultiselectMenu/MultiselectMenu.tsx +++ b/src/components/MultiselectMenu/MultiselectMenu.tsx @@ -18,7 +18,7 @@ type Props = { onSelect: (value: any) => void; }; -const MultiselectMenu = ({ className, title, options, value, onSelect }: Props) => { +const MultiselectMenu = ({ className, title, options, value, disabled, onSelect }: Props) => { const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); const multiselectMenuRef = useOutsideClick(() => closeMenu()); const [level, setLevel] = React.useState(0); @@ -33,6 +33,7 @@ const MultiselectMenu = ({ className, title, options, value, onSelect }: Props)