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: |
diff --git a/package-lock.json b/package-lock.json
index 79e66d642..39a17e5aa 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#8212fa77c4febd22ddb611590e9fb574dc845416",
"url": "0.11.4",
"use-long-press": "^3.2.0"
},
@@ -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",
@@ -66,6 +68,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",
@@ -3806,6 +3809,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",
@@ -4786,6 +4816,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 +12540,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",
@@ -13373,9 +13433,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#8212fa77c4febd22ddb611590e9fb574dc845416",
+ "integrity": "sha512-5DladLUsghLlVRsZh2bBnb7UMqU8NEYMHc+YbzBvb1llgMk9elXFSHtAjInepZlC5zWx2pJYOQ8lQzzqogQdFw==",
"license": "MIT"
},
"node_modules/string_decoder": {
@@ -13976,6 +14036,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..9599f3587 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": "npx jest ./tests/i18nScan.test.js"
},
"dependencies": {
"@babel/runtime": "7.26.0",
@@ -40,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#8212fa77c4febd22ddb611590e9fb574dc845416",
"url": "0.11.4",
"use-long-press": "^3.2.0"
},
@@ -52,6 +53,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",
@@ -70,6 +73,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/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/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/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/components/AddonDetailsModal/AddonDetails/AddonDetails.js b/src/components/AddonDetailsModal/AddonDetails/AddonDetails.js
index da5fde198..78c744b96 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(() => (
), []);
@@ -24,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
}
@@ -41,7 +43,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types,
{
typeof transportUrl === 'string' && transportUrl.length > 0 ?
- URL:
+ {`${t('URL')}:`}
{transportUrl}
:
@@ -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..a89a68b77 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,13 +44,14 @@ function withRemoteAndLocalAddon(AddonDetails) {
}
const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
+ const { t } = useTranslation();
const { core } = useServices();
const platform = usePlatform();
const addonDetails = useAddonDetails(transportUrl);
const modalButtons = React.useMemo(() => {
const cancelButton = {
className: styles['cancel-button'],
- label: 'Cancel',
+ label: t('BUTTON_CANCEL'),
props: {
onClick: (event) => {
if (typeof onCloseRequest === 'function') {
@@ -67,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'));
@@ -86,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({
@@ -113,7 +115,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
{
className: styles['install-button'],
- label: 'Install',
+ label: t('ADDON_INSTALL'),
props: {
onClick: (event) => {
core.transport.dispatch({
@@ -141,21 +143,21 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
return addonDetails.remoteAddon?.content.type === 'Ready' ? addonDetails.remoteAddon.content.content.manifest.background : null;
}, [addonDetails.remoteAddon]);
return (
-
+
{
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;
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/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 ?
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 54%
rename from src/components/ColorInput/ColorInput.js
rename to src/components/ColorInput/ColorInput.tsx
index afa411f63..fa48118b9 100644
--- a/src/components/ColorInput/ColorInput.js
+++ b/src/components/ColorInput/ColorInput.tsx
@@ -1,75 +1,85 @@
// 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();
};
return [
{
- label: 'Select',
+ label: t('SELECT'),
props: {
'data-autofocus': true,
onClick: selectButtonOnClick
}
}
];
- }, [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 (
{
@@ -82,7 +92,7 @@ const ColorInput = ({ className, value, dataset, onChange, ...props }) => {
}
{
modalOpen ?
-
+
:
@@ -92,12 +102,4 @@ const ColorInput = ({ className, value, dataset, onChange, ...props }) => {
);
};
-ColorInput.propTypes = {
- className: PropTypes.string,
- value: PropTypes.string,
- dataset: PropTypes.object,
- onChange: PropTypes.func,
- onClick: PropTypes.func
-};
-
-module.exports = ColorInput;
+export default ColorInput;
diff --git a/src/components/ColorInput/ColorPicker/ColorPicker.js b/src/components/ColorInput/ColorPicker/ColorPicker.js
index 47761ffc6..823ea55c8 100644
--- a/src/components/ColorInput/ColorPicker/ColorPicker.js
+++ b/src/components/ColorInput/ColorPicker/ColorPicker.js
@@ -29,10 +29,7 @@ const ColorPicker = ({ className, value, onInput }) => {
React.useLayoutEffect(() => {
if (typeof onInput === 'function') {
pickerRef.current.on('change', (picker, value) => {
- onInput({
- type: 'input',
- value: parseColor(value)
- });
+ onInput(parseColor(value));
});
}
return () => {
diff --git a/src/components/ColorInput/index.js b/src/components/ColorInput/index.js
deleted file mode 100644
index be9abc89f..000000000
--- a/src/components/ColorInput/index.js
+++ /dev/null
@@ -1,6 +0,0 @@
-// Copyright (C) 2017-2023 Smart code 203358507
-
-const ColorInput = require('./ColorInput');
-
-module.exports = ColorInput;
-
diff --git a/src/components/ColorInput/index.ts b/src/components/ColorInput/index.ts
new file mode 100644
index 000000000..aaa786f2c
--- /dev/null
+++ b/src/components/ColorInput/index.ts
@@ -0,0 +1,6 @@
+// Copyright (C) 2017-2023 Smart code 203358507
+
+import ColorInput from './ColorInput';
+
+export default ColorInput;
+
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/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')}
}
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/components/MultiselectMenu/Dropdown/Dropdown.tsx b/src/components/MultiselectMenu/Dropdown/Dropdown.tsx
index 411ef6ab5..00fd3ff6a 100644
--- a/src/components/MultiselectMenu/Dropdown/Dropdown.tsx
+++ b/src/components/MultiselectMenu/Dropdown/Dropdown.tsx
@@ -10,11 +10,11 @@ import styles from './Dropdown.less';
type Props = {
options: MultiselectMenuOption[];
- value?: string | number;
+ value?: any;
menuOpen: boolean | (() => void);
level: number;
setLevel: (level: number) => void;
- onSelect: (value: string | number) => void;
+ onSelect: (value: any) => void;
};
const Dropdown = ({ level, setLevel, options, onSelect, value, menuOpen }: Props) => {
@@ -24,7 +24,7 @@ const Dropdown = ({ level, setLevel, options, onSelect, value, menuOpen }: Props
const selectedOption = options.find((opt) => opt.value === value);
- const handleSetOptionRef = useCallback((optionValue: string | number) => (node: HTMLButtonElement | null) => {
+ const handleSetOptionRef = useCallback((optionValue: any) => (node: HTMLButtonElement | null) => {
if (node) {
optionsRef.current.set(optionValue, node);
} else {
diff --git a/src/components/MultiselectMenu/Dropdown/Option/Option.tsx b/src/components/MultiselectMenu/Dropdown/Option/Option.tsx
index 9ff1480f1..5d43fb45b 100644
--- a/src/components/MultiselectMenu/Dropdown/Option/Option.tsx
+++ b/src/components/MultiselectMenu/Dropdown/Option/Option.tsx
@@ -8,8 +8,8 @@ import Icon from '@stremio/stremio-icons/react';
type Props = {
option: MultiselectMenuOption;
- selectedValue?: string | number;
- onSelect: (value: string | number) => void;
+ selectedValue?: any;
+ onSelect: (value: any) => void;
};
const Option = forwardRef(({ option, selectedValue, onSelect }, ref) => {
diff --git a/src/components/MultiselectMenu/MultiselectMenu.tsx b/src/components/MultiselectMenu/MultiselectMenu.tsx
index 4fef5ce81..eb288a12b 100644
--- a/src/components/MultiselectMenu/MultiselectMenu.tsx
+++ b/src/components/MultiselectMenu/MultiselectMenu.tsx
@@ -11,13 +11,14 @@ import useOutsideClick from 'stremio/common/useOutsideClick';
type Props = {
className?: string,
- title?: string | (() => string);
+ title?: string | (() => string | null);
options: MultiselectMenuOption[];
- value?: string | number;
- onSelect: (value: string | number) => void;
+ value?: any;
+ disabled?: boolean,
+ 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);
@@ -32,6 +33,7 @@ const MultiselectMenu = ({ className, title, options, value, onSelect }: Props)
{
onClick={selectInputContent}
tabIndex={-1}
/>
-
+
{ t('COPY') }
diff --git a/src/components/Toggle/Toggle.js b/src/components/Toggle/Toggle.js
deleted file mode 100644
index 420f78392..000000000
--- a/src/components/Toggle/Toggle.js
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2017-2023 Smart code 203358507
-
-const React = require('react');
-const PropTypes = require('prop-types');
-const classnames = require('classnames');
-const { Button } = require('stremio/components');
-const styles = require('./styles');
-
-const Toggle = React.forwardRef(({ className, checked, children, ...props }, ref) => {
- return (
-
-
- {children}
-
- );
-});
-
-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 (
+
+
+ {children}
+
+ );
+});
+
+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/components/Video/Video.js b/src/components/Video/Video.js
index 585920cdb..5ec609d2b 100644
--- a/src/components/Video/Video.js
+++ b/src/components/Video/Video.js
@@ -1,9 +1,9 @@
// 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');
const { useRouteFocused } = require('stremio-router');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { Button, Image, Popup } = require('stremio/components');
@@ -107,12 +107,12 @@ const Video = React.forwardRef(({ className, id, title, thumbnail, season, episo
{
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 ?
-
- TBA
+
+ {t('TBA')}
:
null
@@ -121,7 +121,7 @@ const Video = React.forwardRef(({ className, id, title, thumbnail, season, episo
{
upcoming && !watched ?
-
Upcoming
+
{t('UPCOMING')}
:
null
@@ -130,7 +130,7 @@ const Video = React.forwardRef(({ className, id, title, thumbnail, season, episo
watched ?
-
Watched
+
{t('CTX_WATCHED')}
:
null
@@ -145,10 +145,10 @@ const Video = React.forwardRef(({ className, id, title, thumbnail, season, episo
const renderMenu = React.useMemo(() => function renderMenu() {
return (
-
+
{t('CTX_WATCH')}
-
+
{watched ? t('CTX_MARK_NON_WATCHED') : t('CTX_MARK_WATCHED')}
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/Addons/Addon/Addon.js b/src/routes/Addons/Addon/Addon.js
index e2182243a..5a2f29631 100644
--- a/src/routes/Addons/Addon/Addon.js
+++ b/src/routes/Addons/Addon/Addon.js
@@ -89,7 +89,7 @@ const Addon = ({ className, id, name, version, logo, description, types, behavio
{
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 451101cbe..da9544d51 100644
--- a/src/routes/Addons/Addons.js
+++ b/src/routes/Addons/Addons.js
@@ -124,7 +124,7 @@ const Addons = ({ urlParams, queryParams }) => {
value={search}
onChange={searchInputOnChange}
/>
-
+
@@ -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')}
:
@@ -216,7 +216,7 @@ const Addons = ({ urlParams, queryParams }) => {
{
filtersModalOpen ?
-
+
{selectInputs.map((selectInput, index) => (
{
{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/Addons/useSelectableInputs.js b/src/routes/Addons/useSelectableInputs.js
index a8af37fbe..003da745d 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_'),
})),
value: selectedCatalog ? selectedCatalog.deepLinks.addons : undefined,
title: remoteAddons.selected !== null ?
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
diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js
index 1c2b6f122..1fc16c706 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);
@@ -111,7 +113,7 @@ const Discover = ({ urlParams, queryParams }) => {
/>
))}
-
+
@@ -119,9 +121,9 @@ const Discover = ({ urlParams, queryParams }) => {
{
discover.catalog !== null && !discover.catalog.installed ?
-
Addon is not installed. Install now?
-
- Install
+ {t('ERR_ADDON_NOT_INSTALLED')}
+
+ {t('ADDON_INSTALL')}
:
@@ -132,7 +134,7 @@ const Discover = ({ urlParams, queryParams }) => {
-
No catalog selected!
+
{t('NO_CATALOG_SELECTED')}
:
@@ -201,7 +203,7 @@ const Discover = ({ urlParams, queryParams }) => {
{
inputsModalOpen ?
-
+
{selectInputs.map(({ title, options, value, onSelect }, index) => (
{
return {
isRequired: isRequired,
options: options.map(({ value, deepLinks }) => ({
- label: typeof value === 'string' ? t.stringWithPrefix(value) : t.string('NONE'),
+ label: typeof value === 'string' ? t.string(value) : t.string('NONE'),
value: JSON.stringify({
href: deepLinks.discover,
value
@@ -60,8 +60,8 @@ const mapSelectableInputs = (discover, t) => {
value: selectedExtra.value,
}),
title: options.some(({ selected, value }) => selected && value === null) ?
- () => t.stringWithPrefix(name, 'SELECT_')
- : t.stringWithPrefix(selectedExtra.value),
+ () => t.string(name.toUpperCase())
+ : t.string(selectedExtra.value),
onSelect: (value) => {
const { href } = JSON.parse(value);
window.location = href;
diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js
index 6af8f0167..48acadcd4 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')}
@@ -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,37 +332,37 @@ 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}
/>
:
- Forgot password?
+ {t('FORGOT_PASSWORD')}
}
{
@@ -372,22 +372,22 @@ const Intro = ({ queryParams }) => {
null
}
- {state.form === SIGNUP_FORM ? 'Sign up' : 'Log in'}
+ {state.form === SIGNUP_FORM ? t('SIGN_UP') : t('LOG_IN')}
- Continue with Facebook
+ {t('FB_LOGIN')}
- Continue with Apple
+ {t('APPLE_LOGIN')}
{
state.form === SIGNUP_FORM ?
- LOG IN
+ {t('LOG_IN')}
:
null
@@ -395,7 +395,7 @@ const Intro = ({ queryParams }) => {
{
state.form === LOGIN_FORM ?
- SIGN UP WITH EMAIL
+ {t('SIGN_UP_EMAIL')}
:
null
@@ -403,7 +403,7 @@ const Intro = ({ queryParams }) => {
{
state.form === SIGNUP_FORM ?
- GUEST LOGIN
+ {t('GUEST_LOGIN')}
:
null
@@ -421,7 +421,7 @@ const Intro = ({ queryParams }) => {
-
Authenticating...
+
{t('AUTHENTICATING')}
{t('BUTTON_CANCEL')}
diff --git a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js b/src/routes/Intro/PasswordResetModal/PasswordResetModal.js
index 6f295fdd6..8c69c1489 100644
--- a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js
+++ b/src/routes/Intro/PasswordResetModal/PasswordResetModal.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 { useRouteFocused } = require('stremio-router');
const { usePlatform } = require('stremio/common');
@@ -9,6 +10,7 @@ const CredentialsTextInput = require('../CredentialsTextInput');
const styles = require('./styles');
const PasswordResetModal = ({ email, onCloseRequest }) => {
+ const { t } = useTranslation();
const routeFocused = useRouteFocused();
const platform = usePlatform();
const [error, setError] = React.useState('');
@@ -23,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
}
@@ -45,7 +47,7 @@ const PasswordResetModal = ({ email, onCloseRequest }) => {
}
}, [routeFocused]);
return (
-
+
{
+ 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')}
:
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 627b41857..eebc0c3cf 100644
--- a/src/routes/MetaDetails/StreamsList/StreamsList.js
+++ b/src/routes/MetaDetails/StreamsList/StreamsList.js
@@ -133,7 +133,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') ?
diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js
index 51b51f0e7..ea870a931 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(() => {
@@ -56,19 +56,19 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
return (
-
+
- Prev
+ {t('BUTTON_PREV')}
0 ? `${t('SEASON')} ${season}` : t('SPECIAL')}
+ title={season > 0 ? t('SEASON_NUMBER', { season }) : t('SPECIAL')}
value={selectedSeason}
onSelect={seasonOnSelect}
/>
-
- Next
+
+ {t('BUTTON_NEXT')}
diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBarPlaceholder/SeasonsBarPlaceholder.js b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBarPlaceholder/SeasonsBarPlaceholder.js
index d767769ec..4013e0b64 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_NUMBER', { season: 1 })}
-
Next
+
{t('SEASON_NEXT')}
diff --git a/src/routes/MetaDetails/VideosList/VideosList.js b/src/routes/MetaDetails/VideosList/VideosList.js
index a47b2f517..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]);
@@ -122,7 +124,7 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect,
-
No videos found for this meta!
+
{t('ERR_NO_VIDEOS_FOR_META')}
:
@@ -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) => (
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')}
);
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 (
+
+
+
+ );
+};
+
+export default Indicator;
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
- Dismiss
+ {t('PLAYER_NEXT_VIDEO_BUTTON_DISMISS')}
- Watch Now
+ {t('PLAYER_NEXT_VIDEO_BUTTON_WATCH')}
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index 2e26e5240..69fdb73e6 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();
@@ -217,6 +218,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]);
@@ -592,6 +603,14 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
+ case 'KeyG': {
+ onDecreaseSubtitlesDelay();
+ break;
+ }
+ case 'KeyH': {
+ onIncreaseSubtitlesDelay();
+ break;
+ }
case 'Escape': {
closeMenus();
!settings.escExitFullscreen && window.history.back();
@@ -625,7 +644,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);
@@ -765,6 +806,10 @@ const Player = ({ urlParams, queryParams }) => {
onMouseOver={onBarMouseMove}
onTouchEnd={onContainerMouseLeave}
/>
+
{
nextVideoPopupOpen ?
{
+ const { t } = useTranslation();
return (
- Statistics
+ {t('PLAYER_STATISTICS')}
- Peers
+ {t('PLAYER_PEERS')}
{ peers }
@@ -22,15 +24,15 @@ const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => {
- Speed
+ {t('PLAYER_SPEED')}
- { speed } MB/s
+ {`${speed} ${t('MB_S')}`}
- 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 }
diff --git a/src/routes/Player/SubtitlesMenu/DiscreteSelectInput/DiscreteSelectInput.js b/src/routes/Player/SubtitlesMenu/DiscreteSelectInput/DiscreteSelectInput.js
index a57754793..ea7948b83 100644
--- a/src/routes/Player/SubtitlesMenu/DiscreteSelectInput/DiscreteSelectInput.js
+++ b/src/routes/Player/SubtitlesMenu/DiscreteSelectInput/DiscreteSelectInput.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 DiscreteSelectInput = ({ className, value, label, disabled, dataset, onChange }) => {
+ 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/Player/styles.less b/src/routes/Player/styles.less
index 13ccc46cb..4894791f0 100644
--- a/src/routes/Player/styles.less
+++ b/src/routes/Player/styles.less
@@ -107,6 +107,13 @@ html:not(.active-slider-within) {
}
}
+ &.indicator-layer {
+ top: initial;
+ left: 0;
+ right: 0;
+ bottom: 10rem;
+ }
+
&.menu-layer {
top: initial;
left: initial;
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..8f5496dbd
--- /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 &&
+
+ }
+
+
+ {isTraktAuthenticated ? t('LOG_OUT') : t('SETTINGS_TRAKT_AUTHENTICATE')}
+
+
+
+
+
+
+
+
+ {
+ 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..6b44e9903
--- /dev/null
+++ b/src/routes/Settings/General/User/User.tsx
@@ -0,0 +1,66 @@
+import React, { useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useServices } from 'stremio/services';
+import { Link } from '../../components';
+import styles from './User.less';
+
+type Props = {
+ profile: Profile,
+};
+
+const User = ({ profile }: Props) => {
+ const { t } = useTranslation();
+ 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 ? t('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..a0a8e2236
--- /dev/null
+++ b/src/routes/Settings/Info/Info.tsx
@@ -0,0 +1,52 @@
+import React, { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useServices } from 'stremio/services';
+import { Option, Section } from '../components';
+import styles from './Info.less';
+
+type Props = {
+ streamingServer: StreamingServer,
+};
+
+const Info = ({ streamingServer }: Props) => {
+ const { shell } = useServices();
+ const { t } = useTranslation();
+
+ const settings = useMemo(() => (
+ streamingServer?.settings?.type === 'Ready' ?
+ streamingServer.settings.content as StreamingServerSettings : null
+ ), [streamingServer?.settings]);
+
+ return (
+
+
+
+ {process.env.VERSION}
+
+
+
+
+ {process.env.COMMIT_HASH}
+
+
+ {
+ settings?.serverVersion &&
+
+
+ {settings.serverVersion}
+
+
+ }
+ {
+ typeof shell?.transport?.props?.shellVersion === 'string' &&
+
+
+ {shell.transport.props.shellVersion}
+
+
+ }
+
+ );
+};
+
+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..ceafee94b
--- /dev/null
+++ b/src/routes/Settings/Menu/Menu.tsx
@@ -0,0 +1,62 @@
+import React, { useMemo } from 'react';
+import classNames from 'classnames';
+import { useTranslation } from 'react-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 { t } = useTranslation();
+ const { shell } = useServices();
+
+ const settings = useMemo(() => (
+ streamingServer?.settings?.type === 'Ready' ?
+ streamingServer.settings.content as StreamingServerSettings : null
+ ), [streamingServer?.settings]);
+
+ return (
+
+
+ { t('SETTINGS_NAV_GENERAL') }
+
+
+ { t('SETTINGS_NAV_PLAYER') }
+
+
+ { t('SETTINGS_NAV_STREAMING') }
+
+
+ { t('SETTINGS_NAV_SHORTCUTS') }
+
+
+
+
+ {t('SETTINGS_APP_VERSION')}: {process.env.VERSION}
+
+
+ {t('SETTINGS_BUILD_VERSION')}: {process.env.COMMIT_HASH}
+
+ {
+ settings?.serverVersion &&
+
+ {t('SETTINGS_SERVER_VERSION')}: {settings.serverVersion}
+
+ }
+ {
+ typeof shell?.transport?.props?.shellVersion === 'string' &&
+
+ {t('SETTINGS_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 (
-
-
-
-
- { t('SETTINGS_NAV_GENERAL') }
-
-
- { t('SETTINGS_NAV_PLAYER') }
-
-
- { t('SETTINGS_NAV_STREAMING') }
-
-
- { t('SETTINGS_NAV_SHORTCUTS') }
-
-
-
- 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 ?
-
- { t('LOG_OUT') }
-
- :
- null
- }
-
-
-
- {
- profile.auth === null ?
-
-
- { t('LOG_IN') } / { t('SIGN_UP') }
-
-
- :
- null
- }
-
-
-
- {
- profile.auth ?
-
- { t('SETTINGS_DATA_EXPORT') }
-
- :
- null
- }
-
- {
- profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user._id === 'string' ?
-
-
- { t('SETTINGS_SUBSCRIBE_CALENDAR') }
-
-
- :
- null
- }
-
-
- { t('SETTINGS_SUPPORT') }
-
-
-
-
-
- { t('TERMS_OF_SERVICE') }
-
-
-
-
- { t('PRIVACY_POLICY') }
-
-
- {
- profile.auth !== null && profile.auth.user !== null ?
-
-
- { t('SETTINGS_ACC_DELETE') }
-
-
- :
- null
- }
- {
- profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user.email === 'string' ?
-
-
- { t('SETTINGS_CHANGE_PASSWORD') }
-
-
- :
- null
- }
-
-
-
-
- { isTraktAuthenticated ? t('LOG_OUT') : t('SETTINGS_TRAKT_AUTHENTICATE') }
-
-
-
-
-
-
-
-
{ 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('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') }
-
-
-
-
-
-
-
-
- {process.env.VERSION}
-
-
-
-
-
-
-
- {process.env.COMMIT_HASH}
-
-
-
- {
- streamingServer.settings !== null && streamingServer.settings.type === 'Ready' ?
-
-
-
-
- {streamingServer.settings.content.serverVersion}
-
-
-
- :
- null
- }
- {
- typeof shell?.transport?.props?.shellVersion === 'string' ?
-
-
-
-
- { 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..0301eafca
--- /dev/null
+++ b/src/routes/Settings/Shortcuts/Shortcuts.tsx
@@ -0,0 +1,97 @@
+import React, { forwardRef } from 'react';
+import { Section, Option } from '../components';
+import styles from './Shortcuts.less';
+import { useTranslation } from 'react-i18next';
+
+const Shortcuts = forwardRef((_, ref) => {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+ {t('SETTINGS_SHORTCUT_SPACE')}
+
+
+
+
+
→
+
{t('SETTINGS_SHORTCUT_OR')}
+
⇧ {t('SETTINGS_SHORTCUT_SHIFT')}
+
+
+
→
+
+
+
+
+
←
+
{t('SETTINGS_SHORTCUT_OR')}
+
⇧ {t('SETTINGS_SHORTCUT_SHIFT')}
+
+
+
←
+
+
+
+
+ ↑
+
+
+
+
+ ↓
+
+
+
+
+ S
+
+
+
+
+ A
+
+
+
+
+ I
+
+
+
+
+ V
+
+
+
+
+ F
+
+
+
+
+
G
+
{ t('SETTINGS_SHORTCUT_AND') }
+
H
+
+
+
+
+
1
+
{t('SETTINGS_SHORTCUT_TO')}
+
6
+
+
+
+
+ 0
+
+
+
+
+ {t('SETTINGS_SHORTCUT_ESC')}
+
+
+
+ );
+});
+
+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..4eac9a0ef
--- /dev/null
+++ b/src/routes/Settings/Streaming/Streaming.tsx
@@ -0,0 +1,92 @@
+import React, { forwardRef, useCallback } from 'react';
+import { useTranslation } from 'react-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 { t } = useTranslation();
+ 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 &&
+
+ {streamingServerRemoteUrlInput.value}
+
+
+
+
+ }
+ {
+ 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 87%
rename from src/routes/Settings/URLsManager/URLsManager.tsx
rename to src/routes/Settings/Streaming/URLsManager/URLsManager.tsx
index 46e57020d..b0d1245eb 100644
--- a/src/routes/Settings/URLsManager/URLsManager.tsx
+++ b/src/routes/Settings/Streaming/URLsManager/URLsManager.tsx
@@ -30,7 +30,7 @@ const URLsManager = () => {
return (
-
URL
+
{t('URL')}
{t('STATUS')}
@@ -46,11 +46,11 @@ const URLsManager = () => {
}
-
+
{t('SETTINGS_SERVER_ADD_URL')}
-
+
{t('RELOAD')}
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 59%
rename from src/routes/Settings/useStreamingServerSettingsInputs.js
rename to src/routes/Settings/Streaming/useStreamingOptions.ts
index e4bd7e79c..140909132 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);
@@ -140,7 +165,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(
@@ -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 (
+
+ );
+};
+
+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 (
+
+ { label }
+
+ );
+};
+
+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,
};
diff --git a/tests/i18nScan.test.js b/tests/i18nScan.test.js
new file mode 100644
index 000000000..5f2964a9a
--- /dev/null
+++ b/tests/i18nScan.test.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';
+
+function toKey(str) {
+ return str
+ .toLowerCase()
+ .replace(/[^a-z0-9\s]/g, '')
+ .replace(/\s+/g, '_')
+ .slice(0, 40);
+}
+
+function scanFile(filePath, report) {
+ 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, {
+ 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);
+ },
+
+ visitJSXExpressionContainer(path) {
+ const expr = path.node.expression;
+
+ if (
+ expr.type === 'CallExpression' &&
+ expr.callee.type === 'Identifier' &&
+ expr.callee.name === 't'
+ ) {
+ return false;
+ }
+
+ 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, report) {
+ fs.readdirSync(dir).forEach((file) => {
+ const fullPath = path.join(dir, file);
+ if (fs.statSync(fullPath).isDirectory()) {
+ walk(fullPath, report);
+ } else if (/\.(js|jsx|ts|tsx)$/.test(file)) {
+ // console.log('📄 Scanning file:', fullPath);
+ scanFile(fullPath, report);
+ }
+ });
+}
+const report = [];
+
+walk(directoryToScan, report);
+
+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
+ });
+ });
+}