diff --git a/.github/workflows/auto_assign.yml b/.github/workflows/auto_assign.yml
new file mode 100644
index 000000000..dfbddb6a4
--- /dev/null
+++ b/.github/workflows/auto_assign.yml
@@ -0,0 +1,65 @@
+name: PR and Issue Workflow
+on:
+ pull_request:
+ types: [opened, reopened]
+ issues:
+ types: [opened]
+jobs:
+ auto-assign-and-label:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: write
+ steps:
+ # Auto assign PR to author
+ - name: Auto Assign PR to Author
+ if: github.event_name == 'pull_request'
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const pr = context.payload.pull_request;
+ if (pr) {
+ await github.rest.issues.addAssignees({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ assignees: [pr.user.login]
+ });
+ console.log(`Assigned PR #${pr.number} to author @${pr.user.login}`);
+ }
+
+ # Dynamic labeling based on PR/Issue title
+ - name: Label PRs and Issues
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const prTitle = context.payload.pull_request ? context.payload.pull_request.title : context.payload.issue.title;
+ const issueNumber = context.payload.pull_request ? context.payload.pull_request.number : context.payload.issue.number;
+ const isIssue = context.payload.issue !== undefined;
+ const labelMappings = [
+ { pattern: /^feat(ure)?/i, label: 'feature' },
+ { pattern: /^fix/i, label: 'bug' },
+ { pattern: /^refactor/i, label: 'refactor' },
+ { pattern: /^chore/i, label: 'chore' },
+ { pattern: /^docs?/i, label: 'documentation' },
+ { pattern: /^perf(ormance)?/i, label: 'performance' },
+ { pattern: /^test/i, label: 'testing' }
+ ];
+ let labelsToAdd = [];
+ for (const mapping of labelMappings) {
+ if (mapping.pattern.test(prTitle)) {
+ labelsToAdd.push(mapping.label);
+ }
+ }
+ if (labelsToAdd.length > 0) {
+ github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issueNumber,
+ labels: labelsToAdd
+ });
+ }
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index c1ae445b5..6cc585fb6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -38,7 +38,7 @@
"react-router": "6.30.0",
"react-router-dom": "6.30.0",
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
- "stremio-translations": "github:Stremio/stremio-translations#a6be0425573917c2e82b66d28968c1a4d444cb96",
+ "stremio-translations": "github:Stremio/stremio-translations#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f",
"url": "0.11.4",
"use-long-press": "^3.2.0"
},
@@ -68,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",
@@ -4797,6 +4798,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",
@@ -12540,6 +12554,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",
@@ -13416,9 +13447,9 @@
}
},
"node_modules/stremio-translations": {
- "version": "1.44.10",
- "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#a6be0425573917c2e82b66d28968c1a4d444cb96",
- "integrity": "sha512-77kVE/eos/SA16kzeK7TTWmqoLF0mLPCJXjITwVIVzMHr8XyBPZFOfmiVEg4M6W1W7qYqA+dHhzicyLs7hJhlw==",
+ "version": "1.44.12",
+ "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f",
+ "integrity": "sha512-b38OjGwlsvFm/aNn/ia18mPxPjZvnI/GaToppn1XaQqCuZuSHxQlYDddwOYTztskWo4VO/IZmCi3UFewqpsqCQ==",
"license": "MIT"
},
"node_modules/string_decoder": {
@@ -14019,6 +14050,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 c4d61dfd4..c813ad16c 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",
@@ -42,7 +43,7 @@
"react-router": "6.30.0",
"react-router-dom": "6.30.0",
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
- "stremio-translations": "github:Stremio/stremio-translations#a6be0425573917c2e82b66d28968c1a4d444cb96",
+ "stremio-translations": "github:Stremio/stremio-translations#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f",
"url": "0.11.4",
"use-long-press": "^3.2.0"
},
@@ -72,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/App/App.js b/src/App/App.js
index 00fbc66af..a1976b586 100644
--- a/src/App/App.js
+++ b/src/App/App.js
@@ -96,12 +96,18 @@ const App = () => {
// Handle shell events
React.useEffect(() => {
const onOpenMedia = (data) => {
- if (data.startsWith('stremio:///')) return;
- if (data.startsWith('stremio://')) {
- const transportUrl = data.replace('stremio://', 'https://');
- if (URL.canParse(transportUrl)) {
- window.location.href = `#/addons?addon=${encodeURIComponent(transportUrl)}`;
+ try {
+ const { protocol, hostname, pathname, searchParams } = new URL(data);
+ if (protocol === CONSTANTS.PROTOCOL) {
+ if (hostname.length) {
+ const transportUrl = `https://${hostname}${pathname}`;
+ window.location.href = `#/addons?addon=${encodeURIComponent(transportUrl)}`;
+ } else {
+ window.location.href = `#${pathname}?${searchParams.toString()}`;
+ }
}
+ } catch (e) {
+ console.error('Failed to open media:', e);
}
};
diff --git a/src/common/CONSTANTS.js b/src/common/CONSTANTS.js
index 8e4e3efdc..92d009895 100644
--- a/src/common/CONSTANTS.js
+++ b/src/common/CONSTANTS.js
@@ -106,6 +106,8 @@ const EXTERNAL_PLAYERS = [
const WHITELISTED_HOSTS = ['stremio.com', 'strem.io', 'stremio.zendesk.com', 'google.com', 'youtube.com', 'twitch.tv', 'twitter.com', 'x.com', 'netflix.com', 'adex.network', 'amazon.com', 'forms.gle'];
+const PROTOCOL = 'stremio:';
+
module.exports = {
CHROMECAST_RECEIVER_APP_ID,
DEFAULT_STREAMING_SERVER_URL,
@@ -127,4 +129,5 @@ module.exports = {
SUPPORTED_LOCAL_SUBTITLES,
EXTERNAL_PLAYERS,
WHITELISTED_HOSTS,
+ PROTOCOL,
};
diff --git a/src/common/Platform/Platform.tsx b/src/common/Platform/Platform.tsx
index 2212303e4..0da1881ef 100644
--- a/src/common/Platform/Platform.tsx
+++ b/src/common/Platform/Platform.tsx
@@ -1,6 +1,5 @@
import React, { createContext, useContext } from 'react';
import { WHITELISTED_HOSTS } from 'stremio/common/CONSTANTS';
-import useShell from 'stremio/common/useShell';
import { name, isMobile } from './device';
interface PlatformContext {
@@ -16,19 +15,13 @@ type Props = {
};
const PlatformProvider = ({ children }: Props) => {
- const shell = useShell();
-
const openExternal = (url: string) => {
try {
const { hostname } = new URL(url);
const isWhitelisted = WHITELISTED_HOSTS.some((host: string) => hostname.endsWith(host));
const finalUrl = !isWhitelisted ? `https://www.stremio.com/warning#${encodeURIComponent(url)}` : url;
- if (shell.active) {
- shell.send('open-external', finalUrl);
- } else {
- window.open(finalUrl, '_blank');
- }
+ window.open(finalUrl, '_blank');
} catch (e) {
console.error('Failed to parse external url:', e);
}
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/routesRegexp.js b/src/common/routesRegexp.js
index 3903da44b..b9989b4b5 100644
--- a/src/common/routesRegexp.js
+++ b/src/common/routesRegexp.js
@@ -6,7 +6,7 @@ const routesRegexp = {
urlParamsNames: []
},
board: {
- regexp: /^\/?$/,
+ regexp: /^\/?(?:board)?$/,
urlParamsNames: []
},
discover: {
diff --git a/src/common/useFullscreen.ts b/src/common/useFullscreen.ts
index 5f0975fb8..9bd5d0fc5 100644
--- a/src/common/useFullscreen.ts
+++ b/src/common/useFullscreen.ts
@@ -46,6 +46,10 @@ const useFullscreen = () => {
exitFullscreen();
}
+ if (event.code === 'KeyF') {
+ toggleFullscreen();
+ }
+
if (event.code === 'F11' && shell.active) {
toggleFullscreen();
}
@@ -60,7 +64,7 @@ const useFullscreen = () => {
document.removeEventListener('keydown', onKeyDown);
document.removeEventListener('fullscreenchange', onFullscreenChange);
};
- }, [settings.escExitFullscreen]);
+ }, [settings.escExitFullscreen, toggleFullscreen]);
return [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen];
};
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/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/ColorInput.js b/src/components/ColorInput/ColorInput.js
index afa411f63..5bedf7161 100644
--- a/src/components/ColorInput/ColorInput.js
+++ b/src/components/ColorInput/ColorInput.js
@@ -56,7 +56,7 @@ const ColorInput = ({ className, value, dataset, onChange, ...props }) => {
};
return [
{
- label: 'Select',
+ label: t('SELECT'),
props: {
'data-autofocus': true,
onClick: selectButtonOnClick
@@ -82,7 +82,7 @@ const ColorInput = ({ className, value, dataset, onChange, ...props }) => {
}
{
modalOpen ?
-
+
:
diff --git a/src/components/MetaPreview/MetaPreview.js b/src/components/MetaPreview/MetaPreview.js
index c4eb47c0a..c0e9fb165 100644
--- a/src/components/MetaPreview/MetaPreview.js
+++ b/src/components/MetaPreview/MetaPreview.js
@@ -24,7 +24,7 @@ const ALLOWED_LINK_REDIRECTS = [
routesRegexp.metadetails.regexp
];
-const MetaPreview = ({ className, compact, name, logo, background, runtime, releaseInfo, released, description, deepLinks, links, trailerStreams, inLibrary, toggleInLibrary }) => {
+const MetaPreview = React.forwardRef(({ className, compact, name, logo, background, runtime, releaseInfo, released, description, deepLinks, links, trailerStreams, inLibrary, toggleInLibrary }, ref) => {
const { t } = useTranslation();
const [shareModalOpen, openShareModal, closeShareModal] = useBinaryState(false);
const linksGroups = React.useMemo(() => {
@@ -98,7 +98,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
{name}
), [name]);
return (
-
+
{
typeof background === 'string' && background.length > 0 ?
@@ -261,7 +261,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
);
-};
+});
MetaPreview.Placeholder = MetaPreviewPlaceholder;
diff --git a/src/components/ModalDialog/ModalDialog.js b/src/components/ModalDialog/ModalDialog.js
index 211387a0a..44f1f1fdb 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 { useModalsContainer } = require('stremio/router/ModalsContainerContext');
@@ -11,6 +12,7 @@ const { default: Icon } = require('@stremio/stremio-icons/react');
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);
@@ -61,7 +63,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 3da75619b..411ef6ab5 100644
--- a/src/components/MultiselectMenu/Dropdown/Dropdown.tsx
+++ b/src/components/MultiselectMenu/Dropdown/Dropdown.tsx
@@ -10,23 +10,25 @@ import styles from './Dropdown.less';
type Props = {
options: MultiselectMenuOption[];
- selectedOption?: MultiselectMenuOption | null;
+ value?: string | number;
menuOpen: boolean | (() => void);
level: number;
setLevel: (level: number) => void;
- onSelect: (value: number) => void;
+ onSelect: (value: string | number) => void;
};
-const Dropdown = ({ level, setLevel, options, onSelect, selectedOption, menuOpen }: Props) => {
+const Dropdown = ({ level, setLevel, options, onSelect, value, menuOpen }: Props) => {
const { t } = useTranslation();
const optionsRef = useRef(new Map());
const containerRef = useRef(null);
- const handleSetOptionRef = useCallback((value: number) => (node: HTMLButtonElement | null) => {
+ const selectedOption = options.find((opt) => opt.value === value);
+
+ const handleSetOptionRef = useCallback((optionValue: string | number) => (node: HTMLButtonElement | null) => {
if (node) {
- optionsRef.current.set(value, node);
+ optionsRef.current.set(optionValue, node);
} else {
- optionsRef.current.delete(value);
+ optionsRef.current.delete(optionValue);
}
}, []);
@@ -63,11 +65,11 @@ const Dropdown = ({ level, setLevel, options, onSelect, selectedOption, menuOpen
.filter((option: MultiselectMenuOption) => !option.hidden)
.map((option: MultiselectMenuOption) => (
))
}
diff --git a/src/components/MultiselectMenu/Dropdown/Option/Option.tsx b/src/components/MultiselectMenu/Dropdown/Option/Option.tsx
index 91aa173f7..9ff1480f1 100644
--- a/src/components/MultiselectMenu/Dropdown/Option/Option.tsx
+++ b/src/components/MultiselectMenu/Dropdown/Option/Option.tsx
@@ -8,13 +8,12 @@ import Icon from '@stremio/stremio-icons/react';
type Props = {
option: MultiselectMenuOption;
- selectedOption?: MultiselectMenuOption | null;
- onSelect: (value: number) => void;
+ selectedValue?: string | number;
+ onSelect: (value: string | number) => void;
};
-const Option = forwardRef(({ option, selectedOption, onSelect }, ref) => {
- // consider using option.id === selectedOption?.id instead
- const selected = useMemo(() => option?.value === selectedOption?.value, [option, selectedOption]);
+const Option = forwardRef(({ option, selectedValue, onSelect }, ref) => {
+ const selected = useMemo(() => option?.value === selectedValue, [option, selectedValue]);
const handleClick = useCallback(() => {
onSelect(option.value);
diff --git a/src/components/MultiselectMenu/MultiselectMenu.less b/src/components/MultiselectMenu/MultiselectMenu.less
index 3c7b81b59..4aee1a4a8 100644
--- a/src/components/MultiselectMenu/MultiselectMenu.less
+++ b/src/components/MultiselectMenu/MultiselectMenu.less
@@ -14,14 +14,21 @@
}
.multiselect-button {
- color: var(--primary-foreground-color);
padding: 0.75rem 1.5rem;
display: flex;
+ flex: 1;
justify-content: space-between;
align-items: center;
gap: 0 0.5rem;
border-radius: @border-radius;
+ .label {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: var(--primary-foreground-color);
+ }
+
.icon {
width: 1rem;
color: var(--primary-foreground-color);
@@ -33,7 +40,7 @@
}
}
- &:hover {
+ &:hover, &.active {
background-color: var(--overlay-color);
}
}
\ No newline at end of file
diff --git a/src/components/MultiselectMenu/MultiselectMenu.tsx b/src/components/MultiselectMenu/MultiselectMenu.tsx
index 35be107c9..4fef5ce81 100644
--- a/src/components/MultiselectMenu/MultiselectMenu.tsx
+++ b/src/components/MultiselectMenu/MultiselectMenu.tsx
@@ -11,23 +11,25 @@ import useOutsideClick from 'stremio/common/useOutsideClick';
type Props = {
className?: string,
- title?: string;
+ title?: string | (() => string);
options: MultiselectMenuOption[];
- selectedOption?: MultiselectMenuOption;
- onSelect: (value: number) => void;
+ value?: string | number;
+ onSelect: (value: string | number) => void;
};
-const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }: Props) => {
+const MultiselectMenu = ({ className, title, options, value, onSelect }: Props) => {
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
const multiselectMenuRef = useOutsideClick(() => closeMenu());
const [level, setLevel] = React.useState(0);
- const onOptionSelect = (value: number) => {
- level ? setLevel(level + 1) : onSelect(value), closeMenu();
+ const selectedOption = options.find((opt) => opt.value === value);
+
+ const onOptionSelect = (selectedValue: string | number) => {
+ level ? setLevel(level + 1) : onSelect(selectedValue), closeMenu();
};
return (
-
+
- {title}
+
+ {
+ typeof title === 'function'
+ ? title()
+ : title ?? selectedOption?.label
+ }
+
{
@@ -46,7 +54,7 @@ const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }
options={options}
onSelect={onOptionSelect}
menuOpen={menuOpen}
- selectedOption={selectedOption}
+ value={value}
/>
: null
}
diff --git a/src/components/SharePrompt/SharePrompt.js b/src/components/SharePrompt/SharePrompt.js
index aca0f2ed2..eaa342dc1 100644
--- a/src/components/SharePrompt/SharePrompt.js
+++ b/src/components/SharePrompt/SharePrompt.js
@@ -70,7 +70,7 @@ const SharePrompt = ({ className, url }) => {
onClick={selectInputContent}
tabIndex={-1}
/>
-
+
{ t('COPY') }
diff --git a/src/components/Video/Video.js b/src/components/Video/Video.js
index 482c9fa89..962c36394 100644
--- a/src/components/Video/Video.js
+++ b/src/components/Video/Video.js
@@ -110,12 +110,12 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc
{
released instanceof Date && !isNaN(released.getTime()) ?
- {released.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })}
+ {released.toLocaleString(profile.settings.interfaceLanguage, { year: 'numeric', month: 'short', day: 'numeric' })}
:
scheduled ?
-
- TBA
+
+ {t('TBA')}
:
null
@@ -124,7 +124,7 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc
{
upcoming && !watched ?
-
Upcoming
+
{t('UPCOMING')}
:
null
@@ -133,7 +133,7 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc
watched ?
-
Watched
+
{t('CTX_WATCHED')}
:
null
@@ -148,10 +148,10 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc
const renderMenu = React.useMemo(() => function renderMenu() {
return (
-
+
{t('CTX_WATCH')}
-
+
{watched ? t('CTX_MARK_NON_WATCHED') : t('CTX_MARK_WATCHED')}
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 727fec4ec..17f55514a 100644
--- a/src/routes/Addons/Addons.js
+++ b/src/routes/Addons/Addons.js
@@ -7,7 +7,7 @@ const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { usePlatform, useBinaryState, withCoreSuspender } = require('stremio/common');
-const { AddonDetailsModal, Button, Image, MainNavBars, Multiselect, ModalDialog, SearchBar, SharePrompt, TextInput } = require('stremio/components');
+const { AddonDetailsModal, Button, Image, MainNavBars, ModalDialog, SearchBar, SharePrompt, TextInput, MultiselectMenu } = require('stremio/components');
const { useServices } = require('stremio/services');
const Addon = require('./Addon');
const useInstalledAddons = require('./useInstalledAddons');
@@ -110,7 +110,7 @@ const Addons = () => {
{selectInputs.map((selectInput, index) => (
- {
value={search}
onChange={searchInputOnChange}
/>
-
+
@@ -135,12 +135,12 @@ const Addons = () => {
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')}
:
@@ -219,9 +219,9 @@ const Addons = () => {
{
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/styles.less b/src/routes/Addons/styles.less
index 6a057d292..565c0b262 100644
--- a/src/routes/Addons/styles.less
+++ b/src/routes/Addons/styles.less
@@ -90,6 +90,7 @@
}
.select-input-container {
+ background-color: var(--overlay-color);
flex-grow: 0;
flex-shrink: 1;
flex-basis: 15rem;
diff --git a/src/routes/Addons/useSelectableInputs.js b/src/routes/Addons/useSelectableInputs.js
index 8395de636..c1a0b6432 100644
--- a/src/routes/Addons/useSelectableInputs.js
+++ b/src/routes/Addons/useSelectableInputs.js
@@ -5,20 +5,17 @@ const { useNavigate } = require('react-router');
const { useTranslate } = require('stremio/common');
const mapSelectableInputs = (installedAddons, remoteAddons, t, navigate) => {
+ const selectedCatalog = remoteAddons.selectable.catalogs.concat(installedAddons.selectable.catalogs).find(({ selected }) => selected);
const catalogSelect = {
- title: t.string('SELECT_CATALOG'),
options: remoteAddons.selectable.catalogs
.concat(installedAddons.selectable.catalogs)
.map(({ name, deepLinks }) => ({
value: deepLinks.addons,
- label: t.stringWithPrefix(name, 'ADDON_'),
- title: t.stringWithPrefix(name, 'ADDON_'),
+ label: t.stringWithPrefix(name.toUpperCase(), 'ADDON_'),
+ title: t.stringWithPrefix(name.toUpperCase(), 'ADDON_'),
})),
- selected: remoteAddons.selectable.catalogs
- .concat(installedAddons.selectable.catalogs)
- .filter(({ selected }) => selected)
- .map(({ deepLinks }) => deepLinks.addons),
- renderLabelText: remoteAddons.selected !== null ?
+ value: selectedCatalog ? selectedCatalog.deepLinks.addons : undefined,
+ title: remoteAddons.selected !== null ?
() => {
const selectableCatalog = remoteAddons.selectable.catalogs
.find(({ id }) => id === remoteAddons.selected.request.path.id);
@@ -26,12 +23,14 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t, navigate) => {
}
:
null,
- onSelect: (event) => {
- navigate(event.value.replace('#', ''));
+ onSelect: (value) => {
+ navigate(value.replace('#', ''));
}
};
+ const selectedType = installedAddons.selected !== null
+ ? installedAddons.selectable.types.find(({ selected }) => selected)
+ : remoteAddons.selectable.types.find(({ selected }) => selected);
const typeSelect = {
- title: t.string('SELECT_TYPE'),
options: installedAddons.selected !== null ?
installedAddons.selectable.types.map(({ type, deepLinks }) => ({
value: deepLinks.addons,
@@ -42,15 +41,8 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t, navigate) => {
value: deepLinks.addons,
label: t.stringWithPrefix(type, 'TYPE_')
})),
- selected: installedAddons.selected !== null ?
- installedAddons.selectable.types
- .filter(({ selected }) => selected)
- .map(({ deepLinks }) => deepLinks.addons)
- :
- remoteAddons.selectable.types
- .filter(({ selected }) => selected)
- .map(({ deepLinks }) => deepLinks.addons),
- renderLabelText: () => {
+ value: selectedType ? selectedType.deepLinks.addons : undefined,
+ title: () => {
return installedAddons.selected !== null ?
installedAddons.selected.request.type === null ?
t.string('TYPE_ALL')
@@ -62,8 +54,8 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t, navigate) => {
:
typeSelect.title;
},
- onSelect: (event) => {
- navigate(event.value.replace('#', ''));
+ onSelect: (value) => {
+ navigate(value.replace('#', ''));
}
};
return [catalogSelect, typeSelect];
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 a74c2d3bc..57f34d160 100644
--- a/src/routes/Discover/Discover.js
+++ b/src/routes/Discover/Discover.js
@@ -1,13 +1,14 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
+const { useTranslation } = require('react-i18next');
const { useParams } = require('react-router');
const { useSearchParams } = require('react-router-dom');
const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { useServices } = require('stremio/services');
const { CONSTANTS, useBinaryState, useOnScrollToBottom, withCoreSuspender } = require('stremio/common');
-const { AddonDetailsModal, Button, DelayedRenderer, Image, MainNavBars, MetaItem, MetaPreview, Multiselect, ModalDialog } = require('stremio/components');
+const { AddonDetailsModal, Button, DelayedRenderer, Image, MainNavBars, MetaItem, MetaPreview, ModalDialog, MultiselectMenu } = require('stremio/components');
const useDiscover = require('./useDiscover');
const useSelectableInputs = require('./useSelectableInputs');
const styles = require('./styles');
@@ -17,13 +18,17 @@ const SCROLL_TO_BOTTOM_THRESHOLD = 400;
const Discover = () => {
const urlParams = useParams();
const [queryParams] = useSearchParams();
+ const { t } = useTranslation();
const { core } = useServices();
const [discover, loadNextPage] = useDiscover(urlParams, queryParams);
const [selectInputs, hasNextPage] = useSelectableInputs(discover);
const [inputsModalOpen, openInputsModal, closeInputsModal] = useBinaryState(false);
const [addonModalOpen, openAddonModal, closeAddonModal] = useBinaryState(false);
const [selectedMetaItemIndex, setSelectedMetaItemIndex] = React.useState(0);
+
const metasContainerRef = React.useRef();
+ const metaPreviewRef = React.useRef();
+
React.useEffect(() => {
if (discover.catalog?.content.type === 'Loading') {
metasContainerRef.current.scrollTop = 0;
@@ -78,7 +83,8 @@ const Discover = () => {
}
}, []);
const metaItemOnClick = React.useCallback((event) => {
- if (event.currentTarget.dataset.index !== selectedMetaItemIndex.toString()) {
+ const visible = window.getComputedStyle(metaPreviewRef.current).display !== 'none';
+ if (event.currentTarget.dataset.index !== selectedMetaItemIndex.toString() && visible) {
event.preventDefault();
event.currentTarget.focus();
}
@@ -99,19 +105,18 @@ const Discover = () => {
- {selectInputs.map(({ title, options, selected, renderLabelText, onSelect }, index) => (
-
(
+
))}
-
+
@@ -119,9 +124,9 @@ const Discover = () => {
{
discover.catalog !== null && !discover.catalog.installed ?
-
Addon is not installed. Install now?
-
- Install
+ {t('ERR_ADDON_NOT_INSTALLED')}
+
+ {t('ADDON_INSTALL')}
:
@@ -132,7 +137,7 @@ const Discover = () => {
-
No catalog selected!
+
{t('NO_CATALOG_SELECTED')}
:
@@ -178,6 +183,7 @@ const Discover = () => {
{
{
inputsModalOpen ?
-
- {selectInputs.map(({ title, options, selected, renderLabelText, onSelect }, index) => (
-
+ {selectInputs.map(({ title, options, value, onSelect }, index) => (
+
))}
diff --git a/src/routes/Discover/styles.less b/src/routes/Discover/styles.less
index 527238757..a084e4170 100644
--- a/src/routes/Discover/styles.less
+++ b/src/routes/Discover/styles.less
@@ -50,6 +50,7 @@
.select-input {
flex: 0 1 15rem;
+ background-color: var(--overlay-color);
&:not(:first-child) {
margin-left: 1.5rem;
diff --git a/src/routes/Discover/useSelectableInputs.js b/src/routes/Discover/useSelectableInputs.js
index 332218d30..b3b3367f4 100644
--- a/src/routes/Discover/useSelectableInputs.js
+++ b/src/routes/Discover/useSelectableInputs.js
@@ -5,72 +5,70 @@ const { useNavigate } = require('react-router');
const { useTranslate } = require('stremio/common');
const mapSelectableInputs = (discover, t, navigate) => {
+ const selectedType = discover.selectable.types.find(({ selected }) => selected);
const typeSelect = {
- title: t.string('SELECT_TYPE'),
options: discover.selectable.types
.map(({ type, deepLinks }) => ({
value: deepLinks.discover,
label: t.stringWithPrefix(type, 'TYPE_')
})),
- selected: discover.selectable.types
- .filter(({ selected }) => selected)
- .map(({ deepLinks }) => deepLinks.discover),
- renderLabelText: discover.selected !== null ?
- () => t.stringWithPrefix(discover.selected.request.path.type, 'TYPE_')
- :
- null,
- onSelect: (event) => {
- navigate(event.value.replace('#', ''));
+ value: selectedType
+ ? selectedType.deepLinks.discover
+ : undefined,
+ title: discover.selected !== null
+ ? () => t.stringWithPrefix(discover.selected.request.path.type, 'TYPE_')
+ : t.string('SELECT_TYPE'),
+ onSelect: (value) => {
+ navigate(value.replace('#', ''));
}
};
+ const selectedCatalog = discover.selectable.catalogs.find(({ selected }) => selected);
const catalogSelect = {
- title: t.string('SELECT_CATALOG'),
options: discover.selectable.catalogs
.map(({ id, name, addon, deepLinks }) => ({
value: deepLinks.discover,
label: t.catalogTitle({ addon, id, name }),
title: `${name} (${addon.manifest.name})`
})),
- selected: discover.selectable.catalogs
- .filter(({ selected }) => selected)
- .map(({ deepLinks }) => deepLinks.discover),
- renderLabelText: discover.selected !== null ?
- () => {
+ value: discover.selected?.request.path.id
+ ? selectedCatalog.deepLinks.discover
+ : undefined,
+ title: discover.selected !== null
+ ? () => {
const selectableCatalog = discover.selectable.catalogs
.find(({ id }) => id === discover.selected.request.path.id);
return selectableCatalog ? t.catalogTitle(selectableCatalog, false) : discover.selected.request.path.id;
}
:
- null,
+ t.string('SELECT_CATALOG'),
onSelect: (event) => {
navigate(event.value.replace('#', ''));
}
};
- const extraSelects = discover.selectable.extra.map(({ name, isRequired, options }) => ({
- title: t.stringWithPrefix(name, 'SELECT_'),
- isRequired: isRequired,
- options: options.map(({ value, deepLinks }) => ({
- label: typeof value === 'string' ? t.stringWithPrefix(value) : t.string('NONE'),
- value: JSON.stringify({
- href: deepLinks.discover,
- value
- })
- })),
- selected: options
- .filter(({ selected }) => selected)
- .map(({ value, deepLinks }) => JSON.stringify({
- href: deepLinks.discover,
- value
+ const extraSelects = discover.selectable.extra.map(({ name, isRequired, options }) => {
+ const selectedExtra = options.find(({ selected }) => selected);
+ return {
+ isRequired: isRequired,
+ options: options.map(({ value, deepLinks }) => ({
+ label: typeof value === 'string' ? t.string(value) : t.string('NONE'),
+ value: JSON.stringify({
+ href: deepLinks.discover,
+ value
+ })
})),
- renderLabelText: options.some(({ selected, value }) => selected && value === null) ?
- () => t.stringWithPrefix(name, 'SELECT_')
- :
- null,
- onSelect: (event) => {
- const { href } = JSON.parse(event.value);
- navigate(href.replace('#', ''));
- }
- }));
+ value: JSON.stringify({
+ href: selectedExtra.deepLinks.discover,
+ value: selectedExtra.value,
+ }),
+ title: options.some(({ selected, value }) => selected && value === null) ?
+ () => t.string(name.toUpperCase())
+ : t.string(selectedExtra.value),
+ onSelect: (value) => {
+ const { href } = JSON.parse(value);
+ navigate(href.replace('#', ''));
+ }
+ };
+ });
return [[typeSelect, catalogSelect, ...extraSelects], discover.selectable.nextPage];
};
diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js
index a15c104d9..66ab84d69 100644
--- a/src/routes/Intro/Intro.js
+++ b/src/routes/Intro/Intro.js
@@ -302,10 +302,10 @@ const Intro = () => {
- Freedom to Stream
+ {t('WEBSITE_SLOGAN_NEW_NEW')}
- All the Video Content You Enjoy in One Place
+ {t('WEBSITE_SLOGAN_ALL')}
@@ -314,7 +314,7 @@ const Intro = () => {
ref={emailRef}
className={styles['credentials-text-input']}
type={'email'}
- placeholder={'Email'}
+ placeholder={t('EMAIL')}
value={state.email}
onChange={emailOnChange}
onSubmit={emailOnSubmit}
@@ -323,7 +323,7 @@ const Intro = () => {
ref={passwordRef}
className={styles['credentials-text-input']}
type={'password'}
- placeholder={'Password'}
+ placeholder={t('PASSWORD')}
value={state.password}
onChange={passwordOnChange}
onSubmit={passwordOnSubmit}
@@ -335,37 +335,37 @@ const Intro = () => {
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')}
}
{
@@ -375,22 +375,22 @@ const Intro = () => {
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
@@ -398,7 +398,7 @@ const Intro = () => {
{
state.form === LOGIN_FORM ?
- SIGN UP WITH EMAIL
+ {t('SIGN_UP_EMAIL')}
:
null
@@ -406,7 +406,7 @@ const Intro = () => {
{
state.form === SIGNUP_FORM ?
- GUEST LOGIN
+ {t('GUEST_LOGIN')}
:
null
@@ -424,7 +424,7 @@ const Intro = () => {
-
Authenticating...
+
{t('AUTHENTICATING')}
{t('BUTTON_CANCEL')}
diff --git a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js b/src/routes/Intro/PasswordResetModal/PasswordResetModal.js
index 55fcb6e26..1cb83a3c8 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 { default: useRouteFocused } = require('stremio/common/useRouteFocused');
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 urlParams = useParams();
const [queryParams] = useSearchParams();
const navigate = useNavigate();
+ const { t } = useTranslation();
const profile = useProfile();
const notifications = useNotifications();
const [library, loadNextPage] = useLibrary(model, urlParams, queryParams);
@@ -63,14 +65,14 @@ const Library = ({ model }) => {
if (!library.selected?.type && typeSelect.selected) {
navigate(typeSelect.selected[0].replace('#', ''));
}
- }, [typeSelect.selected, library.selected]);
+ }, [typeSelect.value, library.selected]);
return (
{
profile.auth !== null ?
-
+
{
@@ -82,7 +84,7 @@ const Library = ({ model }) => {
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')}
:
@@ -93,7 +95,7 @@ const Library = ({ model }) => {
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/Library/styles.less b/src/routes/Library/styles.less
index 76a16940e..44c0c8851 100644
--- a/src/routes/Library/styles.less
+++ b/src/routes/Library/styles.less
@@ -42,6 +42,7 @@
flex-shrink: 1;
flex-basis: 15rem;
height: 2.75rem;
+ background-color: var(--overlay-color);
&:not(:last-child) {
margin-right: 1.5rem;
diff --git a/src/routes/Library/useSelectableInputs.js b/src/routes/Library/useSelectableInputs.js
index 354d2bd82..7b72f6b10 100644
--- a/src/routes/Library/useSelectableInputs.js
+++ b/src/routes/Library/useSelectableInputs.js
@@ -5,20 +5,16 @@ const { useNavigate } = require('react-router');
const { useTranslate } = require('stremio/common');
const mapSelectableInputs = (library, t, navigate) => {
- const selectedType = library.selectable.types
- .filter(({ selected }) => selected).map(({ deepLinks }) => deepLinks.library);
+ const selectedType = library.selectable.types.find(({ selected }) => selected) || library.selectable.types.find(({ type }) => type === null);
const typeSelect = {
- title: t.string('SELECT_TYPE'),
options: library.selectable.types
.map(({ type, deepLinks }) => ({
value: deepLinks.library,
label: type === null ? t.string('TYPE_ALL') : t.stringWithPrefix(type, 'TYPE_')
})),
- selected: selectedType.length
- ? selectedType
- : [library.selectable.types[0]].map(({ deepLinks }) => deepLinks.library),
- onSelect: (event) => {
- navigate(event.value.replace('#', ''));
+ value: selectedType?.deepLinks.library,
+ onSelect: (value) => {
+ navigate(value.replace('#', ''));
}
};
const sortChips = {
diff --git a/src/routes/MetaDetails/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js
index 98cd2f5d5..10039343a 100644
--- a/src/routes/MetaDetails/MetaDetails.js
+++ b/src/routes/MetaDetails/MetaDetails.js
@@ -2,6 +2,7 @@
const React = require('react');
const { useParams, useLocation, useNavigate } = require('react-router');
+const { useTranslation } = require('react-i18next');
const classnames = require('classnames');
const { useServices } = require('stremio/services');
const { withCoreSuspender } = require('stremio/common');
@@ -17,6 +18,7 @@ const MetaDetails = () => {
const urlParams = useParams();
const location = useLocation();
const navigate = useNavigate();
+ const { t } = useTranslation();
const { core } = useServices();
const metaDetails = useMetaDetails(urlParams);
const [season, setSeason] = useSeason(urlParams);
@@ -132,20 +134,20 @@ const MetaDetails = () => {
-
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 e64958904..45ea707b9 100644
--- a/src/routes/MetaDetails/StreamsList/StreamsList.js
+++ b/src/routes/MetaDetails/StreamsList/StreamsList.js
@@ -6,7 +6,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
-const { Button, Image, Multiselect } = require('stremio/components');
+const { Button, Image, MultiselectMenu } = require('stremio/components');
const { useServices } = require('stremio/services');
const Stream = require('./Stream');
const styles = require('./styles');
@@ -23,9 +23,9 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
const navigate = useNavigate();
const streamsContainerRef = React.useRef(null);
const [selectedAddon, setSelectedAddon] = React.useState(ALL_ADDONS_KEY);
- const onAddonSelected = React.useCallback((event) => {
+ const onAddonSelected = React.useCallback((value) => {
streamsContainerRef.current.scrollTo({ top: 0, left: 0, behavior: platform.name === 'ios' ? 'smooth' : 'instant' });
- setSelectedAddon(event.value);
+ setSelectedAddon(value);
}, [platform]);
const showInstallAddonsButton = React.useMemo(() => {
return !profile || profile.auth === null || profile.auth?.user?.isNewUser === true && !video?.upcoming;
@@ -78,7 +78,6 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
}, [streamsByAddon, selectedAddon]);
const selectableOptions = React.useMemo(() => {
return {
- title: 'Select Addon',
options: [
{
value: ALL_ADDONS_KEY,
@@ -91,7 +90,7 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
title: streamsByAddon[transportUrl].addon.manifest.name,
}))
],
- selected: [selectedAddon],
+ value: selectedAddon,
onSelect: onAddonSelected
};
}, [streamsByAddon, selectedAddon]);
@@ -118,7 +117,7 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
}
{
Object.keys(streamsByAddon).length > 1 ?
-
@@ -135,7 +134,7 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
: null
}
-
No addons were requested for streams!
+
{t('ERR_NO_ADDONS_FOR_STREAMS')}
:
props.streams.every((streams) => streams.content.type === 'Err') ?
diff --git a/src/routes/MetaDetails/StreamsList/styles.less b/src/routes/MetaDetails/StreamsList/styles.less
index 0bffa8fcc..ecfea5162 100644
--- a/src/routes/MetaDetails/StreamsList/styles.less
+++ b/src/routes/MetaDetails/StreamsList/styles.less
@@ -114,7 +114,7 @@
.select-input-container {
min-width: 40%;
flex-grow: 1;
- background: none;
+ background-color: none;
&:hover, &:focus, &:global(.active) {
background-color: var(--overlay-color);
diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js
index 29637a24c..ea870a931 100644
--- a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js
+++ b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js
@@ -13,11 +13,11 @@ 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(() => {
- return { label: String(season), value: String(season) };
+ return String(season);
}, [season]);
const prevNextButtonOnClick = React.useCallback((event) => {
if (typeof onSelect === 'function') {
@@ -56,19 +56,19 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
return (
-
+
- Prev
+ {t('BUTTON_PREV')}
0 ? `${t('SEASON')} ${season}` : t('SPECIAL')}
- selectedOption={selectedSeason}
+ 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/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 eab4052da..abf4c1cb9 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -404,6 +404,13 @@ const Player = () => {
if (!defaultSubtitlesSelected.current) {
const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
+ if (settings.subtitlesLanguage === null) {
+ onSubtitlesTrackSelected(null);
+ onExtraSubtitlesTrackSelected(null);
+ defaultSubtitlesSelected.current = true;
+ return;
+ }
+
const subtitlesTrack = findTrackByLang(video.state.subtitlesTracks, settings.subtitlesLanguage);
const extraSubtitlesTrack = findTrackByLang(video.state.extraSubtitlesTracks, settings.subtitlesLanguage);
diff --git a/src/routes/Player/SideDrawer/SideDrawer.less b/src/routes/Player/SideDrawer/SideDrawer.less
index 2d2178c3e..0fe22f58a 100644
--- a/src/routes/Player/SideDrawer/SideDrawer.less
+++ b/src/routes/Player/SideDrawer/SideDrawer.less
@@ -57,7 +57,7 @@
.info {
padding: @padding;
overflow-y: auto;
- flex: none;
+ flex: 1;
.side-drawer-meta-preview {
.action-buttons-container {
@@ -78,12 +78,6 @@
}
}
-@media screen and (max-width: @small) {
- .side-drawer {
- max-width: 40dvw;
- }
-}
-
@media @phone-portrait {
.side-drawer {
max-width: 100dvw;
diff --git a/src/routes/Player/StatisticsMenu/StatisticsMenu.js b/src/routes/Player/StatisticsMenu/StatisticsMenu.js
index 69ee2bf8d..b5f5232ea 100644
--- a/src/routes/Player/StatisticsMenu/StatisticsMenu.js
+++ b/src/routes/Player/StatisticsMenu/StatisticsMenu.js
@@ -1,20 +1,22 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
+const { useTranslation } = require('react-i18next');
const classNames = require('classnames');
const PropTypes = require('prop-types');
const styles = require('./styles.less');
const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => {
+ const { t } = useTranslation();
return (
- Statistics
+ {t('PLAYER_STATISTICS')}
- Peers
+ {t('PLAYER_PEERS')}
{ peers }
@@ -22,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/Settings/Settings.js b/src/routes/Settings/Settings.js
index 72ff88383..d19c84318 100644
--- a/src/routes/Settings/Settings.js
+++ b/src/routes/Settings/Settings.js
@@ -8,7 +8,7 @@ const { default: Icon } = require('@stremio/stremio-icons/react');
const { default: useRouteFocused } = require('stremio/common/useRouteFocused');
const { useServices } = require('stremio/services');
const { useProfile, usePlatform, useStreamingServer, withCoreSuspender, useToast } = require('stremio/common');
-const { Button, ColorInput, MainNavBars, Multiselect, Toggle } = require('stremio/components');
+const { Button, ColorInput, MainNavBars, MultiselectMenu, Toggle } = require('stremio/components');
const useProfileSettingsInputs = require('./useProfileSettingsInputs');
const useStreamingServerSettingsInputs = require('./useStreamingServerSettingsInputs');
const useDataExport = require('./useDataExport');
@@ -184,20 +184,20 @@ const Settings = () => {
- App Version: {process.env.VERSION}
+ {`${t('SETTINGS_APP_VERSION')}: ${process.env.VERSION}`}
- Build Version: {process.env.COMMIT_HASH}
+ {`${t('SETTINGS_BUILD_VERSION')}: ${process.env.COMMIT_HASH}`}
{
streamingServer.settings !== null && streamingServer.settings.type === 'Ready' ?
-
Server Version: {streamingServer.settings.content.serverVersion}
+
{`${t('SETTINGS_SERVER_VERSION')}: ${streamingServer.settings.content.serverVersion}`}
:
null
}
{
typeof shell?.transport?.props?.shellVersion === 'string' ?
-
Shell Version: {shell.transport.props.shellVersion}
+
{`${t('SETTINGS_APP_VERSION')}: ${shell.transport.props.shellVersion}`}
:
null
}
@@ -219,9 +219,9 @@ const Settings = () => {
}}
/>
-
+
- {profile.auth === null ? 'Anonymous user' : profile.auth.user.email}
+ {profile.auth === null ? t('ANONYMOUS_USER') : profile.auth.user.email}
{
@@ -273,8 +273,8 @@ const Settings = () => {
-
- Source code
+
+ {t('SETTINGS_SOURCE_CODE')}
@@ -310,11 +310,11 @@ const Settings = () => {
-
Trakt Scrobbling
+
{t('SETTINGS_TRAKT')}
-
+
- { profile.auth !== null && profile.auth.user !== null && profile.auth.user.trakt !== null ? t('LOG_OUT') : t('SETTINGS_TRAKT_AUTHENTICATE') }
+ { isTraktAuthenticated ? t('LOG_OUT') : t('SETTINGS_TRAKT_AUTHENTICATE') }
@@ -324,7 +324,7 @@ const Settings = () => {
{ t('SETTINGS_UI_LANGUAGE') }
-
{
{ t('SETTINGS_SUBTITLES_LANGUAGE') }
-
@@ -385,7 +385,7 @@ const Settings = () => {
{ t('SETTINGS_SUBTITLES_SIZE') }
-
@@ -427,7 +427,7 @@ const Settings = () => {
{ t('SETTINGS_DEFAULT_AUDIO_TRACK') }
-
@@ -452,7 +452,7 @@ const Settings = () => {
{ t('SETTINGS_SEEK_KEY') }
-
@@ -461,7 +461,7 @@ const Settings = () => {
{ t('SETTINGS_SEEK_KEY_SHIFT') }
-
@@ -496,7 +496,7 @@ const Settings = () => {
{ t('SETTINGS_NEXT_VIDEO_POPUP_DURATION') }
- {
{ t('SETTINGS_PLAY_IN_EXTERNAL_PLAYER') }
-
@@ -568,7 +568,7 @@ const Settings = () => {
{ t('SETTINGS_HTTPS_ENDPOINT') }
-
@@ -582,7 +582,7 @@ const Settings = () => {
{ t('SETTINGS_SERVER_CACHE_SIZE') }
-
@@ -596,7 +596,7 @@ const Settings = () => {
{ t('SETTINGS_SERVER_TORRENT_PROFILE') }
-
@@ -610,7 +610,7 @@ const Settings = () => {
{ t('SETTINGS_TRANSCODE_PROFILE') }
-
@@ -740,7 +740,7 @@ const Settings = () => {
- App Version
+ {t('SETTINGS_APP_VERSION')}
@@ -752,7 +752,7 @@ const Settings = () => {
- Build Version
+ {t('SETTINGS_BUILD_VERSION')}
@@ -766,7 +766,7 @@ const Settings = () => {
- Server Version
+ {t('SETTINGS_SERVER_VERSION')}
@@ -783,7 +783,7 @@ const Settings = () => {
- Shell Version
+ {t('SETTINGS_SHELL_VERSION')}
diff --git a/src/routes/Settings/URLsManager/URLsManager.tsx b/src/routes/Settings/URLsManager/URLsManager.tsx
index 46e57020d..b0d1245eb 100644
--- a/src/routes/Settings/URLsManager/URLsManager.tsx
+++ b/src/routes/Settings/URLsManager/URLsManager.tsx
@@ -30,7 +30,7 @@ const URLsManager = () => {
return (
-
URL
+
{t('URL')}
{t('STATUS')}
@@ -46,11 +46,11 @@ const URLsManager = () => {
}
-
+
{t('SETTINGS_SERVER_ADD_URL')}
-
+
{t('RELOAD')}
diff --git a/src/routes/Settings/styles.less b/src/routes/Settings/styles.less
index 072dd32a3..2fc2bd893 100644
--- a/src/routes/Settings/styles.less
+++ b/src/routes/Settings/styles.less
@@ -267,6 +267,11 @@
.option-input-container {
padding: 1rem 1.5rem;
+
+ &.multiselect-container {
+ padding: 0;
+ background: var(--overlay-color);
+ }
&.button-container {
justify-content: center;
diff --git a/src/routes/Settings/useProfileSettingsInputs.js b/src/routes/Settings/useProfileSettingsInputs.js
index 2a31fc254..4bbe54059 100644
--- a/src/routes/Settings/useProfileSettingsInputs.js
+++ b/src/routes/Settings/useProfileSettingsInputs.js
@@ -15,17 +15,15 @@ const useProfileSettingsInputs = (profile) => {
value: codes[0],
label: name,
})),
- selected: [
- interfaceLanguages.find(({ codes }) => codes[1] === profile.settings.interfaceLanguage)?.codes?.[0] || profile.settings.interfaceLanguage
- ],
- onSelect: (event) => {
+ 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: event.value
+ interfaceLanguage: value
}
}
});
@@ -65,19 +63,22 @@ const useProfileSettingsInputs = (profile) => {
}), [profile.settings]);
const subtitlesLanguageSelect = React.useMemo(() => ({
- options: Object.keys(languageNames).map((code) => ({
- value: code,
- label: languageNames[code]
- })),
- selected: [profile.settings.subtitlesLanguage],
- onSelect: (event) => {
+ options: [
+ { value: null, label: t('NONE') },
+ ...Object.keys(languageNames).map((code) => ({
+ value: code,
+ label: languageNames[code]
+ }))
+ ],
+ value: profile.settings.subtitlesLanguage,
+ onSelect: (value) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
- subtitlesLanguage: event.value
+ subtitlesLanguage: value
}
}
});
@@ -88,18 +89,18 @@ const useProfileSettingsInputs = (profile) => {
value: `${size}`,
label: `${size}%`
})),
- selected: [`${profile.settings.subtitlesSize}`],
- renderLabelText: () => {
+ value: `${profile.settings.subtitlesSize}`,
+ title: () => {
return `${profile.settings.subtitlesSize}%`;
},
- onSelect: (event) => {
+ onSelect: (value) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
- subtitlesSize: parseInt(event.value, 10)
+ subtitlesSize: parseInt(value, 10)
}
}
});
@@ -107,14 +108,14 @@ const useProfileSettingsInputs = (profile) => {
}), [profile.settings]);
const subtitlesTextColorInput = React.useMemo(() => ({
value: profile.settings.subtitlesTextColor,
- onChange: (event) => {
+ onChange: (value) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
- subtitlesTextColor: event.value
+ subtitlesTextColor: value
}
}
});
@@ -122,14 +123,14 @@ const useProfileSettingsInputs = (profile) => {
}), [profile.settings]);
const subtitlesBackgroundColorInput = React.useMemo(() => ({
value: profile.settings.subtitlesBackgroundColor,
- onChange: (event) => {
+ onChange: (value) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
- subtitlesBackgroundColor: event.value
+ subtitlesBackgroundColor: value
}
}
});
@@ -137,14 +138,14 @@ const useProfileSettingsInputs = (profile) => {
}), [profile.settings]);
const subtitlesOutlineColorInput = React.useMemo(() => ({
value: profile.settings.subtitlesOutlineColor,
- onChange: (event) => {
+ onChange: (value) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
- subtitlesOutlineColor: event.value
+ subtitlesOutlineColor: value
}
}
});
@@ -155,15 +156,15 @@ const useProfileSettingsInputs = (profile) => {
value: code,
label: languageNames[code]
})),
- selected: [profile.settings.audioLanguage],
- onSelect: (event) => {
+ value: profile.settings.audioLanguage,
+ onSelect: (value) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
- audioLanguage: event.value
+ audioLanguage: value
}
}
});
@@ -205,18 +206,18 @@ const useProfileSettingsInputs = (profile) => {
value: `${size}`,
label: `${size / 1000} ${t('SECONDS')}`
})),
- selected: [`${profile.settings.seekTimeDuration}`],
- renderLabelText: () => {
+ value: `${profile.settings.seekTimeDuration}`,
+ title: () => {
return `${profile.settings.seekTimeDuration / 1000} ${t('SECONDS')}`;
},
- onSelect: (event) => {
+ onSelect: (value) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
- seekTimeDuration: parseInt(event.value, 10)
+ seekTimeDuration: parseInt(value, 10)
}
}
});
@@ -227,18 +228,18 @@ const useProfileSettingsInputs = (profile) => {
value: `${size}`,
label: `${size / 1000} ${t('SECONDS')}`
})),
- selected: [`${profile.settings.seekShortTimeDuration}`],
- renderLabelText: () => {
+ value: `${profile.settings.seekShortTimeDuration}`,
+ title: () => {
return `${profile.settings.seekShortTimeDuration / 1000} ${t('SECONDS')}`;
},
- onSelect: (event) => {
+ onSelect: (value) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
- seekShortTimeDuration: parseInt(event.value, 10)
+ seekShortTimeDuration: parseInt(value, 10)
}
}
});
@@ -251,19 +252,19 @@ const useProfileSettingsInputs = (profile) => {
value,
label: t(label),
})),
- selected: [profile.settings.playerType],
- renderLabelText: () => {
+ value: profile.settings.playerType,
+ title: () => {
const selectedOption = CONSTANTS.EXTERNAL_PLAYERS.find(({ value }) => value === profile.settings.playerType);
return selectedOption ? t(selectedOption.label, { defaultValue: selectedOption.label }) : profile.settings.playerType;
},
- onSelect: (event) => {
+ onSelect: (value) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
- playerType: event.value
+ playerType: value
}
}
});
@@ -274,21 +275,21 @@ const useProfileSettingsInputs = (profile) => {
value: `${duration}`,
label: duration === 0 ? 'Disabled' : `${duration / 1000} ${t('SECONDS')}`
})),
- selected: [`${profile.settings.nextVideoNotificationDuration}`],
- renderLabelText: () => {
+ value: `${profile.settings.nextVideoNotificationDuration}`,
+ title: () => {
return profile.settings.nextVideoNotificationDuration === 0 ?
'Disabled'
:
`${profile.settings.nextVideoNotificationDuration / 1000} ${t('SECONDS')}`;
},
- onSelect: (event) => {
+ onSelect: (value) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
- nextVideoNotificationDuration: parseInt(event.value, 10)
+ nextVideoNotificationDuration: parseInt(value, 10)
}
}
});
diff --git a/src/routes/Settings/useStreamingServerSettingsInputs.js b/src/routes/Settings/useStreamingServerSettingsInputs.js
index 1d4eee066..11c60a5af 100644
--- a/src/routes/Settings/useStreamingServerSettingsInputs.js
+++ b/src/routes/Settings/useStreamingServerSettingsInputs.js
@@ -77,15 +77,15 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
value: address,
}))
],
- selected: [streamingServer.settings.content.remoteHttps],
- onSelect: (event) => {
+ value: streamingServer.settings.content.remoteHttps,
+ onSelect: (value) => {
core.transport.dispatch({
action: 'StreamingServer',
args: {
action: 'UpdateSettings',
args: {
...streamingServer.settings.content,
- remoteHttps: event.value,
+ remoteHttps: value,
}
}
});
@@ -103,18 +103,18 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
label: cacheSizeToString(size),
value: JSON.stringify(size)
})),
- selected: [JSON.stringify(streamingServer.settings.content.cacheSize)],
- renderLabelText: () => {
+ value: JSON.stringify(streamingServer.settings.content.cacheSize),
+ title: () => {
return cacheSizeToString(streamingServer.settings.content.cacheSize);
},
- onSelect: (event) => {
+ onSelect: (value) => {
core.transport.dispatch({
action: 'StreamingServer',
args: {
action: 'UpdateSettings',
args: {
...streamingServer.settings.content,
- cacheSize: JSON.parse(event.value),
+ cacheSize: JSON.parse(value),
}
}
});
@@ -140,7 +140,7 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
return {
options: Object.keys(TORRENT_PROFILES)
.map((profileName) => ({
- label: profileName,
+ label: t('TORRENT_PROFILE_' + profileName.replace(' ', '_').toUpperCase()),
value: JSON.stringify(TORRENT_PROFILES[profileName])
}))
.concat(
@@ -152,15 +152,15 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
:
[]
),
- selected: [JSON.stringify(selectedTorrentProfile)],
- onSelect: (event) => {
+ value: JSON.stringify(selectedTorrentProfile),
+ onSelect: (value) => {
core.transport.dispatch({
action: 'StreamingServer',
args: {
action: 'UpdateSettings',
args: {
...streamingServer.settings.content,
- ...JSON.parse(event.value),
+ ...JSON.parse(value),
}
}
});
@@ -183,15 +183,15 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
value: name,
}))
],
- selected: [streamingServer.settings.content.transcodeProfile],
- onSelect: (event) => {
+ value: streamingServer.settings.content.transcodeProfile,
+ onSelect: (value) => {
core.transport.dispatch({
action: 'StreamingServer',
args: {
action: 'UpdateSettings',
args: {
...streamingServer.settings.content,
- transcodeProfile: event.value,
+ transcodeProfile: value,
}
}
});
diff --git a/src/services/KeyboardShortcuts/KeyboardShortcuts.js b/src/services/KeyboardShortcuts/KeyboardShortcuts.js
index 22ef0e41f..4bc4683fc 100644
--- a/src/services/KeyboardShortcuts/KeyboardShortcuts.js
+++ b/src/services/KeyboardShortcuts/KeyboardShortcuts.js
@@ -56,16 +56,6 @@ function KeyboardShortcuts() {
window.history.back();
}
- break;
- }
- case 'KeyF': {
- event.preventDefault();
- if (document.fullscreenElement === document.documentElement) {
- document.exitFullscreen();
- } else {
- document.documentElement.requestFullscreen();
- }
-
break;
}
}
diff --git a/src/types/models/Ctx.d.ts b/src/types/models/Ctx.d.ts
index 47f18749f..e649b305b 100644
--- a/src/types/models/Ctx.d.ts
+++ b/src/types/models/Ctx.d.ts
@@ -35,7 +35,7 @@ type Settings = {
subtitlesBackgroundColor: string,
subtitlesBold: boolean,
subtitlesFont: string,
- subtitlesLanguage: string,
+ subtitlesLanguage: string | null,
subtitlesOffset: number,
subtitlesOutlineColor: string,
subtitlesSize: number,
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
+ });
+ });
+}